mirror of
https://github.com/thelounge/thelounge.git
synced 2026-03-14 22:45:50 +01:00
Compare commits
6,403 commits
v2.0.0-pre
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2e3112806 |
||
|
|
c247ae31ae |
||
|
|
9533ee46b2 | ||
|
|
be603c6bdc |
||
|
|
32f0181433 | ||
|
|
793d01c0f5 | ||
|
|
d7316a0691 | ||
|
|
e919e1ce79 | ||
|
|
857a2bbd6a | ||
|
|
892562902f |
||
|
|
c20e9d2ef0 |
||
|
|
fc135023a4 | ||
|
|
b270f51022 | ||
|
|
59a333d982 | ||
|
|
3f2697cca6 | ||
|
|
f97c4df2a2 | ||
|
|
23fff58fc3 | ||
|
|
a966b711ac | ||
|
|
89b1ca1e3f | ||
|
|
f86a7f6377 | ||
|
|
1760e8b339 | ||
|
|
793313db77 |
||
|
|
61f8ce1ae6 |
||
|
|
4ba16364cc | ||
|
|
e44bed1758 | ||
|
|
59e8b16990 | ||
|
|
381ddca358 |
||
|
|
3d33ef8083 | ||
|
|
0c57694c00 | ||
|
|
834b12218e | ||
|
|
e81273148b |
||
|
|
6d6179c69d | ||
|
|
6f09b6fd70 | ||
|
|
a016db68bb | ||
|
|
2962b07c1f | ||
|
|
e7aff2862a | ||
|
|
ca2c231eec | ||
|
|
a196b1ad95 | ||
|
|
05a87130ee |
||
|
|
a6af6bb45c |
||
|
|
ac485c483b |
||
|
|
0a4adc4592 |
||
|
|
8ed898846c |
||
|
|
374c6a9b04 |
||
|
|
6b30b07fc5 |
||
|
|
e42ad91e04 | ||
|
|
22388087df |
||
|
|
f89288414a |
||
|
|
a61bc14456 | ||
|
|
718db3ae88 | ||
|
|
e2ddabe032 | ||
|
|
6b9c2f5f58 | ||
|
|
74cfff0034 | ||
|
|
40a4434c8c | ||
|
|
322d293f88 | ||
|
|
210d830fd0 | ||
|
|
4990a6d9c6 | ||
|
|
f59ebc7d86 | ||
|
|
1ccce14d01 |
||
|
|
a051a70b02 |
||
|
|
81c8db8374 |
||
|
|
a900943345 |
||
|
|
f3bdec0078 |
||
|
|
0d9c184f19 | ||
|
|
4de413070d |
||
|
|
0955d9df06 | ||
|
|
cb4aaf6a97 | ||
|
|
45c2fc87ee | ||
|
|
29fcc2da05 | ||
|
|
12679081c8 | ||
|
|
0e48014d5a | ||
|
|
4819406af5 | ||
|
|
9e6eef3020 |
||
|
|
33e08390f9 |
||
|
|
74563effa7 |
||
|
|
cbab10f416 |
||
|
|
4dfeb899b4 | ||
|
|
3259ac596d | ||
|
|
3fbbc39cd6 | ||
|
|
9ae9482223 | ||
|
|
a3953405ed | ||
|
|
9086bc648d | ||
|
|
da2572fe25 | ||
|
|
d9977df315 | ||
|
|
cc0aa5e8e5 | ||
|
|
02df78b0f2 | ||
|
|
18b0e06855 | ||
|
|
d5db9c653b | ||
|
|
f7926267d9 | ||
|
|
8eb398c5cc | ||
|
|
36cb75ee99 | ||
|
|
1ec67a6605 | ||
|
|
8372c5a57e | ||
|
|
5567f07a7c | ||
|
|
a200bab8bd | ||
|
|
91ac363cc6 | ||
|
|
6c9d2c36a1 | ||
|
|
6241eed8f4 | ||
|
|
03151e0ab1 | ||
|
|
7f5e0f3ebf | ||
|
|
5e444be37b | ||
|
|
c8664301ba | ||
|
|
1edb5a72c1 | ||
|
|
31d987283a | ||
|
|
4ceafb653f | ||
|
|
f25fee4c6c | ||
|
|
96848c1c1b | ||
|
|
4b07e05491 | ||
|
|
fc9805545b | ||
|
|
82e4150cc8 | ||
|
|
e61e356f1e | ||
|
|
5001d607b1 | ||
|
|
8c41356ae9 | ||
|
|
e2b56cf16b | ||
|
|
92a0affba1 | ||
|
|
edb96f683b | ||
|
|
5c8951ffc3 | ||
|
|
c3fc54e158 | ||
|
|
917fdb2a0a | ||
|
|
b8400a3a46 | ||
|
|
071a5afda6 | ||
|
|
5274fdc21a | ||
|
|
b8a9fe08ab | ||
|
|
a4afa08add | ||
|
|
4614c35486 | ||
|
|
540144c417 | ||
|
|
bb7c3925c6 | ||
|
|
9898f38de6 | ||
|
|
9f2c82e152 | ||
|
|
17ba07db3b | ||
|
|
0311e5f836 | ||
|
|
4d0474b897 | ||
|
|
14b9169899 | ||
|
|
50037644c0 | ||
|
|
7287c6bcaa | ||
|
|
bfca0ca612 | ||
|
|
300bd4c84c | ||
|
|
42ea66c343 | ||
|
|
1565eb8d05 | ||
|
|
29750a3e51 | ||
|
|
3ea5170e6a | ||
|
|
fe4f497fad | ||
|
|
c20cd6bda1 | ||
|
|
1c4ce5d4a5 | ||
|
|
9c4d24d1f7 | ||
|
|
35e38d13c4 | ||
|
|
bf7eb0e727 | ||
|
|
5ee9c2b338 | ||
|
|
e15b121080 | ||
|
|
98452ccc18 | ||
|
|
a8e7022d04 | ||
|
|
60486bf5e3 | ||
|
|
46f3fd9682 | ||
|
|
56215382a3 | ||
|
|
9ab9ad0f56 | ||
|
|
0660a8772c | ||
|
|
f5c691f37b | ||
|
|
0067c30273 | ||
|
|
843db1727b | ||
|
|
e9ef59b641 | ||
|
|
fceffd42b9 | ||
|
|
b89b0cad53 | ||
|
|
c869ea9a73 | ||
|
|
9aee3e3e98 | ||
|
|
636b5c5b04 | ||
|
|
6984e8f25a | ||
|
|
e43cbb139c | ||
|
|
e57e547b74 | ||
|
|
3217536245 | ||
|
|
194b4e1a2f | ||
|
|
88c8830a17 | ||
|
|
7073584f1c | ||
|
|
8e6920af1d | ||
|
|
7bc184b252 | ||
|
|
4d237600d5 | ||
|
|
383907c2b8 | ||
|
|
f0ee3be6fb | ||
|
|
12a0b0b6f9 | ||
|
|
d716402da2 | ||
|
|
d0b71aba32 | ||
|
|
3f0ee6a961 | ||
|
|
b67e4699f5 | ||
|
|
68ba13ca12 | ||
|
|
3eb19135f5 | ||
|
|
549c445853 |
||
|
|
2466c1b1e4 | ||
|
|
f5867c3643 | ||
|
|
231c498def | ||
|
|
eeaec413d6 | ||
|
|
515f894c13 | ||
|
|
e8f6ba5b08 |
||
|
|
07276bbde4 |
||
|
|
9ad92e1860 |
||
|
|
7923d4a2cd |
||
|
|
9248358169 |
||
|
|
6ab52bc9a9 |
||
|
|
48213955b9 | ||
|
|
682b3b91aa | ||
|
|
be3e27aa19 | ||
|
|
c09f751552 | ||
|
|
fb5864ee00 | ||
|
|
3bd5b704c7 | ||
|
|
139ce47b73 | ||
|
|
45563d9a59 |
||
|
|
e2fda1fb84 |
||
|
|
a77fbb894f |
||
|
|
fe50a90235 |
||
|
|
a8be84028c | ||
|
|
25e55ce75c |
||
|
|
113e9bd2fb | ||
|
|
2b146ba3e6 |
||
|
|
f95dd29a0d |
||
|
|
91dc719c93 |
||
|
|
5af893db3a |
||
|
|
daabb76781 |
||
|
|
393d0a63b7 | ||
|
|
037fc479b8 | ||
|
|
646bafab99 | ||
|
|
d4c77c74f6 | ||
|
|
eeefeb229c | ||
|
|
29c5323bfd | ||
|
|
a12ddc75d8 | ||
|
|
dd24cb1300 | ||
|
|
ae6bae69ac | ||
|
|
b5372e3ed7 |
||
|
|
d15998d919 | ||
|
|
436bf6a180 | ||
|
|
1d2fdd95b0 |
||
|
|
eaa70caad7 |
||
|
|
aa95032760 |
||
|
|
e636121d7a |
||
|
|
083abae750 |
||
|
|
01cfe3d19d |
||
|
|
7f0b721790 | ||
|
|
edb1226b47 | ||
|
|
b0ca8e51fb | ||
|
|
21b1152f53 | ||
|
|
74aff7ee5a | ||
|
|
14d9ff247d | ||
|
|
aec8d0b033 | ||
|
|
60ddf17124 | ||
|
|
20227b174c |
||
|
|
d18182da8b | ||
|
|
ea35040b42 | ||
|
|
97f553eea8 | ||
|
|
6603c1a6e6 |
||
|
|
73a529acea |
||
|
|
2f40d9dbcc |
||
|
|
d1561f8ebc | ||
|
|
ec75ff00cb | ||
|
|
884a92c74b | ||
|
|
77b64c546b | ||
|
|
cc59e6b578 | ||
|
|
fb1d79f5fa | ||
|
|
100ff3c198 | ||
|
|
d893feff1c | ||
|
|
88a5fef4ea | ||
|
|
5b64ecbe68 | ||
|
|
5024acd7dc | ||
|
|
bbfada251c | ||
|
|
8cec292f2c | ||
|
|
22ae594cc3 | ||
|
|
1c6bec2323 | ||
|
|
9105fbc23a | ||
|
|
8c54cd50d8 | ||
|
|
59de6afd3f |
||
|
|
b506966b08 |
||
|
|
785ec0a0e2 |
||
|
|
250433c875 |
||
|
|
d4d5a8e386 |
||
|
|
bcca111a4d |
||
|
|
b686059c6b |
||
|
|
ff77a33663 |
||
|
|
d308e74183 |
||
|
|
f999db99c7 | ||
|
|
76c896aea2 | ||
|
|
08413c7b6b |
||
|
|
48301b1ca3 |
||
|
|
03795a2718 | ||
|
|
2985727996 |
||
|
|
9f05a75c39 |
||
|
|
c0b38d4762 | ||
|
|
2878f87879 | ||
|
|
447a237fc6 |
||
|
|
430a865e9f | ||
|
|
816b7686e3 | ||
|
|
2e019a2fdb | ||
|
|
4f9ca3e192 | ||
|
|
57c4d5513c | ||
|
|
3e21bfcbea | ||
|
|
607b9fc96a | ||
|
|
1a1153aed6 | ||
|
|
54ff563247 |
||
|
|
ed0a47fe2c | ||
|
|
3af4ad1076 | ||
|
|
79fae26f39 | ||
|
|
c6b1913b91 | ||
|
|
071ad96d9b | ||
|
|
2ef8b37009 | ||
|
|
8aa5e33b1d | ||
|
|
43a2b397a2 | ||
|
|
c43a47afc1 | ||
|
|
14575c94cf | ||
|
|
303f53fe72 |
||
|
|
06f1387f7b |
||
|
|
c5326e8795 | ||
|
|
355c5d6fa4 | ||
|
|
7ac2a6fd77 | ||
|
|
c4879fdbba |
||
|
|
4255c1cdec | ||
|
|
ae9d312b2a | ||
|
|
f7c6ba5eb1 |
||
|
|
4d60d9c282 | ||
|
|
af49ef21ea |
||
|
|
7a9ddc01e1 | ||
|
|
8f08cf3d0b | ||
|
|
e05871fd2f | ||
|
|
ede48ab034 | ||
|
|
4c6fa550aa | ||
|
|
9388960497 | ||
|
|
7bce779254 | ||
|
|
a7b85db990 | ||
|
|
f4ef11de3f | ||
|
|
3a63484762 | ||
|
|
04b2bf036b | ||
|
|
3066f48a69 |
||
|
|
ed40c83a2b |
||
|
|
f21f665384 | ||
|
|
12d9ef34f0 | ||
|
|
9ee1cf13a8 | ||
|
|
ba1a4206a6 | ||
|
|
59cf29ef4a | ||
|
|
8e43d8083d | ||
|
|
3cd0a75ac2 | ||
|
|
21d1dbaad6 | ||
|
|
90ad06a29a | ||
|
|
0c7cc85184 | ||
|
|
3be805bd38 | ||
|
|
e25c296901 | ||
|
|
4babd17383 |
||
|
|
b408843ff1 |
||
|
|
0f3487c533 | ||
|
|
21ada132b1 | ||
|
|
2f162daee1 | ||
|
|
3ac9c36d95 | ||
|
|
c30da27f95 | ||
|
|
30a3ba489a | ||
|
|
0dca3954f4 | ||
|
|
e8b6434144 | ||
|
|
edc6f77c64 |
||
|
|
0dd74a93bf | ||
|
|
4e954b919c | ||
|
|
eb509f7100 | ||
|
|
845dabad53 | ||
|
|
6b00ccf82b | ||
|
|
34a01c2dd1 |
||
|
|
320075e376 | ||
|
|
d58fb84565 | ||
|
|
a049a01aeb | ||
|
|
76098d7e76 | ||
|
|
a67cee1ee4 |
||
|
|
efd24fd12c | ||
|
|
bc4c3082b8 |
||
|
|
d471a4c959 | ||
|
|
4831c20804 | ||
|
|
eddcbcc766 | ||
|
|
0183d89384 | ||
|
|
95e56300db | ||
|
|
8e249d46af |
||
|
|
50e8d2a890 |
||
|
|
7f6059d5b7 | ||
|
|
8ca9ee873b | ||
|
|
402332340b | ||
|
|
4742a07721 |
||
|
|
2f8dc01930 |
||
|
|
fade6a8d2e | ||
|
|
dfed1dd757 | ||
|
|
d67277d996 | ||
|
|
95aaba43fa | ||
|
|
3e7255ff20 | ||
|
|
86e376fc03 | ||
|
|
899762cddd | ||
|
|
063aca948c | ||
|
|
25642fbe98 | ||
|
|
c2e7390127 |
||
|
|
d10a59395c |
||
|
|
8fc696620f |
||
|
|
c6a202d6ab |
||
|
|
7c9ed14909 |
||
|
|
bdc1f23107 | ||
|
|
e9a09f5447 | ||
|
|
d93cd88dd5 | ||
|
|
2f04150461 | ||
|
|
c816e4053e |
||
|
|
4cff2ccabe |
||
|
|
26b7fbf2c0 |
||
|
|
243cb10e2a |
||
|
|
7304acd8e0 |
||
|
|
511209a100 | ||
|
|
2ce374fe85 | ||
|
|
00366967ae |
||
|
|
f2c59c23e2 | ||
|
|
90d17cacc1 |
||
|
|
12c03a868d | ||
|
|
b7540b5827 | ||
|
|
6f13735a7f | ||
|
|
60bb561e49 | ||
|
|
e305e23c43 | ||
|
|
9d34955836 | ||
|
|
a8149c0f1a | ||
|
|
21d1eea6b8 | ||
|
|
e1ae79cb9c | ||
|
|
429efb0c3c | ||
|
|
c3e3322a79 | ||
|
|
e31c95e32d | ||
|
|
f785acb07d |
||
|
|
bde5c3d443 |
||
|
|
375164ca88 | ||
|
|
7f3ac62e0d | ||
|
|
ce3ad56ced | ||
|
|
efd3b64564 | ||
|
|
6b23b87063 | ||
|
|
502fb7a705 | ||
|
|
c854d27d3d | ||
|
|
2803018c5a |
||
|
|
0ebc3a574c | ||
|
|
958a948456 | ||
|
|
52b8a2a78e | ||
|
|
661d5cb5b0 | ||
|
|
e597e75847 | ||
|
|
8b1a4f72fa | ||
|
|
502780c5a3 | ||
|
|
073a38ef1e |
||
|
|
c67df36a29 | ||
|
|
d50296385f | ||
|
|
068de0c10c | ||
|
|
d61ab7e7a0 |
||
|
|
2d4143b779 | ||
|
|
f55f772659 | ||
|
|
982816ff20 | ||
|
|
8204c3481a | ||
|
|
deeea274da | ||
|
|
d34b58811a | ||
|
|
dfb4217167 | ||
|
|
f8eb0ebafd |
||
|
|
fd14b4a172 | ||
|
|
1597c2c56e | ||
|
|
4c7337b625 | ||
|
|
0765d209f2 | ||
|
|
7ee4b80a6e | ||
|
|
21c8b0d17f | ||
|
|
89245455ce |
||
|
|
d4bbd9191c | ||
|
|
5037383c4c |
||
|
|
83e11b0143 | ||
|
|
51c9ce078d | ||
|
|
8095d9e88a | ||
|
|
221884166d |
||
|
|
19307d05e7 |
||
|
|
dfe288ef16 | ||
|
|
b5ea7cceb3 | ||
|
|
0ad033fe0a | ||
|
|
5a4a39b9d1 | ||
|
|
cb17f8d87f | ||
|
|
5a803ccd23 | ||
|
|
53f6041f42 |
||
|
|
dca202427a | ||
|
|
6b617f893d | ||
|
|
d62dd3e62d | ||
|
|
f068fd4290 | ||
|
|
bbe81bb2fa | ||
|
|
f04a06682d | ||
|
|
5e1cbe32f9 | ||
|
|
ee8223c200 | ||
|
|
cc3302e874 | ||
|
|
89ee537364 | ||
|
|
e62b169a6a | ||
|
|
f6b292107e | ||
|
|
bea4545abf | ||
|
|
cebc6d069f | ||
|
|
0fa203569a |
||
|
|
30e9f45fac | ||
|
|
117c5fa3fd |
||
|
|
621fa92036 | ||
|
|
11f7ae98be | ||
|
|
a95ab55154 | ||
|
|
38bccd3635 | ||
|
|
3240997347 | ||
|
|
57ed37c1fd |
||
|
|
0495761c44 |
||
|
|
520646a212 | ||
|
|
0cb4791cd0 | ||
|
|
740618ca49 | ||
|
|
e97216518a | ||
|
|
31739b8ac9 | ||
|
|
e221e708c1 | ||
|
|
c8cd4057bc |
||
|
|
d6e1af0e7d | ||
|
|
d72d8694bb |
||
|
|
80f65c5b72 |
||
|
|
bc709af9fe |
||
|
|
e7d18a91c0 |
||
|
|
ddcee5371a |
||
|
|
194b85be4d |
||
|
|
f715c833e7 |
||
|
|
a15ac88ff2 | ||
|
|
4af5fc6f33 |
||
|
|
dd05ee3a65 |
||
|
|
2e3d9a6265 |
||
|
|
c205b89523 |
||
|
|
5f7acbf994 |
||
|
|
d4cc2dd361 |
||
|
|
38f13525e6 |
||
|
|
99c48dbcea |
||
|
|
9dbb6e5e19 |
||
|
|
791205d4f0 | ||
|
|
437dd1667d |
||
|
|
24bdc46b0a |
||
|
|
5a383814f6 |
||
|
|
1f39e078f4 |
||
|
|
6f64243671 |
||
|
|
31b67b7786 |
||
|
|
abf8906757 | ||
|
|
c8115e22ac | ||
|
|
aa7db1e7f7 | ||
|
|
da02350725 |
||
|
|
c9c8cadb1a | ||
|
|
3726a8d00b |
||
|
|
605b75c6ed |
||
|
|
5e8adafb3e |
||
|
|
487d880d32 |
||
|
|
7cb8d33122 |
||
|
|
bbe103ca6f | ||
|
|
ec757c9b69 | ||
|
|
7b725ea55c | ||
|
|
0d12be138b | ||
|
|
7db0d4619d |
||
|
|
bdd6e71049 |
||
|
|
57b1e51e9f |
||
|
|
20ed3e6dc5 |
||
|
|
e4840b4d75 |
||
|
|
d7bba325a7 |
||
|
|
815319810c |
||
|
|
37d7de7671 |
||
|
|
e362704f6b |
||
|
|
48f2b79c37 |
||
|
|
3a84290314 |
||
|
|
ff886846a8 |
||
|
|
b2a363f099 |
||
|
|
3796485217 |
||
|
|
3202b79990 |
||
|
|
a42325d801 |
||
|
|
bbc7280c41 |
||
|
|
b76058e4cf | ||
|
|
ace09d434c | ||
|
|
4d9442d9e3 |
||
|
|
56bf078e29 |
||
|
|
9f7a2e942b |
||
|
|
f440b67dbe | ||
|
|
ae7020f569 | ||
|
|
38fa3bee22 |
||
|
|
2e1b2d44f6 |
||
|
|
69f3501165 |
||
|
|
8a92bc9fb9 |
||
|
|
7cf95d3cbd |
||
|
|
53f5b8e991 |
||
|
|
d145fb3738 |
||
|
|
551f85ea51 |
||
|
|
66455f2c40 |
||
|
|
c12dd6c740 |
||
|
|
cb28204517 | ||
|
|
e2e050d3c3 | ||
|
|
17b174dddb | ||
|
|
027c5b4ff7 | ||
|
|
1ed4f57afc |
||
|
|
53b4d00732 | ||
|
|
ba210e853b | ||
|
|
bd2a6cc5be | ||
|
|
bcd4a060ec | ||
|
|
ed3ec6a560 | ||
|
|
8edec1a5a8 | ||
|
|
9dfb2a3fdb |
||
|
|
4be9a282fa |
||
|
|
337bfa489b |
||
|
|
1e3a7b1250 | ||
|
|
3fb18717a7 |
||
|
|
76cbec9ac6 |
||
|
|
734f5b18d3 |
||
|
|
d228a8c4f4 |
||
|
|
f07d6b1ea4 |
||
|
|
d0fab98c1d |
||
|
|
d4d139505f |
||
|
|
9528515647 | ||
|
|
c0b81902f5 | ||
|
|
a86fa168b8 |
||
|
|
3e387156f7 | ||
|
|
7e0afc90fd | ||
|
|
dcce9eba25 |
||
|
|
b1aa8528a4 |
||
|
|
4489d5c8b8 |
||
|
|
1f8881a1d7 | ||
|
|
c7e504eeab | ||
|
|
4db2d28216 | ||
|
|
be498e8f93 |
||
|
|
e999171f29 | ||
|
|
0d209fce09 | ||
|
|
acf520bd9a |
||
|
|
26c2562124 | ||
|
|
763047889d | ||
|
|
e0bbf19d9d |
||
|
|
0fce974f2c |
||
|
|
cd7916b6d9 |
||
|
|
2c79d53c6d |
||
|
|
514c6fbf95 | ||
|
|
1953e03253 | ||
|
|
981de663fb |
||
|
|
2c2dd1c76f | ||
|
|
ecc0b9183e |
||
|
|
4065d5de97 |
||
|
|
1c08b6dce6 | ||
|
|
0c50c2d274 | ||
|
|
96c2d2419b | ||
|
|
304d207820 |
||
|
|
35d8f4e212 |
||
|
|
3c70fab7c6 |
||
|
|
0ff9703a28 | ||
|
|
dc3a387120 | ||
|
|
212212fe70 |
||
|
|
9b9b357001 |
||
|
|
684d7f2db4 |
||
|
|
8040945913 |
||
|
|
168f2ba46b |
||
|
|
d9f2fed398 |
||
|
|
84d779a4d0 |
||
|
|
324fb9023e |
||
|
|
cda3bb4e7c |
||
|
|
117792fb4d |
||
|
|
c69588dd10 |
||
|
|
361af7f514 |
||
|
|
5b76ec45ee |
||
|
|
d596c0cee5 |
||
|
|
1bb5b74236 |
||
|
|
62fd807f78 |
||
|
|
5d6746c9c4 |
||
|
|
60f4b3a434 |
||
|
|
a05dd6c612 |
||
|
|
244daea66c |
||
|
|
5f78574ecd |
||
|
|
e39e9d2f8a |
||
|
|
98e38c8947 |
||
|
|
40fb2190fa |
||
|
|
1160517c2c |
||
|
|
f719027566 |
||
|
|
cffb838284 |
||
|
|
b02001c079 |
||
|
|
79e56d1c4b |
||
|
|
b54cdf7880 |
||
|
|
cb404cd986 |
||
|
|
1d5291929c |
||
|
|
172cd63739 |
||
|
|
368f3f910b |
||
|
|
5f7ec9e8da |
||
|
|
4419029d2e |
||
|
|
af96f7771c |
||
|
|
315198ac0b |
||
|
|
f4096234d4 |
||
|
|
bfdbbce77d |
||
|
|
9dbf647f7e |
||
|
|
6dfd51bb57 |
||
|
|
371ebfb810 |
||
|
|
c439e51617 |
||
|
|
58110189fe |
||
|
|
54d1be6b29 |
||
|
|
1199183157 |
||
|
|
40a5ee70b6 |
||
|
|
3cec329e3b |
||
|
|
25d493453e |
||
|
|
f3af454c9e |
||
|
|
186f8f68cd |
||
|
|
59280cfdfd |
||
|
|
67503efd21 | ||
|
|
7ba977d56a |
||
|
|
2a901b3475 |
||
|
|
2777cc2db9 |
||
|
|
979dfaf3eb |
||
|
|
9592563a27 |
||
|
|
0381cd11bf |
||
|
|
b5e99c0489 |
||
|
|
ea619f5463 | ||
|
|
3cab39c59b |
||
|
|
fd730eeeb1 |
||
|
|
a8d438261a |
||
|
|
3bb8d2f4b8 |
||
|
|
80e0e0fd16 |
||
|
|
3da5e8e8ca |
||
|
|
411ce5d2f8 |
||
|
|
602de668ee | ||
|
|
393d4fe591 | ||
|
|
a3a9a2cdd9 |
||
|
|
044cd2403b | ||
|
|
544146d9aa | ||
|
|
97f3800785 |
||
|
|
8ab486ef0f | ||
|
|
cf18d04f06 | ||
|
|
5d7e62ed67 |
||
|
|
206d554ce1 |
||
|
|
578b1947e2 |
||
|
|
56d4a6afde | ||
|
|
3ba7fb6de4 |
||
|
|
21c6abdd1d |
||
|
|
80acbc7c06 |
||
|
|
1e896a9672 | ||
|
|
5d76ed888c |
||
|
|
2b634a6ba6 |
||
|
|
2693db4274 |
||
|
|
1d33e0195a |
||
|
|
fcffab1259 |
||
|
|
02ccbc1f69 | ||
|
|
bb4ab4f168 | ||
|
|
8a57f90b65 |
||
|
|
9a0ba1da6c |
||
|
|
5c614785bf | ||
|
|
a48f449c59 | ||
|
|
2ab671664e |
||
|
|
91a0815bb5 | ||
|
|
ebe39b26dc | ||
|
|
7b28d3c0f8 |
||
|
|
324f3aa30f |
||
|
|
e9f0313892 |
||
|
|
969d3e4ec1 |
||
|
|
7873847a7e |
||
|
|
cc0dc6266e |
||
|
|
535ac7ca39 |
||
|
|
c8cdadeb02 |
||
|
|
beb5530c65 |
||
|
|
8fcd079204 |
||
|
|
0a6c33af57 |
||
|
|
162b42d9b0 |
||
|
|
03d38812e3 |
||
|
|
35fcacb767 |
||
|
|
d96704835a |
||
|
|
0d839c501e |
||
|
|
24316fc304 |
||
|
|
11ba27d809 |
||
|
|
a59c5d65fb |
||
|
|
7fdd363ee8 |
||
|
|
75cf4445c4 |
||
|
|
18b003db9c |
||
|
|
372d74db69 |
||
|
|
bbda392c3d |
||
|
|
23f6886cc1 |
||
|
|
521426bb05 |
||
|
|
69c37a535b |
||
|
|
98e8640932 |
||
|
|
998f8d2beb |
||
|
|
058b3155d0 |
||
|
|
e0e12c1960 |
||
|
|
16177eb9f4 |
||
|
|
5e0a12b124 |
||
|
|
6439afd5c6 |
||
|
|
4dacaa46f3 |
||
|
|
426841e6b7 |
||
|
|
22801a629e |
||
|
|
47b151ab51 |
||
|
|
d05cf5fe62 |
||
|
|
3e4b22255d |
||
|
|
cc97d91ef8 |
||
|
|
c5e18e3cdd | ||
|
|
79c57ebf38 |
||
|
|
d106889127 |
||
|
|
b33fd78ed7 |
||
|
|
bec25f6243 | ||
|
|
646a98270a |
||
|
|
5a7781eabc |
||
|
|
cbe81968ee | ||
|
|
a42a1fc6a2 |
||
|
|
a2d23810bf |
||
|
|
aa310fe877 |
||
|
|
a046bfe8d1 |
||
|
|
6b852d14c8 |
||
|
|
5a9f3c5f70 |
||
|
|
f23cc0712c |
||
|
|
7107372a6f |
||
|
|
867fff33c0 |
||
|
|
e5a6554c9a |
||
|
|
38c0c343c3 |
||
|
|
53b7c46e69 |
||
|
|
e7a8476cfe |
||
|
|
a3f0314f6b |
||
|
|
3fdc42350e | ||
|
|
243f514243 | ||
|
|
a93ccd680f | ||
|
|
53a7227e2e |
||
|
|
8bb2fbbf15 |
||
|
|
0fa37a6a05 |
||
|
|
d5b6a8521f |
||
|
|
c5fcc5d72f | ||
|
|
9ec02d1e91 |
||
|
|
beb9bcd8d4 |
||
|
|
8fc7a6c0df |
||
|
|
6182d23758 |
||
|
|
c369a764ed | ||
|
|
4d310cd545 | ||
|
|
fa854fde78 |
||
|
|
6f7fd80044 |
||
|
|
28c413319f |
||
|
|
af236dd280 |
||
|
|
58217cffb1 |
||
|
|
fc6c916e7c |
||
|
|
ad8a315cf9 |
||
|
|
42bafe7165 |
||
|
|
df5befb60e |
||
|
|
db807d0c56 |
||
|
|
ab0d9e6200 |
||
|
|
adf1b5abec |
||
|
|
2c30293ad2 |
||
|
|
042cfb7582 |
||
|
|
dbf6ff064b | ||
|
|
7b1cb88658 |
||
|
|
7b298cf439 |
||
|
|
a985d763d0 |
||
|
|
d097370316 |
||
|
|
a3229f1cdf | ||
|
|
cadcc4b97c |
||
|
|
24a738d521 |
||
|
|
b95643e1a6 |
||
|
|
3f984fad4b |
||
|
|
9b4f55bdb6 |
||
|
|
abcad094d1 |
||
|
|
0bfcd955e3 |
||
|
|
04cf2277d9 |
||
|
|
26a38b12ab |
||
|
|
bc7a920de5 |
||
|
|
78da0eb674 |
||
|
|
db8102b058 |
||
|
|
4b96682d7f |
||
|
|
11aa52687c |
||
|
|
bd4e821614 |
||
|
|
c5f6b4617f |
||
|
|
c66f9c885e |
||
|
|
bb41871873 |
||
|
|
115d970604 |
||
|
|
ef710a2631 |
||
|
|
ddff3ac162 |
||
|
|
0aabacd549 |
||
|
|
0fb6dae8a6 |
||
|
|
ee43e7bdf4 |
||
|
|
e010fe47cc |
||
|
|
89390b3fc5 |
||
|
|
c2c66031c0 |
||
|
|
846da41b01 |
||
|
|
3a6ac4e5ec |
||
|
|
1b13905195 |
||
|
|
13d4f035df |
||
|
|
3fb9c8523a |
||
|
|
544594a7ad |
||
|
|
e36ae64c83 |
||
|
|
be141bea65 |
||
|
|
40aaa17c9b |
||
|
|
d6a23061fc |
||
|
|
de86c144b5 |
||
|
|
fe0178c0d2 |
||
|
|
49cd90d0e9 |
||
|
|
e6856a9e7d |
||
|
|
283ef445e5 |
||
|
|
08f45eabb2 |
||
|
|
db9eb05dfa |
||
|
|
df4f78098c |
||
|
|
0ccbb90d98 |
||
|
|
3a42b5385e |
||
|
|
14d76f8023 |
||
|
|
a5e9463431 |
||
|
|
f213a8973c |
||
|
|
9382beb3b1 |
||
|
|
c6d7bd4b4a | ||
|
|
8dd9bc0e98 |
||
|
|
7df94f01a7 |
||
|
|
d94d09f4ba |
||
|
|
d248f98618 |
||
|
|
5cef511469 |
||
|
|
c6282b0a50 |
||
|
|
1913a3ade6 |
||
|
|
386f90614b |
||
|
|
d1995a0f7d |
||
|
|
aede86bc98 |
||
|
|
a496ba8cfc | ||
|
|
d600a10f48 |
||
|
|
500034ff5d |
||
|
|
e4069f8ce9 |
||
|
|
4f6659897f |
||
|
|
5329483a40 |
||
|
|
dc0e233fe0 | ||
|
|
6b074a6660 |
||
|
|
57bce195de |
||
|
|
34086369db | ||
|
|
8ce947130b |
||
|
|
70fcf6f3ee |
||
|
|
f0a3611d1e |
||
|
|
df6226c7ca |
||
|
|
8dfa7305b3 |
||
|
|
850b49d802 |
||
|
|
8a14b75a47 |
||
|
|
c94ace5843 |
||
|
|
19c7a513f1 |
||
|
|
fbad88f9da |
||
|
|
2f29089bbf |
||
|
|
afe136fee8 |
||
|
|
9474cd96d3 |
||
|
|
c782ca5b93 |
||
|
|
e6fc726c91 | ||
|
|
7c17662fea | ||
|
|
f99e4eef77 |
||
|
|
c974ecb14a |
||
|
|
ab66c3f487 |
||
|
|
32ec420763 |
||
|
|
7fef41131a |
||
|
|
de12699fa6 | ||
|
|
d09f6f6144 |
||
|
|
27195dd34a |
||
|
|
b29850b2d0 |
||
|
|
02c0290ee3 |
||
|
|
43a70df1b1 |
||
|
|
247f20c8ef |
||
|
|
6fc72624aa |
||
|
|
3d1834cc5e |
||
|
|
eb056c4997 |
||
|
|
9aadf1a739 |
||
|
|
03377c6ced |
||
|
|
877e4acf7d |
||
|
|
aa84e13656 |
||
|
|
0e7a5f5c9b | ||
|
|
8fa8eed1e5 | ||
|
|
b2d5cdd4fc |
||
|
|
4529118fd9 |
||
|
|
651a7ac2e9 | ||
|
|
51b0ec1e98 | ||
|
|
ee16d98a94 |
||
|
|
2bbad443c0 |
||
|
|
a4f4d23693 |
||
|
|
a76e75f609 | ||
|
|
69986b3ee5 | ||
|
|
c2e8eaf9df | ||
|
|
41831d18b1 |
||
|
|
800fc95278 | ||
|
|
3e9262a345 | ||
|
|
a9fb563c01 | ||
|
|
e7a8258ac0 | ||
|
|
0322c043e3 |
||
|
|
e790a72e59 | ||
|
|
6ca3bae73e |
||
|
|
c89b2bb0d6 | ||
|
|
1c004cbd17 | ||
|
|
02357ab9de | ||
|
|
d4bf0e365f |
||
|
|
61ebd65367 | ||
|
|
e622662c16 | ||
|
|
7ee0732f56 | ||
|
|
75926432d0 | ||
|
|
3fde2aa7b9 | ||
|
|
27b3e50a64 |
||
|
|
b9540636de | ||
|
|
eef782fd2c |
||
|
|
5b602c72dc | ||
|
|
570890f2f9 | ||
|
|
ea5c95ac94 | ||
|
|
b74b692391 | ||
|
|
ac842108f3 |
||
|
|
12ceb10c75 |
||
|
|
037f09a22f |
||
|
|
f66ee9473a |
||
|
|
2194f91a55 |
||
|
|
df115333ba |
||
|
|
4307d2da9d |
||
|
|
c89dcca449 | ||
|
|
1df4dfad4a |
||
|
|
8fb6f291f8 |
||
|
|
fedaada5a9 |
||
|
|
381b6904e6 | ||
|
|
86e570efb2 | ||
|
|
1e38262d69 | ||
|
|
9e13694b21 | ||
|
|
7bf4f68ff8 | ||
|
|
19e7017d31 | ||
|
|
b398a0696b | ||
|
|
14ed73ed9b |
||
|
|
b97b145df1 | ||
|
|
c29ae50392 |
||
|
|
3557bf00fd |
||
|
|
67e4a4bbb2 | ||
|
|
f63f1abb7c | ||
|
|
1ef7d5ed49 | ||
|
|
928436a9ce |
||
|
|
5861ffadf2 | ||
|
|
2d88ae7503 |
||
|
|
82c83c5f18 |
||
|
|
19d6b7d98f | ||
|
|
d588ecea58 | ||
|
|
b6782da837 |
||
|
|
8bf55527ed | ||
|
|
2a11c07ba9 |
||
|
|
5720c98869 |
||
|
|
7fce28ad90 |
||
|
|
8c6460b58a | ||
|
|
70937d29e0 | ||
|
|
f1fc7a8968 |
||
|
|
40954c9a3a | ||
|
|
7d6f98d974 |
||
|
|
d658b7dfd5 |
||
|
|
87299bb893 | ||
|
|
0dba477eb3 |
||
|
|
9a5d80cecc |
||
|
|
89165d798b | ||
|
|
ec65fd17af |
||
|
|
5a1963647e |
||
|
|
d6cace3959 |
||
|
|
9502b6adf0 |
||
|
|
a8a2bd7755 |
||
|
|
a0cfa4900e |
||
|
|
3e26611e9f | ||
|
|
aaaf498ada |
||
|
|
bf12d7f4c3 |
||
|
|
07e4663e02 |
||
|
|
1de351794d |
||
|
|
a7f4008ec8 |
||
|
|
9e77dc3cca |
||
|
|
1ec728c2b0 |
||
|
|
ccea8a35f2 |
||
|
|
d63a85a15c |
||
|
|
2f434be75d | ||
|
|
a2c1d1175b | ||
|
|
181a198994 | ||
|
|
63a420ac21 | ||
|
|
3e5933bfd3 |
||
|
|
0ac1fcb471 |
||
|
|
531ea920e0 | ||
|
|
cf4a776a93 |
||
|
|
4f7dd37303 |
||
|
|
c0258b847e |
||
|
|
af65f11b68 |
||
|
|
79beff1d8a |
||
|
|
0b7c76ef49 |
||
|
|
970208b470 |
||
|
|
21c496d534 |
||
|
|
b7c5f2031c | ||
|
|
b1115475bf | ||
|
|
f979c72ca7 |
||
|
|
5e6b5f7400 | ||
|
|
4becb152bb | ||
|
|
761d482572 |
||
|
|
189f7d84ba |
||
|
|
020323ca45 |
||
|
|
d0d3a205b9 |
||
|
|
d01d39deda | ||
|
|
70f45ab7f4 |
||
|
|
d7c641ffc7 | ||
|
|
c21ccad823 | ||
|
|
5fcfcf4f23 |
||
|
|
5f3133a609 | ||
|
|
6a0708b676 |
||
|
|
fae9d75d6d |
||
|
|
67d9317f20 | ||
|
|
c5f9ef3e3d | ||
|
|
9d7888814c | ||
|
|
bba2f21f5e | ||
|
|
f8448c2521 |
||
|
|
bc862698ea |
||
|
|
cf64de66c9 |
||
|
|
dbe1427e7a |
||
|
|
b6bd869d5f |
||
|
|
a6ee6efb6a |
||
|
|
6ae3821e12 |
||
|
|
9e278dc812 |
||
|
|
2323afc0d3 |
||
|
|
2ef16d5f9b |
||
|
|
b605bf3a95 |
||
|
|
91a377b015 |
||
|
|
2544b19525 |
||
|
|
34abca6af7 |
||
|
|
111201f212 |
||
|
|
cb2a7d02ba |
||
|
|
a3e4d4c99d |
||
|
|
32d39410da |
||
|
|
7ddfc63327 |
||
|
|
21d872519f |
||
|
|
8fe9add310 | ||
|
|
0d9571e43e |
||
|
|
47624efd24 |
||
|
|
ff52ee58e4 |
||
|
|
f635292dfe |
||
|
|
99def3c0ef |
||
|
|
91a32d9d51 |
||
|
|
9e8033e36e | ||
|
|
eebfb7a6c5 |
||
|
|
8ec54169f3 |
||
|
|
6f58a875de |
||
|
|
b7035d3cae |
||
|
|
7effc2f873 |
||
|
|
2b1d04938a |
||
|
|
fa0396a764 |
||
|
|
d2d3880f23 |
||
|
|
cf842e8ebf |
||
|
|
f50a40dfb4 |
||
|
|
e3f0cd4fc3 |
||
|
|
8f102f7316 | ||
|
|
b904b4875c | ||
|
|
c3f96472c2 | ||
|
|
72b7906949 | ||
|
|
612b84ceb7 |
||
|
|
a4f0add6e1 |
||
|
|
3ac2d2c22a |
||
|
|
fd9863d919 |
||
|
|
b3ad1b1419 |
||
|
|
d29f2fb251 | ||
|
|
1918485d8c |
||
|
|
eeac12fe49 |
||
|
|
78426087d1 |
||
|
|
1eafb231af |
||
|
|
154ac3a8fa |
||
|
|
4888f538d7 |
||
|
|
d23f7af439 |
||
|
|
bbd9bbeb46 |
||
|
|
75ec058910 |
||
|
|
05feff3d28 |
||
|
|
02da351c3c |
||
|
|
c7b78779e5 |
||
|
|
bc087ab74d |
||
|
|
6315befa32 |
||
|
|
984bb76c5f |
||
|
|
1299e1e4c9 |
||
|
|
f3236538a0 | ||
|
|
b4d02c3c56 | ||
|
|
3194777b98 |
||
|
|
f593af9b12 |
||
|
|
889d2f8482 |
||
|
|
7ea79d10a2 |
||
|
|
cd1171b4c1 |
||
|
|
97a2805a8a |
||
|
|
a119e5c4a0 |
||
|
|
ebcbabde95 |
||
|
|
a863c2c553 |
||
|
|
253d820225 |
||
|
|
f1c04be695 |
||
|
|
7d418d6cbb |
||
|
|
e7b6fdf0c3 |
||
|
|
56d5c77c76 |
||
|
|
336504c306 |
||
|
|
c54e7f9f47 |
||
|
|
a740074fd9 | ||
|
|
1de0294642 |
||
|
|
9443f4e848 |
||
|
|
f4cbcee1b0 |
||
|
|
56883f655a |
||
|
|
24b53c9ff6 |
||
|
|
230f26156a |
||
|
|
42715720b1 |
||
|
|
31a7228509 |
||
|
|
6322132921 |
||
|
|
70bff5616d |
||
|
|
9bb0b02261 | ||
|
|
ff25f43eeb | ||
|
|
b8322d6aa7 |
||
|
|
23bbdb08aa |
||
|
|
ac9bb8442b |
||
|
|
eadd225363 |
||
|
|
a7c9cf5baa |
||
|
|
5de26dbee9 |
||
|
|
a9f5f72218 |
||
|
|
bd4c4414dd |
||
|
|
1453c75c3f |
||
|
|
1800c2a892 |
||
|
|
7747114b2e |
||
|
|
48be6771b2 |
||
|
|
abb8566ce7 |
||
|
|
bc7bf9870c | ||
|
|
c3d8855ec3 | ||
|
|
dbc829b5f8 | ||
|
|
c4ff314a12 |
||
|
|
147d3b2a9d |
||
|
|
0e2e26ef88 |
||
|
|
a439102403 |
||
|
|
e1157642a8 |
||
|
|
91333d9009 |
||
|
|
9f6c374d36 |
||
|
|
aef952678a |
||
|
|
b73755b0c4 |
||
|
|
67378c7a48 |
||
|
|
10d58f2f19 |
||
|
|
ca23475620 |
||
|
|
5aaac56a1a |
||
|
|
6941b3cb7f |
||
|
|
b5b5d5e7d4 |
||
|
|
39a8bed4ca |
||
|
|
033565bfc5 |
||
|
|
b507b340a6 |
||
|
|
ae276a69eb |
||
|
|
5310c90a83 |
||
|
|
512fc5ca04 |
||
|
|
13a7a4b5c1 |
||
|
|
fb9e3e6a53 |
||
|
|
06d6dbe3a3 | ||
|
|
8263b17861 | ||
|
|
24d4276a7c | ||
|
|
8d8183eabb | ||
|
|
801c7a07c0 |
||
|
|
61d8884bef | ||
|
|
5d017b09b8 | ||
|
|
9a1fb0c0a0 | ||
|
|
88644314ce | ||
|
|
4ba458b9ea | ||
|
|
28c740ab67 | ||
|
|
2591ae9e8e | ||
|
|
0f3c292098 | ||
|
|
16646e1586 | ||
|
|
8978be2fd7 | ||
|
|
183226e190 | ||
|
|
185fcfef33 | ||
|
|
7a4ee6db27 | ||
|
|
ea11e5cfd9 | ||
|
|
ae7426b6ff | ||
|
|
f72d29b391 | ||
|
|
9a7ae60392 | ||
|
|
fc61500a29 | ||
|
|
bcdd548238 | ||
|
|
2e9d375f36 | ||
|
|
f436dfdd41 |
||
|
|
480a2576c3 |
||
|
|
f0253075d8 | ||
|
|
96a983b310 |
||
|
|
53bd9c2f68 |
||
|
|
ad6569cf06 | ||
|
|
4ac25d4bc5 |
||
|
|
878ac0d192 | ||
|
|
a0d10989ad | ||
|
|
36844f948c |
||
|
|
2b0afcacf2 | ||
|
|
beb9fbd940 | ||
|
|
0642ae58ce |
||
|
|
635b8b3eef |
||
|
|
bcd2e7cb08 | ||
|
|
91b48e061d |
||
|
|
89edc6aa30 |
||
|
|
be78a5809a | ||
|
|
ce6f188acc |
||
|
|
b8eaae3a50 |
||
|
|
9105a3db06 | ||
|
|
18a10a9efb |
||
|
|
e772c4eab5 |
||
|
|
177d4d78ba | ||
|
|
fce71f4a7c | ||
|
|
6ee71779d1 | ||
|
|
8a281bacd8 | ||
|
|
f8f692af05 | ||
|
|
3900e9dd81 | ||
|
|
58553d7691 |
||
|
|
f3d2dc1678 |
||
|
|
05ff8530cc | ||
|
|
0fcaa46095 |
||
|
|
1754c77517 |
||
|
|
999095b7df |
||
|
|
d39a6dd012 |
||
|
|
bc4f9b5f51 | ||
|
|
8e00e26054 |
||
|
|
c1607bd8e7 |
||
|
|
4ce2efe86b | ||
|
|
99bb58a7a7 |
||
|
|
49189d5649 |
||
|
|
74181d0783 |
||
|
|
b885673341 |
||
|
|
013e55a9a6 | ||
|
|
2e31325de6 |
||
|
|
0ad907982d |
||
|
|
2156c6ba97 |
||
|
|
e7d0ad93f9 |
||
|
|
a5bb486012 |
||
|
|
a3490af5a3 |
||
|
|
484ec95f24 |
||
|
|
d8495bdc54 |
||
|
|
2891c1e89c |
||
|
|
c8eee85b28 |
||
|
|
f0d985637b |
||
|
|
dc288c4c66 |
||
|
|
d810c3aec9 |
||
|
|
5f06cfe483 |
||
|
|
9db3b009f3 |
||
|
|
de6aa7df90 |
||
|
|
b72e49c902 |
||
|
|
63f412aab1 |
||
|
|
b756de003e |
||
|
|
aa96a2ad31 |
||
|
|
37cbc1562c |
||
|
|
5b555835e2 |
||
|
|
119afbdf2f |
||
|
|
31f814d66d |
||
|
|
d584dd7e11 |
||
|
|
ee2e2608a3 |
||
|
|
7e321d399c |
||
|
|
86d84b70a1 |
||
|
|
c5596f658e |
||
|
|
16ade38851 |
||
|
|
87d902d028 |
||
|
|
600313fded |
||
|
|
4641cb4b8c |
||
|
|
599c4ce769 |
||
|
|
b14a8a267a | ||
|
|
55e99b299a |
||
|
|
20e47aaadb |
||
|
|
55767d733d |
||
|
|
56dfa5ef40 |
||
|
|
39e70670b5 |
||
|
|
beac893dd0 |
||
|
|
6e655d457e |
||
|
|
a7db950a52 |
||
|
|
f4528e6f00 |
||
|
|
c35412625e |
||
|
|
4c3594b832 |
||
|
|
52bf7b116e | ||
|
|
6de6f8185e | ||
|
|
487a438f02 | ||
|
|
4bf4b7baf0 |
||
|
|
9c2607df89 |
||
|
|
05e806d762 |
||
|
|
68618da7f1 |
||
|
|
eb171c01f5 |
||
|
|
881b3eda19 | ||
|
|
a46c9e8403 |
||
|
|
9da36b9966 |
||
|
|
c2e6b13504 |
||
|
|
42d568ad2c |
||
|
|
4b29cdeb0c |
||
|
|
a3c204f978 |
||
|
|
0f1e7d5036 |
||
|
|
a6f70696f3 | ||
|
|
4c177b8d02 | ||
|
|
ecda9e225e | ||
|
|
a9d2b30d96 |
||
|
|
424bc4f7df | ||
|
|
52002c3e22 |
||
|
|
80b0e8ad12 | ||
|
|
b000a594f4 | ||
|
|
0ec242738f |
||
|
|
5e935189a3 |
||
|
|
0035910765 |
||
|
|
9e6b6c582f |
||
|
|
b7562362c1 |
||
|
|
9926c83ba7 |
||
|
|
6c0acfcfb7 |
||
|
|
babadbd955 |
||
|
|
64aa510abf | ||
|
|
0b38a88147 |
||
|
|
b3fa46ad10 |
||
|
|
5e30d3698d |
||
|
|
04de1ebc30 | ||
|
|
ef473b0f53 | ||
|
|
0e62103010 | ||
|
|
a4ef328d8d |
||
|
|
e5596d9d81 |
||
|
|
e47e54b934 |
||
|
|
b8de7e68b5 |
||
|
|
464c54b2cb |
||
|
|
6b46a55def |
||
|
|
37f4e0ff93 |
||
|
|
9277907c43 |
||
|
|
960862df27 |
||
|
|
1376177a09 |
||
|
|
d2994d501f |
||
|
|
1ea9d6c2ac |
||
|
|
0a6f8a76ec |
||
|
|
c57b42c22b |
||
|
|
f93061de29 |
||
|
|
06a15181c3 |
||
|
|
d11b704f22 |
||
|
|
48f96e9ae0 |
||
|
|
47b254a29e | ||
|
|
7ba2807b01 | ||
|
|
6121a3ab0b |
||
|
|
d8ab40d8ee |
||
|
|
8d119630eb | ||
|
|
5233fb2dbb | ||
|
|
234938ed4b | ||
|
|
3630ab8519 | ||
|
|
c463d1ddd3 | ||
|
|
44a8925b8c | ||
|
|
7216b8124b | ||
|
|
eb7f9ab298 | ||
|
|
6f04216af5 | ||
|
|
b79c91ff1e |
||
|
|
d2e4f56219 |
||
|
|
ee0002fe6a |
||
|
|
ab8593d3cd | ||
|
|
8f15548770 | ||
|
|
d99d56fe81 |
||
|
|
365613f0ee |
||
|
|
bec6665044 |
||
|
|
8976fa163e | ||
|
|
db866f9823 | ||
|
|
568427ca98 | ||
|
|
d9985e7318 | ||
|
|
9b9db35e3c | ||
|
|
77279675ec |
||
|
|
2127153b73 |
||
|
|
66b6517855 |
||
|
|
c62d3c2f15 |
||
|
|
abd414beb9 |
||
|
|
58003e1f59 |
||
|
|
63fd0def6c | ||
|
|
8a515a8a70 | ||
|
|
e20b1a55c3 |
||
|
|
b48adc434d |
||
|
|
f213875d48 |
||
|
|
7401174523 |
||
|
|
641cd951cb |
||
|
|
b734a8b983 |
||
|
|
a8ffe7768e |
||
|
|
9500edc89f |
||
|
|
381cfb7099 |
||
|
|
e0d5f4c2ff | ||
|
|
0134276f01 |
||
|
|
258db10ea9 |
||
|
|
3ca9fd2e80 | ||
|
|
9db1d0f7c8 | ||
|
|
5a0e0b6718 | ||
|
|
44de1dd03f |
||
|
|
2cf2c6d0e6 |
||
|
|
94978b334c |
||
|
|
96099694d6 |
||
|
|
3f0afed3c9 |
||
|
|
c6054c3f40 |
||
|
|
22e7217e06 |
||
|
|
1c13ff7922 |
||
|
|
6c9d6d04de |
||
|
|
bf3f593004 |
||
|
|
8e25fa8a3b |
||
|
|
4aec23a6fc |
||
|
|
7fdb70d451 |
||
|
|
d95f1fa5b6 |
||
|
|
989ecdb2f6 |
||
|
|
469fe577f2 | ||
|
|
1fb78d7218 |
||
|
|
9e76fe2a76 | ||
|
|
054760d49f | ||
|
|
f5884957a5 | ||
|
|
606c62dc70 | ||
|
|
0b5cbceffd | ||
|
|
dbdf98537c |
||
|
|
f12a13916b |
||
|
|
fbf6f48d7a |
||
|
|
f7f92c5f39 |
||
|
|
86abe1e2df |
||
|
|
1bdeae2b76 |
||
|
|
f8642dd2a5 |
||
|
|
1b2894bf99 | ||
|
|
61305cd27c |
||
|
|
0f44c51b00 |
||
|
|
d0697d39d7 |
||
|
|
0639fdb410 |
||
|
|
705261cdb5 | ||
|
|
b5a7bc6be6 |
||
|
|
1ccd910e14 |
||
|
|
25b870fcd1 | ||
|
|
4ca97bc955 |
||
|
|
1b9040deed |
||
|
|
87c9abe9da | ||
|
|
4d5f15b32e |
||
|
|
342b97f68b |
||
|
|
6aabd9bacb | ||
|
|
15100c853c |
||
|
|
55f3f9ef13 |
||
|
|
77c2fc0ea7 |
||
|
|
60b398c6d8 |
||
|
|
6bedf78019 |
||
|
|
c17e0a0813 |
||
|
|
6422136d50 | ||
|
|
6fcfcb6219 |
||
|
|
99eab4ddcb |
||
|
|
7afafdd25e | ||
|
|
66cdec0075 | ||
|
|
8b71e6a18e |
||
|
|
803fe930f8 |
||
|
|
34436f9a72 |
||
|
|
965b50a341 |
||
|
|
e4c01f7c2f |
||
|
|
d8682126f3 |
||
|
|
2f3f2c4d90 |
||
|
|
6c20a59993 |
||
|
|
f92a442330 | ||
|
|
d607111c86 |
||
|
|
245b44ab02 |
||
|
|
62171e13b3 |
||
|
|
b176d26302 |
||
|
|
10cba8d9b0 | ||
|
|
b890e7e976 |
||
|
|
bbe6b34371 | ||
|
|
2451f222e8 | ||
|
|
a9bad593b0 |
||
|
|
63540e102b | ||
|
|
4e6bd9e943 | ||
|
|
0dd0d8fb12 | ||
|
|
e8ba4f4fb9 |
||
|
|
5b68fb5054 |
||
|
|
8b04979eac |
||
|
|
510b859df9 |
||
|
|
f1a11d3a0b |
||
|
|
08f77528d9 |
||
|
|
184936fa38 |
||
|
|
4fbca2b219 |
||
|
|
309b9027be |
||
|
|
b025647ad9 |
||
|
|
cdd28ba2cb |
||
|
|
5b7abd6e02 |
||
|
|
aa0df1d6b4 |
||
|
|
1a7135c5e0 | ||
|
|
de6d6906f8 | ||
|
|
5657d9c221 |
||
|
|
26bf0850d7 | ||
|
|
36f4284e07 | ||
|
|
4d3fd1c8f2 | ||
|
|
b9e7e401ca |
||
|
|
6b9d2baf97 |
||
|
|
d5ac13f91c | ||
|
|
3f928d8742 | ||
|
|
efc421c0a6 | ||
|
|
0bdac63953 | ||
|
|
304e8bf5b0 |
||
|
|
fd04a528f6 |
||
|
|
8842242fb4 |
||
|
|
6dac3d122a | ||
|
|
bcd7e7cfff |
||
|
|
d7513b43dc |
||
|
|
ba4ec355e1 |
||
|
|
fe17324fef |
||
|
|
7dfbf215db |
||
|
|
4bfd599393 |
||
|
|
10e31a1ce0 |
||
|
|
46f9f6a6a3 |
||
|
|
fef4b8b93a |
||
|
|
8a2631f503 |
||
|
|
858fe0185a |
||
|
|
f1e6ada2d0 |
||
|
|
4682a83827 | ||
|
|
e83a1ac7d8 |
||
|
|
f0967ddf5f |
||
|
|
144c4b7ce9 |
||
|
|
69bac8f517 |
||
|
|
f1a0ccbe13 |
||
|
|
fa57814678 |
||
|
|
fbdd888c3d | ||
|
|
2e49175840 |
||
|
|
6164862af5 |
||
|
|
b5f5775cfc |
||
|
|
41e3762e57 | ||
|
|
299a9324f6 |
||
|
|
df5cb3081e |
||
|
|
85bc4df1e2 |
||
|
|
14f6032316 |
||
|
|
23ac0fef32 | ||
|
|
a2349f96cb | ||
|
|
1c190d1adb | ||
|
|
5b34395587 |
||
|
|
2266350f23 |
||
|
|
79cbe63067 |
||
|
|
e73575a342 | ||
|
|
5c64eaf41e | ||
|
|
b93cae2e01 |
||
|
|
00cdb6e808 |
||
|
|
e0cd9cbcdf |
||
|
|
5fe0710724 | ||
|
|
c4ddf6d93e | ||
|
|
7b507e5248 |
||
|
|
1870145674 | ||
|
|
ff4fd0a13d | ||
|
|
98aef9b6ad |
||
|
|
bf0a8c4e4d | ||
|
|
ba3e0dae79 |
||
|
|
5c8c854d18 |
||
|
|
5921bb1ee1 |
||
|
|
c6f77f0668 |
||
|
|
27e08baf25 |
||
|
|
05c69dfb6d |
||
|
|
e9db7e5f82 |
||
|
|
8ba286a308 |
||
|
|
7ef88523ca | ||
|
|
42ee21bfb8 | ||
|
|
0c246f0bbe | ||
|
|
84107ab516 |
||
|
|
66302d25e0 |
||
|
|
093ef2ff55 |
||
|
|
a8e7cfd2cd |
||
|
|
d288cbb625 |
||
|
|
242b068cdd |
||
|
|
0a9157b935 |
||
|
|
34939d9961 |
||
|
|
6109bf3faa |
||
|
|
be7f2c3c84 |
||
|
|
b52ae5414e |
||
|
|
53934bcbca |
||
|
|
9e84328748 |
||
|
|
9d3a5d4d86 |
||
|
|
29d188f927 |
||
|
|
6301a0012a |
||
|
|
9568316cd0 |
||
|
|
1ce6821585 |
||
|
|
f056cce641 |
||
|
|
5a3f17b647 | ||
|
|
af1da708a3 |
||
|
|
08871ef75a | ||
|
|
78bf87f29c | ||
|
|
8d17bbd6a2 |
||
|
|
b1f5ba87cf |
||
|
|
280018e052 |
||
|
|
99175bef82 | ||
|
|
6e0ab062c5 | ||
|
|
17588560e6 |
||
|
|
813b49d7b1 | ||
|
|
30595ed23f | ||
|
|
c055a07f45 | ||
|
|
a12a24adbe | ||
|
|
56cc6d0b68 |
||
|
|
e4a6aa3160 |
||
|
|
10932abb87 |
||
|
|
b18cb15f7d |
||
|
|
dbfa5c5746 | ||
|
|
55e5c69958 |
||
|
|
d2932ccea8 | ||
|
|
769585e72d | ||
|
|
fe031c8b12 | ||
|
|
27986f5811 | ||
|
|
8696f03e8d | ||
|
|
d79b6e6c2f |
||
|
|
6c0fc8dbb4 | ||
|
|
b808c89322 |
||
|
|
7effbd35a7 |
||
|
|
2a02f96017 |
||
|
|
40ea6e85ce |
||
|
|
a5b9773eac |
||
|
|
92b2f6220d |
||
|
|
5db6714718 |
||
|
|
5f7f56d8ef |
||
|
|
0f90f6b7c2 |
||
|
|
8f2eebf3e4 |
||
|
|
c71c9135ab |
||
|
|
e1e8dae02b |
||
|
|
fd9ed3335f |
||
|
|
c7338e9e11 |
||
|
|
44f25324ff |
||
|
|
e9458f0a65 |
||
|
|
146b6d02f4 |
||
|
|
67aba10e34 |
||
|
|
0ac698e0bb |
||
|
|
25b65b39db |
||
|
|
7c5f4c404d | ||
|
|
18bfd32704 | ||
|
|
74e8c7e51c |
||
|
|
c12e7bcebb |
||
|
|
356a896fe2 |
||
|
|
bc6017aed7 |
||
|
|
126bf1794e |
||
|
|
7a8bb0376c | ||
|
|
03dd00284c |
||
|
|
749e7f4469 |
||
|
|
7a350ac69a |
||
|
|
c04beb8b08 |
||
|
|
f1eee6c9b2 |
||
|
|
d2f0590c73 | ||
|
|
72a954b865 | ||
|
|
60ca8850d9 | ||
|
|
456cdb2f54 | ||
|
|
d9f8f45169 | ||
|
|
8cb49ae56a | ||
|
|
b16d023657 |
||
|
|
cd6821a196 |
||
|
|
117dd0faed |
||
|
|
0ee52e47e4 |
||
|
|
5da8d089c3 |
||
|
|
6cc2471f4e |
||
|
|
a414563eae |
||
|
|
5611f98a4e |
||
|
|
03d5fab794 | ||
|
|
0d7b980f90 | ||
|
|
6091514630 | ||
|
|
2365c9489e | ||
|
|
f269ac3bee | ||
|
|
def56dc694 | ||
|
|
c1920eb566 | ||
|
|
a9f97ddf22 | ||
|
|
4a345eb6d9 | ||
|
|
c108c20c91 | ||
|
|
86341f063c |
||
|
|
f1d806a80f |
||
|
|
e2c74a1014 |
||
|
|
0ba11d8a0e |
||
|
|
c6b568c165 |
||
|
|
f3b383ce63 | ||
|
|
408eb75a88 | ||
|
|
f2bf1fa90a | ||
|
|
f0b0c53536 |
||
|
|
dcf08ecac6 | ||
|
|
8fb8b94650 |
||
|
|
a8dd85d21e | ||
|
|
6a920fd4eb |
||
|
|
61369b3e5a |
||
|
|
98708a2ebd |
||
|
|
5b55ac7d02 |
||
|
|
52ce1aebbd |
||
|
|
dc93bc0f1e |
||
|
|
935b193a64 | ||
|
|
5b4a5fd4b1 | ||
|
|
309be48906 | ||
|
|
317f4fb991 | ||
|
|
f806c32c49 |
||
|
|
6731e584da | ||
|
|
58a558247b |
||
|
|
578f5fa1c9 | ||
|
|
ebbe798c16 |
||
|
|
0486f43f9f |
||
|
|
bfffc8d0df |
||
|
|
ead372e6e6 |
||
|
|
a59af9b941 |
||
|
|
05af830a15 | ||
|
|
f00c71c81b | ||
|
|
1495ce3772 | ||
|
|
0e9fdf9e08 | ||
|
|
b592657f7d | ||
|
|
0e8b9fdd5c | ||
|
|
c23c786c58 |
||
|
|
e8ed36bfd6 | ||
|
|
6f7444dfe3 |
||
|
|
d99b6d0a17 |
||
|
|
e3a2fa7dd1 | ||
|
|
7fbba14b69 | ||
|
|
059cedcf7a |
||
|
|
8840a80209 |
||
|
|
eda437f40d | ||
|
|
a0f684c0d9 |
||
|
|
bc3e6292b6 |
||
|
|
a91f5dfb49 |
||
|
|
dccdd4869c |
||
|
|
9ac1257e76 |
||
|
|
d550c6e11c |
||
|
|
0582303f3b |
||
|
|
1ede6c8463 |
||
|
|
24e41327a3 | ||
|
|
bbf92f1aa0 | ||
|
|
15d7f2f224 |
||
|
|
51360711c9 |
||
|
|
87244fb4d5 |
||
|
|
0e3d7bb5bd |
||
|
|
53a34a0509 |
||
|
|
7eaf6fb58f |
||
|
|
f5103ac4b4 | ||
|
|
2bc78e24a2 |
||
|
|
74cc1722ea | ||
|
|
58545353f7 | ||
|
|
fd6bc3ecb6 | ||
|
|
2a84d8239b |
||
|
|
c022377c49 | ||
|
|
e9cbea9569 | ||
|
|
8d227ee37e |
||
|
|
2d983b94eb |
||
|
|
371f676fd5 |
||
|
|
f7391f252b |
||
|
|
636c4a6204 |
||
|
|
3426ee31c0 |
||
|
|
cf0a222cf9 |
||
|
|
bfbb6627d0 |
||
|
|
a9f0b1d5ff |
||
|
|
1501566824 |
||
|
|
4ad7dc1ad3 |
||
|
|
bdfbfdd475 |
||
|
|
21bbfffb21 |
||
|
|
320832dfd9 |
||
|
|
45d7b0531a | ||
|
|
6032bd16a5 |
||
|
|
21c8e7cd62 |
||
|
|
3224c988b8 |
||
|
|
e64f53ad33 | ||
|
|
07ea17b180 |
||
|
|
278595df1f |
||
|
|
e60a8e8bff |
||
|
|
f2ea562d16 |
||
|
|
0bd676355e |
||
|
|
c260e1a82f |
||
|
|
4ef5c66fd6 |
||
|
|
3dae767937 | ||
|
|
db4b292a38 | ||
|
|
d90a81240f |
||
|
|
ad39229867 |
||
|
|
6199c3defc |
||
|
|
f945c29cda |
||
|
|
cf0a4999e9 | ||
|
|
674d9cfbd8 |
||
|
|
dd2b15b7af |
||
|
|
5d1eb385e6 |
||
|
|
801f56b168 |
||
|
|
7425033fdb |
||
|
|
36b105021b | ||
|
|
6b46097479 |
||
|
|
f6a432da32 |
||
|
|
69840cd8c1 |
||
|
|
661d4a9ba4 |
||
|
|
1d8bd7acb5 |
||
|
|
09ddbd156c | ||
|
|
320b3ea98f |
||
|
|
6d342b9847 |
||
|
|
f0dfb909dd |
||
|
|
45f2576e96 |
||
|
|
8b7fb33627 | ||
|
|
e923696bb0 | ||
|
|
c19cbd7ffd | ||
|
|
446f99f62a |
||
|
|
b089b92b1e | ||
|
|
2ee30abd56 |
||
|
|
eb0094618e | ||
|
|
bbbaf128bb |
||
|
|
c0b8f6f86a | ||
|
|
c813d6ee2a | ||
|
|
85400ed9c2 |
||
|
|
5f2651a252 |
||
|
|
fa68d74f9e | ||
|
|
c790d9fadf | ||
|
|
d6923d0c6d | ||
|
|
10b1cedbb6 | ||
|
|
12cdf280fc | ||
|
|
9784423808 | ||
|
|
e74c35687e |
||
|
|
a3be259567 | ||
|
|
c2ed3fae56 | ||
|
|
c70d0fb224 | ||
|
|
9051861f4d | ||
|
|
049e9a1680 | ||
|
|
57ba119edb | ||
|
|
83f3fe772a | ||
|
|
ec85372132 | ||
|
|
90ec37ce82 | ||
|
|
9b9c547e8c | ||
|
|
dca6543070 | ||
|
|
0c49f025b4 | ||
|
|
2a6c57abaa | ||
|
|
de76a86757 | ||
|
|
49dc6ffd8f | ||
|
|
0ac9601a3a | ||
|
|
e76d5d2ef9 | ||
|
|
d0444d7d7f | ||
|
|
f00dfc7524 | ||
|
|
21bbe7d4c3 | ||
|
|
85907f54ba | ||
|
|
9147772cb2 | ||
|
|
0cb8dc73bb | ||
|
|
b2cc8d9531 | ||
|
|
fcf7488e1e | ||
|
|
a71472a427 | ||
|
|
111c3665f9 | ||
|
|
7584f47c7d | ||
|
|
17365d9967 | ||
|
|
54a1e11f50 | ||
|
|
033f565c0e | ||
|
|
a4490bf1d6 | ||
|
|
91e0349486 | ||
|
|
f2309c7c89 | ||
|
|
5a0f1c1f4e | ||
|
|
3a6b075745 | ||
|
|
2044bc88dd | ||
|
|
d5ebdc943c | ||
|
|
cbaf4db339 | ||
|
|
16f8304c4e | ||
|
|
6a15fd95f0 | ||
|
|
dd9efad23c | ||
|
|
1adbbdda2a | ||
|
|
347802a4b6 | ||
|
|
94bdff4fa0 | ||
|
|
0c7db6dffe | ||
|
|
897f238c38 | ||
|
|
5c0a7722a4 | ||
|
|
d232ef1557 | ||
|
|
916da73108 | ||
|
|
80c6cfbd7c | ||
|
|
25da9dd63e | ||
|
|
703848919c | ||
|
|
a2a2aff2bc | ||
|
|
a1f183f216 | ||
|
|
fc1c9568e2 | ||
|
|
b164e95290 | ||
|
|
8972242863 | ||
|
|
6b8fea8afc | ||
|
|
c26de4cf6a | ||
|
|
743ae987ec | ||
|
|
2b5a13a043 | ||
|
|
aba2487126 | ||
|
|
742cd8d4bf | ||
|
|
2f635069e0 | ||
|
|
3c43a2bfd3 | ||
|
|
c4d6afe3d6 | ||
|
|
c8b22b2df3 | ||
|
|
8fa42c5c48 | ||
|
|
2049a16d64 | ||
|
|
e845e17a63 | ||
|
|
c6dca616e6 | ||
|
|
c393dd1a11 | ||
|
|
f76ad57c63 | ||
|
|
b74cc4387a | ||
|
|
431221c21e | ||
|
|
737afc759b | ||
|
|
7355c91839 | ||
|
|
af0d48de72 | ||
|
|
4f6565c24a | ||
|
|
5c4b402341 | ||
|
|
af777106bf | ||
|
|
70a795dced | ||
|
|
2d8417cd8b | ||
|
|
cd36555b63 | ||
|
|
ef500f12a1 | ||
|
|
055ba5caff | ||
|
|
b95f89c4c2 | ||
|
|
2b602ca333 | ||
|
|
ee92de0ff7 | ||
|
|
b5f2e7f0cc | ||
|
|
e0ec340de8 | ||
|
|
b994ecd1f1 | ||
|
|
addd4124bf | ||
|
|
7fd48d8155 | ||
|
|
467ebab31f | ||
|
|
e73bf1e9a7 | ||
|
|
5b17a2fbe4 | ||
|
|
111beb5f12 | ||
|
|
2ef3e3e5b4 | ||
|
|
6c10a2a6cf | ||
|
|
08635beb61 | ||
|
|
5a3ad194e8 | ||
|
|
c4a3108dc0 | ||
|
|
0da059118d | ||
|
|
69cb891b1a | ||
|
|
e71360ad39 | ||
|
|
3f7889e534 | ||
|
|
70d9d8d226 | ||
|
|
71f54f6a5d | ||
|
|
09e12affe8 | ||
|
|
bdb0a2efca |
||
|
|
98f75a5a1c | ||
|
|
bd2a6be257 | ||
|
|
72b0edabf9 |
||
|
|
9fe218a625 |
||
|
|
f614ebd712 |
||
|
|
1a53635d1a |
||
|
|
83648c0571 |
||
|
|
8e1ce206e1 |
||
|
|
3c0754e6df |
||
|
|
1b47d0fe90 |
||
|
|
b90db81025 |
||
|
|
2b60730532 |
||
|
|
c64a8728b5 |
||
|
|
3cbf67bacb |
||
|
|
f51bb4ea65 |
||
|
|
f0d37d7e08 |
||
|
|
2c280685ef |
||
|
|
67a84fe2f1 |
||
|
|
6a75ab3f27 |
||
|
|
b8ae278fba |
||
|
|
7a34c661cf |
||
|
|
20d40d2a32 |
||
|
|
aaedaffa83 |
||
|
|
59dd897099 |
||
|
|
586ceacd2a |
||
|
|
3471413a00 | ||
|
|
1410256e42 |
||
|
|
74fd296d61 |
||
|
|
36002757be |
||
|
|
a78a7fbc92 |
||
|
|
b26b73e994 |
||
|
|
1f2e69a550 |
||
|
|
3d9f185494 |
||
|
|
d84a2dbecc |
||
|
|
b550591262 |
||
|
|
17b2d2fc32 |
||
|
|
a13bcb8e93 | ||
|
|
f87bb85f37 |
||
|
|
38dd077bdf |
||
|
|
295ed871c7 |
||
|
|
86f3baae90 |
||
|
|
7ff508ca4e |
||
|
|
d3ccf17953 |
||
|
|
f98a70d58d |
||
|
|
de25fdbf87 |
||
|
|
89e14d6ddc |
||
|
|
1822a67ef1 |
||
|
|
0f3a088404 |
||
|
|
561cb5cfa8 |
||
|
|
874385814d |
||
|
|
61f86b1557 |
||
|
|
19d8178606 | ||
|
|
95cc9a47fb | ||
|
|
8a224809dd |
||
|
|
6f8364b1dd | ||
|
|
901d96c8cc |
||
|
|
3ed54a3e11 | ||
|
|
39213bc4e7 |
||
|
|
c62305039e |
||
|
|
b7b3717e3b |
||
|
|
14c0e4071a |
||
|
|
0ede916b78 |
||
|
|
14c2cf6b0b | ||
|
|
79e0558b73 | ||
|
|
48713428b7 | ||
|
|
d5224a9d01 |
||
|
|
1de39524f7 |
||
|
|
064d36a6cc |
||
|
|
adef07f6d8 |
||
|
|
aaf2a563c8 |
||
|
|
75eb812f05 | ||
|
|
959ec5b598 |
||
|
|
a35675ddc1 |
||
|
|
5b3399f95a | ||
|
|
e58a895293 | ||
|
|
ddebb22afe |
||
|
|
fb250682a1 |
||
|
|
eb971a7d23 |
||
|
|
1f2ca91d89 |
||
|
|
e09599aeae | ||
|
|
32e86dc699 |
||
|
|
97cfd1a2bc | ||
|
|
372f9f7ce4 |
||
|
|
6c57339668 | ||
|
|
a0c2495c42 | ||
|
|
fe4e0343a4 | ||
|
|
d8a6b137fe |
||
|
|
64efa0cf7b |
||
|
|
4d55614db1 |
||
|
|
27a06b533c |
||
|
|
479d2eadf0 |
||
|
|
59e37cf73d |
||
|
|
46e6b7282e |
||
|
|
e26abb07fb | ||
|
|
0e5d64e027 | ||
|
|
298aa4c664 |
||
|
|
8f46f101b8 |
||
|
|
c89aea3c1e | ||
|
|
2aa5ed44ad | ||
|
|
0c97a8e48e |
||
|
|
c72fce75de |
||
|
|
49fb6cc049 | ||
|
|
5ba5505ba4 |
||
|
|
3df6e00b70 |
||
|
|
dbec8330ce | ||
|
|
8f7bee8dd3 | ||
|
|
8379a46c53 |
||
|
|
41b78f1ae2 |
||
|
|
19d69ba4c3 | ||
|
|
41e5090fb0 | ||
|
|
5595b17060 |
||
|
|
7988a0d006 |
||
|
|
63c638e9ad | ||
|
|
d237647f8a | ||
|
|
c47dd965c9 |
||
|
|
d4198e4360 |
||
|
|
c4a637ab49 |
||
|
|
28949fb5e2 |
||
|
|
2273c913ac |
||
|
|
00e59000fd |
||
|
|
ee91217d98 |
||
|
|
93cb395b75 |
||
|
|
d020875556 |
||
|
|
2447c00a61 |
||
|
|
6d4ee6e76a |
||
|
|
0daf985dff |
||
|
|
4e17067a07 | ||
|
|
f22cfe0547 | ||
|
|
ddfadcd326 |
||
|
|
5dfd1b8809 | ||
|
|
ed8f4c6c57 |
||
|
|
4d34a837a5 |
||
|
|
1ca16816c2 | ||
|
|
ebfecc3e9d | ||
|
|
51147f35b2 | ||
|
|
8aa8768dcc | ||
|
|
e3e0495a9f | ||
|
|
8c3c426998 |
||
|
|
ee4de6a871 |
||
|
|
7687c90edc | ||
|
|
5027b9bf47 |
||
|
|
ba517bbac9 | ||
|
|
44d207f5f5 | ||
|
|
e66f47904e |
||
|
|
b0264f0725 |
||
|
|
dedc70d3ff |
||
|
|
13f728e2c0 |
||
|
|
76ff5dfef5 |
||
|
|
742cf433d9 |
||
|
|
2dfebda8a1 |
||
|
|
ebd894e915 |
||
|
|
bc3b1875cd |
||
|
|
b0969d9856 |
||
|
|
4f1fe2b6f1 |
||
|
|
a39a49f711 |
||
|
|
5fead0d909 |
||
|
|
b0cff121ec |
||
|
|
4c684ecad4 |
||
|
|
9c6c03b23a |
||
|
|
b3a13f1aa5 |
||
|
|
493f9b1b6c | ||
|
|
8c19613bce |
||
|
|
037fa6d114 | ||
|
|
c39f0d01e6 |
||
|
|
deb5d2d090 | ||
|
|
390a0b8e83 | ||
|
|
75f7666548 |
||
|
|
48060e9743 |
||
|
|
a552a36a4a |
||
|
|
577b1a5952 |
||
|
|
a26976918b |
||
|
|
e7f1cff44d |
||
|
|
f163e20a93 | ||
|
|
e3abd5db48 |
||
|
|
59ad41aea4 |
||
|
|
667f203476 |
||
|
|
7d53aa145a |
||
|
|
086237a06a |
||
|
|
048a272a61 |
||
|
|
b115f07c35 |
||
|
|
3bfd84ad5d |
||
|
|
28f6971a27 |
||
|
|
7827d16571 |
||
|
|
a81a1e6c44 |
||
|
|
71841bf721 |
||
|
|
5d13e4c97d | ||
|
|
d6abc96a30 |
||
|
|
a24c03a35c | ||
|
|
fb8290399f |
||
|
|
d90b3a5f0f |
||
|
|
36508faa2e |
||
|
|
f8bc2ec285 |
||
|
|
4dd0f71843 |
||
|
|
a2d636ff2d |
||
|
|
a69bb80500 |
||
|
|
d4e89127d0 |
||
|
|
f293bdbdc3 |
||
|
|
faf684576c |
||
|
|
2b4ee0d6c6 |
||
|
|
13e4af3600 |
||
|
|
bfa97390be |
||
|
|
b96e5cc042 | ||
|
|
a658d768d2 |
||
|
|
fe0dc97197 |
||
|
|
eba043d0b3 |
||
|
|
7e27f2d058 |
||
|
|
600115b8d1 | ||
|
|
d09a35b129 | ||
|
|
c4236e0e12 |
||
|
|
cee3a50ddc | ||
|
|
eb1d9079a3 |
||
|
|
526cc126a0 |
||
|
|
34b0985587 |
||
|
|
edc3e498e6 |
||
|
|
0052390c4c |
||
|
|
c728bcb95e |
||
|
|
860ea0b3f6 |
||
|
|
959f2a7786 |
||
|
|
9a07a8f96c |
||
|
|
bb604f2a2b |
||
|
|
5d873d42a3 |
||
|
|
3b1c3f9c13 |
||
|
|
0e6252ef4d |
||
|
|
b89aaf1030 |
||
|
|
8745910452 |
||
|
|
0567c38a4c |
||
|
|
07da692c2b |
||
|
|
a564d2f8cb |
||
|
|
9eff3b51d7 | ||
|
|
c546279d89 | ||
|
|
25dc6b52b4 |
||
|
|
5a94727d79 | ||
|
|
3f52e15444 |
||
|
|
f62f92b36f |
||
|
|
70772ec92d |
||
|
|
5e4b8605b2 |
||
|
|
ecb0bb7590 |
||
|
|
d902a0ad67 |
||
|
|
9f76b83fa7 |
||
|
|
d35b0fdb34 |
||
|
|
f6b6a9138c |
||
|
|
70ac60857c |
||
|
|
8bb7da8a55 |
||
|
|
5ccd6b76c0 |
||
|
|
6f95e3769d |
||
|
|
05510c0694 |
||
|
|
58ad80c3bb |
||
|
|
54c8e5bfc2 |
||
|
|
c083b2ce19 |
||
|
|
0939adad0a |
||
|
|
ccb1595ed3 |
||
|
|
d9844720f9 |
||
|
|
554c602230 | ||
|
|
04e1e004da | ||
|
|
16d070c19e | ||
|
|
9e8e138359 |
||
|
|
846ab3ece3 |
||
|
|
62ed80010f |
||
|
|
aadfede911 |
||
|
|
7615f18536 |
||
|
|
cc1620b5c6 |
||
|
|
c4095c28b1 |
||
|
|
c244d1b9ed |
||
|
|
57aa286ab4 |
||
|
|
4fffb4af2b |
||
|
|
0a0626cc5f |
||
|
|
3be0285467 |
||
|
|
b864674e84 |
||
|
|
4b7fdc85cc |
||
|
|
2d88116e5a |
||
|
|
4d665b6a5e | ||
|
|
7169622d01 | ||
|
|
2b91bf0374 |
||
|
|
3c9ba130a0 | ||
|
|
9ad785c44e | ||
|
|
0f72b57d36 | ||
|
|
d7dcdd1cd4 |
||
|
|
4753d58c0b | ||
|
|
68925a532d |
||
|
|
f4ce7ecd1e |
||
|
|
aa6475e83d |
||
|
|
217cfb4701 |
||
|
|
e69e448396 | ||
|
|
49652fc40a | ||
|
|
e241d31c15 |
||
|
|
fc9e20c09d |
||
|
|
c6b19d5144 | ||
|
|
b4eb538903 |
||
|
|
51fb42c379 |
||
|
|
d1e7392b97 |
||
|
|
91f944f5f5 |
||
|
|
c2c94025f3 |
||
|
|
8b904fa136 |
||
|
|
858f8425fd | ||
|
|
96efaed07a | ||
|
|
1f27c4fad4 | ||
|
|
ac2165bdca | ||
|
|
72bebd8681 |
||
|
|
85025a6840 |
||
|
|
ecb4dd9675 |
||
|
|
b8948856f3 |
||
|
|
65713e5509 |
||
|
|
f4cf33da4b |
||
|
|
c8819e9a13 | ||
|
|
7bb588be5d |
||
|
|
59d2d6fec8 | ||
|
|
914ad9f11a |
||
|
|
580d83858c |
||
|
|
5345d84d1f |
||
|
|
db126dc75c |
||
|
|
e0879ed075 |
||
|
|
9c7e27ada4 |
||
|
|
d4a729d316 |
||
|
|
1650383846 |
||
|
|
b3c9a57d0c |
||
|
|
734699de5b |
||
|
|
ea8d734fc2 |
||
|
|
2cdfe5ee7a |
||
|
|
ad984fa377 | ||
|
|
805a86c67e |
||
|
|
5f9a706fe0 |
||
|
|
932d9c809b | ||
|
|
e92082de57 |
||
|
|
ed911ddba4 |
||
|
|
8514503fc3 |
||
|
|
5041e82980 | ||
|
|
efa0aeb2c6 | ||
|
|
526a689e14 |
||
|
|
b7f445ade3 |
||
|
|
998bdd49aa |
||
|
|
fc979c17f7 |
||
|
|
2500602d3b |
||
|
|
ba356ae34c |
||
|
|
e246c06c32 | ||
|
|
fc532be5df |
||
|
|
d7ff13a41d |
||
|
|
20816d509d | ||
|
|
f40e35515f | ||
|
|
a7bdc99d47 | ||
|
|
e536730dc1 |
||
|
|
9ef5c6c67e |
||
|
|
33de4fb4f4 | ||
|
|
cc7b4e4817 | ||
|
|
2fbdbead55 | ||
|
|
133e7bf710 | ||
|
|
48eeb11391 | ||
|
|
7e5c2672b2 |
||
|
|
06d3178633 |
||
|
|
19d7333dce |
||
|
|
9f1b9fa310 |
||
|
|
ef6c884441 |
||
|
|
1922314827 |
||
|
|
4371cd6db1 |
||
|
|
2af6b87369 |
||
|
|
fe0c9a22da |
||
|
|
9003566d5c |
||
|
|
f5c3111445 |
||
|
|
3ff71701a4 |
||
|
|
a1c307ee8d |
||
|
|
0ff5a7df67 |
||
|
|
03233c3f4d |
||
|
|
803de9a877 |
||
|
|
ab8d819193 | ||
|
|
2b1197880d | ||
|
|
2741421401 |
||
|
|
701be850b6 |
||
|
|
e04bfe39bf | ||
|
|
48767d6030 |
||
|
|
54a4085b68 | ||
|
|
6b70f2a99f |
||
|
|
01347787b7 |
||
|
|
ee8228ced2 |
||
|
|
8b38cbcb52 |
||
|
|
442389c544 |
||
|
|
eac4fb73f2 |
||
|
|
7f481634f0 |
||
|
|
10948b3bb7 |
||
|
|
bf2c6a6bcf | ||
|
|
ac23215115 |
||
|
|
b2e5be33d6 | ||
|
|
820a67802d |
||
|
|
a3c77266e0 |
||
|
|
295b3a4251 | ||
|
|
d3a98a523f | ||
|
|
257ce5d0a8 | ||
|
|
776b20cf4f | ||
|
|
f1594e8a00 |
||
|
|
011fd99503 | ||
|
|
5f13d3308f | ||
|
|
f20ec1b9c3 | ||
|
|
13eaed3eb9 | ||
|
|
6da7aa4cb9 | ||
|
|
db1144749e | ||
|
|
20ae2e0bed |
||
|
|
d48d619cb1 |
||
|
|
6660041376 |
||
|
|
7595880517 |
||
|
|
9e6a3569d5 |
||
|
|
3a6df0a36f |
||
|
|
2ff8d09a44 |
||
|
|
48ae5a4cdd | ||
|
|
85ffaa1ec7 | ||
|
|
983c6602a8 | ||
|
|
807856dbef |
||
|
|
492ab82255 |
||
|
|
633b1d506c |
||
|
|
7a08d8ac3f |
||
|
|
d5ac3a7040 |
||
|
|
1fcdc51861 | ||
|
|
63ebf1d301 |
||
|
|
38a949bdab |
||
|
|
5f19eea6a2 |
||
|
|
6cc8341543 |
||
|
|
85acfdcf92 |
||
|
|
2ce93e82c7 |
||
|
|
a56e6de51f | ||
|
|
95a3ec42c2 | ||
|
|
bac95325dc |
||
|
|
4765f06940 |
||
|
|
1b38e6cc02 |
||
|
|
4cbe72f635 | ||
|
|
0d3fc7b4fb | ||
|
|
db651276ba | ||
|
|
fb118b43ca |
||
|
|
e34c966ba8 |
||
|
|
edc316cf04 |
||
|
|
5f1aa1b24a |
||
|
|
126ad16bc0 |
||
|
|
f0d0511b5f |
||
|
|
2196e15799 |
||
|
|
db1bc95e78 |
||
|
|
1425130436 | ||
|
|
15e1a0af7a |
||
|
|
0df403433d |
||
|
|
6a0229ff43 |
||
|
|
921307a914 |
||
|
|
14de647a1b |
||
|
|
f093f29302 |
||
|
|
b52fe1a782 |
||
|
|
56306b995c |
||
|
|
851c6ebac8 |
||
|
|
2561c71a96 |
||
|
|
ef6684b77b | ||
|
|
01bc09d01e |
||
|
|
7886e831bb | ||
|
|
d999c22df7 |
||
|
|
eac14ba58b |
||
|
|
7c823c90a5 |
||
|
|
d8f080ede6 |
||
|
|
001b643d88 |
||
|
|
521b62be24 |
||
|
|
b95c8236bd | ||
|
|
9949fa9963 |
||
|
|
d36f60bb4f | ||
|
|
17e01746ba | ||
|
|
a4f829e0a5 |
||
|
|
be1cfae166 |
||
|
|
3b3b2f4304 |
||
|
|
25d5ffee78 |
||
|
|
0c53d784aa |
||
|
|
64791d1109 |
||
|
|
84d30f9842 |
||
|
|
0317c59bbc |
||
|
|
06648da93e |
||
|
|
515b64c596 |
||
|
|
dd30b37fa9 |
||
|
|
67a7215e63 |
||
|
|
1dfdbbcc78 |
||
|
|
edf7f001ae | ||
|
|
a4ebafabcd | ||
|
|
a6d8dfcc8a |
||
|
|
5abbae1c75 |
||
|
|
29de64e36e |
||
|
|
94d6718c3b |
||
|
|
c731b57911 |
||
|
|
deaf0514c7 |
||
|
|
8e628584f1 |
||
|
|
f8f47d7c68 |
||
|
|
aaff970571 |
||
|
|
e910ab5194 |
||
|
|
4e2a94583a |
||
|
|
cdcd7df425 |
||
|
|
e26687bbd3 |
||
|
|
c9d857dbb4 |
||
|
|
d5e1146776 |
||
|
|
e71bd3a9a3 |
||
|
|
702aa6fdfa | ||
|
|
39d8a6a7de |
||
|
|
a9e774b13b | ||
|
|
559e464216 |
||
|
|
ee0f0e8727 |
||
|
|
779ef07442 |
||
|
|
5b73f32023 |
||
|
|
4555cf7dcf | ||
|
|
3ebc4db256 |
||
|
|
786bcc5691 |
||
|
|
6b65d63a29 |
||
|
|
faed0b0439 |
||
|
|
67be3d66df |
||
|
|
39c998fa34 |
||
|
|
46033ee29f |
||
|
|
721c0d4374 |
||
|
|
0967fa26cf |
||
|
|
8687a513de |
||
|
|
806650dcf9 |
||
|
|
fe68f2a1ee | ||
|
|
21cb4dca1e |
||
|
|
0eddb9153d |
||
|
|
4c35b80b88 | ||
|
|
af976c52c3 | ||
|
|
ff8b706cc6 |
||
|
|
34e48d6040 |
||
|
|
8ce56a6da6 |
||
|
|
7fe5267d1d |
||
|
|
5e856730b6 |
||
|
|
702decab5c |
||
|
|
81aea4994f | ||
|
|
1247d29fcf | ||
|
|
85aad8c9ba |
||
|
|
bd3bb08805 |
||
|
|
67adbc328a |
||
|
|
0979fffd08 |
||
|
|
74b17ca5cb |
||
|
|
8bda748096 |
||
|
|
fe5539e3c4 |
||
|
|
d78dcee47f |
||
|
|
f0114835ce |
||
|
|
3f0f851de1 |
||
|
|
9a7cd95b97 |
||
|
|
d37dbde757 |
||
|
|
93f182b98a |
||
|
|
9546deff31 |
||
|
|
85ba9d933a |
||
|
|
e66c992148 |
||
|
|
cb52a6c1cd |
||
|
|
59b1b1d216 |
||
|
|
d6fc926f73 |
||
|
|
dc4a42c7da |
||
|
|
aa0918a6ff |
||
|
|
0c3b8e2a6c |
||
|
|
05b3a26dda |
||
|
|
d9f75ff73c |
||
|
|
04cb4f49b6 |
||
|
|
dae84af2ce |
||
|
|
63fc320ad1 |
||
|
|
4892161059 |
||
|
|
e2d9385000 |
||
|
|
ecfc5cc28b |
||
|
|
db97c9bbdd |
||
|
|
fa39f57d61 |
||
|
|
d08c1d0ede |
||
|
|
bfcb9d7bd9 |
||
|
|
45648a08ad |
||
|
|
2d8a28fa36 |
||
|
|
2fba91bc06 |
||
|
|
99c6e65d31 |
||
|
|
95370622cc |
||
|
|
d9bffd718e |
||
|
|
b7ebec9ade |
||
|
|
2d5317ca0c |
||
|
|
4f60797f9d |
||
|
|
f80635ca1f |
||
|
|
54acbe0335 |
||
|
|
dba768203f |
||
|
|
f235e6a64d |
||
|
|
738b8a7e8f |
||
|
|
c29b3b5c4b |
||
|
|
93ff33140f |
||
|
|
451cd3657a |
||
|
|
830fdda91a | ||
|
|
ce49049cd7 |
||
|
|
b0f4ad57f0 |
||
|
|
892478d2db |
||
|
|
1a84b3c0cf |
||
|
|
5d479f3e62 |
||
|
|
0bc36ad486 |
||
|
|
d63c212b6a |
||
|
|
4cb1fb48c7 |
||
|
|
59f440346c |
||
|
|
3e0711ca2d |
||
|
|
66f8b0a123 |
||
|
|
88ca219cc9 |
||
|
|
57f13aac0d |
||
|
|
1235d8a699 |
||
|
|
ec11b62b08 |
||
|
|
42ab78d4fb |
||
|
|
33e9fb58d3 |
||
|
|
2c10729225 |
||
|
|
7c3b0e81b4 |
||
|
|
fc99a7f9e1 |
||
|
|
a173040537 |
||
|
|
eb1fc22715 |
||
|
|
01dab06b0a |
||
|
|
49f484df89 |
||
|
|
1ebb71f3a7 |
||
|
|
0f06ed1f56 |
||
|
|
1d2ee012cd |
||
|
|
a3bc59c36f |
||
|
|
6a0b53c71f |
||
|
|
6e8c8da5b0 |
||
|
|
bf034a187c |
||
|
|
dc5fd9e78d |
||
|
|
18271f8c32 |
||
|
|
8ed2866ed1 |
||
|
|
5941a009e0 |
||
|
|
066a08eee0 |
||
|
|
dde3465cfa |
||
|
|
56248116a2 |
||
|
|
f9459ee1c3 | ||
|
|
5723be00c7 |
||
|
|
c574234b99 |
||
|
|
c0708fd955 |
||
|
|
e3dfedf8b5 |
||
|
|
9026f73533 |
||
|
|
a23f0a76e9 |
||
|
|
a58c12a09e |
||
|
|
231622c7a3 |
||
|
|
3153830005 | ||
|
|
88df9148dc |
||
|
|
ab27a4e39f |
||
|
|
74fe337afe |
||
|
|
a8a634983e |
||
|
|
0f1ae61a38 |
||
|
|
4b6ef5d978 |
||
|
|
2893dcc6f1 |
||
|
|
892737d1d8 |
||
|
|
20f99863d6 |
||
|
|
f3323e3583 |
||
|
|
2b7c4021ad |
||
|
|
310c5a843d |
||
|
|
b61bef80e5 |
||
|
|
94f54cb5b4 |
||
|
|
c1f7a9ef46 |
||
|
|
00510e07ad |
||
|
|
0525f123a3 |
||
|
|
8c4f04206b |
||
|
|
cfc6a6adc8 |
||
|
|
3ef844b6be |
||
|
|
1362592afb |
||
|
|
9d3f5efbd5 | ||
|
|
2c85f61e62 | ||
|
|
05afb2dbd4 |
||
|
|
04dc1feaba | ||
|
|
92098286e7 |
||
|
|
68de8cdd22 |
||
|
|
50fbf93d64 | ||
|
|
b0571c5a36 |
||
|
|
bb2a1d6a6f |
||
|
|
7e7ac8229b | ||
|
|
12cf5ed070 | ||
|
|
f84e4199e9 | ||
|
|
bee160d623 |
||
|
|
4937a79384 |
||
|
|
53d2e697d0 | ||
|
|
fb2206028c |
||
|
|
f7ebff0b67 |
||
|
|
b06eda8471 |
||
|
|
527c1564ae |
||
|
|
eb812fafef |
||
|
|
23a5be7dcc |
||
|
|
8f9e6931a8 |
||
|
|
fc2b8d94e1 |
||
|
|
cff4f1ce2c | ||
|
|
036a2ffb4a |
||
|
|
fe52719b9d |
||
|
|
0d7870ba25 |
||
|
|
e44318358b |
||
|
|
e0258d4734 |
||
|
|
7df26dc373 | ||
|
|
0084d3d04b |
||
|
|
a33108bd49 |
||
|
|
b7958be526 |
||
|
|
43315da408 |
||
|
|
fe4d92a4cd |
||
|
|
de9459dd83 | ||
|
|
bb28ecaff7 | ||
|
|
a4d52f7dee |
||
|
|
a6c0fa8926 |
||
|
|
71332f59d9 | ||
|
|
8c10436630 | ||
|
|
b6b767ee95 |
||
|
|
cac2ef65b7 |
||
|
|
9cb2ca3383 | ||
|
|
5d560c99b8 | ||
|
|
4ba5c911ab |
||
|
|
d8b49f0fa5 |
||
|
|
44c4476037 |
||
|
|
a05502f5c1 |
||
|
|
3453ed353c |
||
|
|
f3d79785bc |
||
|
|
16e38f92d8 |
||
|
|
79ba243053 |
||
|
|
0c72e8d018 |
||
|
|
16c4894bf9 |
||
|
|
b6c7f45298 |
||
|
|
7c1efb18d1 | ||
|
|
2e0100d31c |
||
|
|
b2d9dfe3a5 |
||
|
|
e792419c1c |
||
|
|
a121bec4e3 |
||
|
|
3c0f1408fa |
||
|
|
4e95aebe4a |
||
|
|
6aa8da4099 |
||
|
|
6029fe7c71 |
||
|
|
722cc23b10 |
||
|
|
1c5d81e38f |
||
|
|
df01874b24 |
||
|
|
212f4be3c3 |
||
|
|
be3260dcde |
||
|
|
67effc59dc |
||
|
|
f84d764822 |
||
|
|
f8b5983f2c |
||
|
|
848186d14b |
||
|
|
08931d7b3f |
||
|
|
d9f36ce505 |
||
|
|
6f70ce9e15 |
||
|
|
6f2a919d13 |
||
|
|
3f8fc0ef1b |
||
|
|
0c0b59b3fb |
||
|
|
f4bec8298b |
||
|
|
d0421a2541 |
||
|
|
988115b468 |
||
|
|
5b8b6b714a |
||
|
|
6b8f3a6f73 |
||
|
|
e173b84fdb |
||
|
|
c5e0840ceb |
||
|
|
881e8593ed |
||
|
|
0812ffc2e7 |
||
|
|
b99495334e |
||
|
|
f8a39a0ea8 |
||
|
|
a9eced888f | ||
|
|
c8f229f708 |
||
|
|
4d9c01deea | ||
|
|
b79ee2c180 |
||
|
|
80077b33b8 |
||
|
|
3a681bc33c |
||
|
|
b684bc65aa |
||
|
|
27b8333ebb |
||
|
|
ca598098ce |
||
|
|
457653717c |
||
|
|
1e40b1170b |
||
|
|
94f89dc89f |
||
|
|
c151ad69fa | ||
|
|
9769f023c5 |
||
|
|
ebe7546dd9 |
||
|
|
29c11e3174 |
||
|
|
0a1d67bf5d |
||
|
|
e96bb1e20a |
||
|
|
b0a3daf8a5 |
||
|
|
1c5c226a0a |
||
|
|
ea1fa25fdb |
||
|
|
e9182c1454 |
||
|
|
0c4e35b309 |
||
|
|
3df4661126 |
||
|
|
e172a36581 |
||
|
|
8dee92bd04 |
||
|
|
404de0ff5b |
||
|
|
a21affedb5 |
||
|
|
98d33a8d6a |
||
|
|
1966a64a48 |
||
|
|
c9c9748959 |
||
|
|
781f6aac22 |
||
|
|
aa1fb6a5cd |
||
|
|
dadccb7665 |
||
|
|
8b98c2c93c |
||
|
|
d8d1d3aee0 |
||
|
|
4faf7bfd3c |
||
|
|
0fbf301e0f |
||
|
|
16e4be12a8 |
||
|
|
e1b31d4b8e |
||
|
|
4084ff2e93 | ||
|
|
494f03e747 | ||
|
|
804c9ad11a |
||
|
|
2c17cddf0e |
||
|
|
5786230aef |
||
|
|
98644bfc9e |
||
|
|
a0d3fbadf4 |
||
|
|
01c3f6556d |
||
|
|
09853c4c8b |
||
|
|
3b60dcdd07 |
||
|
|
5d988e14f6 |
||
|
|
01981e30f8 |
||
|
|
61ad93b9d6 |
||
|
|
38d3fc3f58 |
||
|
|
e9ee768f39 |
||
|
|
392a747d88 |
||
|
|
ae72d5828c |
||
|
|
2657332919 |
||
|
|
013129da1f | ||
|
|
c8568b5429 | ||
|
|
3f3a22aa1e | ||
|
|
a26fcb45b4 | ||
|
|
199df6b729 | ||
|
|
32f17d50b4 | ||
|
|
f3cf69796d | ||
|
|
b066dc301e | ||
|
|
40bf9ee8ba | ||
|
|
b02c3b6c17 | ||
|
|
19693bc9b7 | ||
|
|
987cc6d3b4 | ||
|
|
2b653a30d6 | ||
|
|
a4ec875c87 | ||
|
|
71b3acb152 | ||
|
|
c13840e029 | ||
|
|
c33326e25e | ||
|
|
89355e50c3 | ||
|
|
759e69ed07 | ||
|
|
42717e3dec | ||
|
|
5f0f745d10 | ||
|
|
7ba14fe4e3 | ||
|
|
99cda335ab | ||
|
|
00b84d31f5 | ||
|
|
6d1d2e006a | ||
|
|
4d400b6ace | ||
|
|
e5b3c518e2 | ||
|
|
05303e4cf1 | ||
|
|
7d7005c8af | ||
|
|
893d59e7c4 | ||
|
|
4a0f319e91 | ||
|
|
3c4a9efe7e | ||
|
|
def494533b | ||
|
|
5ba0e33fb9 | ||
|
|
f1994352bd | ||
|
|
caf728a2a7 | ||
|
|
446ad6a5f4 | ||
|
|
7b3f7d1c59 | ||
|
|
b3e86dbab4 | ||
|
|
395be41728 | ||
|
|
74edfcaa04 | ||
|
|
d1185da9eb | ||
|
|
cd76512619 | ||
|
|
dd686b563d | ||
|
|
cc895e67ee | ||
|
|
7394e6b9f1 | ||
|
|
7b15c53ed4 | ||
|
|
f526aa8b8b | ||
|
|
4c103b467b | ||
|
|
b6e07a43f5 | ||
|
|
47b9924f26 | ||
|
|
4f3dbc4b8f | ||
|
|
ba2522ecda | ||
|
|
c3a1c77447 | ||
|
|
5c69fe104d | ||
|
|
f45f9a83ee | ||
|
|
32776f0642 | ||
|
|
8dff4a9478 | ||
|
|
26dc37033c | ||
|
|
e2c65fd0de | ||
|
|
5d4400ef90 | ||
|
|
9d8f02ce99 | ||
|
|
a8cad55fda | ||
|
|
140c14959c | ||
|
|
7c8441a93b | ||
|
|
839b07fdc2 | ||
|
|
1d4ab8fff1 | ||
|
|
6877199515 | ||
|
|
bd57c6d620 | ||
|
|
fea6fff13a | ||
|
|
0c5faf5879 | ||
|
|
e91f2aa024 | ||
|
|
2c5549a567 | ||
|
|
a21e0e34cd | ||
|
|
450f3f891a | ||
|
|
853f67ab40 | ||
|
|
f2cbd1efed | ||
|
|
cebfa6ac84 | ||
|
|
104692007f | ||
|
|
ae56191b9f | ||
|
|
d55eb28aea | ||
|
|
5452d26c17 | ||
|
|
a10ac4e7da | ||
|
|
ad0f638487 | ||
|
|
bc69ef5f0b | ||
|
|
c168e15db8 | ||
|
|
c70b4d4c80 | ||
|
|
0e7880a049 | ||
|
|
1bfaef1985 | ||
|
|
987f48ae13 | ||
|
|
0ba6200bb7 | ||
|
|
2303a02839 | ||
|
|
d00c70f804 | ||
|
|
86ddce974d | ||
|
|
c42fc55c6f | ||
|
|
cd94b5d655 | ||
|
|
8580592a91 | ||
|
|
04d86a3550 | ||
|
|
a899b1b46d | ||
|
|
36b6fdcc88 | ||
|
|
efdf11dcae | ||
|
|
7e5e031ea8 | ||
|
|
4b5252d285 | ||
|
|
f4b4cfdee2 | ||
|
|
c9f5e06ee4 | ||
|
|
a15b10ca45 | ||
|
|
5d8a581201 | ||
|
|
8133805dec | ||
|
|
a0f42af0d8 | ||
|
|
bb0450cb31 | ||
|
|
9926157683 | ||
|
|
8e130f0259 | ||
|
|
9249464445 | ||
|
|
797731262a | ||
|
|
d9206c1087 | ||
|
|
77947b46c8 | ||
|
|
b88a186d05 | ||
|
|
6a82114b62 | ||
|
|
3d43b96d5a | ||
|
|
8931570c02 | ||
|
|
80a12d98b4 | ||
|
|
e92f5d573b | ||
|
|
a8c777c797 | ||
|
|
825e3beba6 | ||
|
|
499beb0257 | ||
|
|
dbe95fcc13 | ||
|
|
b982623aaa | ||
|
|
a5625ba203 | ||
|
|
a138237155 | ||
|
|
ae692b1f2f | ||
|
|
e3ff385ae0 | ||
|
|
d83dcc35e2 | ||
|
|
1cd28a5ccf | ||
|
|
2bb8287519 | ||
|
|
ebfc6fa724 | ||
|
|
ebda927bb1 | ||
|
|
5792bff49d | ||
|
|
c6262a36e6 | ||
|
|
c369f0fdb7 | ||
|
|
c84eee22f2 | ||
|
|
771739cf94 | ||
|
|
739d44b561 | ||
|
|
b963fe3cf0 | ||
|
|
d97356e65a | ||
|
|
48187a0260 | ||
|
|
c431ac6306 | ||
|
|
060097c118 | ||
|
|
30bdfe9d3f | ||
|
|
0654a4373f | ||
|
|
2ab3518c52 | ||
|
|
207ab28b92 | ||
|
|
595915fefd | ||
|
|
5f5b5fef3d | ||
|
|
db803a8548 | ||
|
|
96569e71a3 | ||
|
|
121dd35c3b | ||
|
|
ee0413de4b | ||
|
|
2d2c836a34 | ||
|
|
aa1446c19d | ||
|
|
f9967a92c7 | ||
|
|
a54a726e93 | ||
|
|
0a774758b9 | ||
|
|
084e01cf46 | ||
|
|
f9d255b678 | ||
|
|
2e3b95b9ed | ||
|
|
dbe6df1ab6 | ||
|
|
1831e2e63e | ||
|
|
f0390dae63 | ||
|
|
eff6dcb514 | ||
|
|
aea779cfdf | ||
|
|
8e64670b4e | ||
|
|
9ab5b9d791 | ||
|
|
6116edaa06 | ||
|
|
0730825185 | ||
|
|
dee76adc0c | ||
|
|
e0e48925b5 | ||
|
|
1e09ccdb90 | ||
|
|
c20af6329b | ||
|
|
a229138ca6 | ||
|
|
3300276c27 | ||
|
|
7b209e5d31 | ||
|
|
e28e13bd10 | ||
|
|
9290264fa5 | ||
|
|
ebb63f2742 | ||
|
|
09fa3e5c86 | ||
|
|
0e930c9356 | ||
|
|
25840dfef4 | ||
|
|
e931866aeb | ||
|
|
1d2a11729f | ||
|
|
0b269423aa | ||
|
|
631fd6138c | ||
|
|
d4a23ffc98 | ||
|
|
7e332b817d | ||
|
|
18bca3bce1 |
||
|
|
a30e1e8b61 |
||
|
|
4f3e4a0865 |
||
|
|
77dc3086a0 | ||
|
|
0f0c9f2e65 |
||
|
|
51384403fe |
||
|
|
03a355f109 |
||
|
|
ffd51a627b |
||
|
|
c19fecb240 |
||
|
|
48f10ec52a |
||
|
|
e4209714ee |
||
|
|
95ac570c7b |
||
|
|
034c0947f6 |
||
|
|
8884c90fcf |
||
|
|
21b3e4b67c |
||
|
|
dfcabb51f0 |
||
|
|
211ebbfe5f | ||
|
|
d33021773c | ||
|
|
f3864f8295 |
||
|
|
fbce1ec5c0 |
||
|
|
1c16c329c0 |
||
|
|
244ab1378e |
||
|
|
4ff96cb28e |
||
|
|
adc6fa8409 |
||
|
|
39071aff50 | ||
|
|
c785bfa261 |
||
|
|
7840b56690 |
||
|
|
eaab413409 |
||
|
|
a994830bd8 |
||
|
|
4809884182 |
||
|
|
0f4e29d60d |
||
|
|
f14490cdb5 |
||
|
|
ff8ecdeef7 |
||
|
|
b06dc8022b |
||
|
|
92311e2089 |
||
|
|
3d82378cbd | ||
|
|
cf4d2bdfe6 |
||
|
|
9003768d56 | ||
|
|
314a6346c6 |
||
|
|
1ea8b576b3 | ||
|
|
f6d02dd816 |
||
|
|
7c024864b2 |
||
|
|
682207ffe6 |
||
|
|
b8dde41482 | ||
|
|
85d87a3b37 |
||
|
|
e4451fb207 |
||
|
|
4fe13ae858 |
||
|
|
ce87521b37 |
||
|
|
9b8e677803 |
||
|
|
182944bb30 | ||
|
|
f54cf50a0b |
||
|
|
bd5cbba03e |
||
|
|
b3c55bc200 |
||
|
|
17fbf10909 |
||
|
|
99e69c55aa |
||
|
|
7579bba079 |
||
|
|
9a2d845edd |
||
|
|
579118eb72 |
||
|
|
8c142811ef |
||
|
|
89bbbc0e27 |
||
|
|
8bf731fedd |
||
|
|
b0c18e06b9 |
||
|
|
113e77b1d7 |
||
|
|
ac59d99d8e |
||
|
|
7052e4721b |
||
|
|
eb7f311d94 |
||
|
|
d785af9c05 |
||
|
|
8ef1bc63e8 |
||
|
|
cf79ba3b80 |
||
|
|
76544a192a |
||
|
|
46c649c7ff |
||
|
|
19812197c7 |
||
|
|
0bf4e3fab3 |
||
|
|
cfa420de2e |
||
|
|
76bc057937 |
||
|
|
e2e1781612 |
||
|
|
e58ad69a38 |
||
|
|
f37e0ffde3 |
||
|
|
3e623e8b77 |
||
|
|
b27e92f11d |
||
|
|
03c4fc58a3 |
||
|
|
ba397c158a |
||
|
|
7ebdb84a30 |
||
|
|
eec6ce80ae |
||
|
|
3c3546c3d1 |
||
|
|
4ae7d5b0ef |
||
|
|
d260ee05da | ||
|
|
c870545b46 | ||
|
|
02a343624e | ||
|
|
f49bf19023 |
||
|
|
b81de36403 |
||
|
|
221b7145f1 |
||
|
|
6e1a73eea2 |
||
|
|
69788a6b9b |
||
|
|
35e13cad62 |
||
|
|
2b10cf203b |
||
|
|
c3ba9234ac |
||
|
|
2e4b543ca9 |
||
|
|
e452ba5e84 |
||
|
|
84ae91c525 |
||
|
|
17115fa74d | ||
|
|
6937e6e772 | ||
|
|
e80b058550 |
||
|
|
95a0045a0d |
||
|
|
589e846b73 | ||
|
|
bd8fdea618 | ||
|
|
6fb41b44d4 |
||
|
|
3457e2cfde |
||
|
|
13d21ce002 |
||
|
|
c9f407c224 |
||
|
|
2f777627b9 |
||
|
|
185fb9c71c |
||
|
|
eb648195de | ||
|
|
9031999033 |
||
|
|
80b5fa20fe |
||
|
|
347faa8674 |
||
|
|
cb59681cd9 | ||
|
|
e86b754390 |
||
|
|
07052d2870 |
||
|
|
d0f6c70df5 | ||
|
|
2a1e711f42 |
||
|
|
aa39f31fa9 |
||
|
|
41c1b3275e |
||
|
|
a9aac1648c |
||
|
|
fedbb0b819 |
||
|
|
4b75b42597 |
||
|
|
c30684aad1 | ||
|
|
ffeb2322f4 |
||
|
|
ab083f9eb6 | ||
|
|
1b657973a4 |
||
|
|
ed10e14b47 |
||
|
|
0995f95296 |
||
|
|
ce1e12b151 |
||
|
|
263d2ca133 |
||
|
|
b4c47b09b6 |
||
|
|
a07c80322d |
||
|
|
80b95b1fa6 |
||
|
|
c7f2a373f1 |
||
|
|
71912dad44 |
||
|
|
7da036bf34 | ||
|
|
87107c7ad8 |
||
|
|
5cd82b1a40 |
||
|
|
6eb2af1bd8 |
||
|
|
a1df89d956 |
||
|
|
74b7d22a44 |
||
|
|
f832f8e65b |
||
|
|
8e2b64d8b1 |
||
|
|
99181b9e15 |
||
|
|
bf89191c6b |
||
|
|
439924532d |
||
|
|
99056d3c2d |
||
|
|
2ad32b1be0 |
||
|
|
5fdbef39a4 |
||
|
|
63aaa9e1c8 |
||
|
|
bdcac810a1 |
||
|
|
6ec00bb159 |
||
|
|
3298ebe0e3 |
||
|
|
2482fdc7d0 |
||
|
|
3a40d17bbc |
||
|
|
0df3f8b731 |
||
|
|
1981da9667 |
||
|
|
47abb00f19 |
||
|
|
9f76532fa6 |
||
|
|
5efde5b2aa |
||
|
|
f02949ef0a | ||
|
|
62e24a9c2b |
||
|
|
bfd47de523 |
||
|
|
5055d7d481 |
||
|
|
0e4eb4e269 |
||
|
|
9e69aed06b |
||
|
|
f39e013917 | ||
|
|
27c694e6ed |
||
|
|
956dbac94c |
||
|
|
335ceb8013 |
||
|
|
1fcd81229c |
||
|
|
8635f2f3d8 | ||
|
|
64c3c5b446 | ||
|
|
6a4a45711b |
||
|
|
6d183325da |
||
|
|
2c852de7ff |
||
|
|
e9370c984e |
||
|
|
82ed742c27 |
||
|
|
3a3e89ccdd |
||
|
|
500472a358 | ||
|
|
4038236e8a | ||
|
|
dedaa1f337 | ||
|
|
e97db8e244 |
||
|
|
4f0a22482b |
||
|
|
4c9bb98a9b |
||
|
|
1c046ddac8 |
||
|
|
9cd9d4c7d2 |
||
|
|
4b3374c749 |
||
|
|
c67909a511 |
||
|
|
ca63aac056 |
||
|
|
a56dbd79ac |
||
|
|
057599819d |
||
|
|
520865c558 |
||
|
|
61a097f2f3 |
||
|
|
106092c630 |
||
|
|
e033010841 | ||
|
|
53436ef97d |
||
|
|
a2d20ecd42 |
||
|
|
c7082db602 |
||
|
|
eb522e9208 |
||
|
|
dd61e6e09a | ||
|
|
943e25cb3e |
||
|
|
d2b2cf308c |
||
|
|
a7d79c8a7f |
||
|
|
fe047264fb |
||
|
|
e23fe19504 |
||
|
|
d5a3241887 |
||
|
|
c26b703f66 |
||
|
|
70188bff72 |
||
|
|
9e51d12acd |
||
|
|
e04c100fe6 |
||
|
|
b1f110c1a3 |
||
|
|
a3c8212669 |
||
|
|
bddb31b8b2 |
||
|
|
e91a55fd99 |
||
|
|
f01bf0cc09 |
||
|
|
4ec6649b39 |
||
|
|
419f193348 |
||
|
|
d90b9445b2 |
||
|
|
ade556c300 |
||
|
|
7c96e1ff03 |
||
|
|
414cd390b6 |
||
|
|
e321c7eb99 | ||
|
|
927fa92518 |
||
|
|
7c4fce55ff |
||
|
|
9434f87d66 |
||
|
|
6e7eb04ee8 |
||
|
|
5269106198 |
||
|
|
97ae79a6a3 |
||
|
|
b0095488f9 | ||
|
|
89fe386611 |
||
|
|
0a6649e938 |
||
|
|
9980419bc6 |
||
|
|
af7e7e8281 |
||
|
|
a7817cf94b |
||
|
|
c018bbfaf4 |
||
|
|
568ae78fad |
||
|
|
a6aff32655 | ||
|
|
ed7ddf43fe |
||
|
|
1aa14b1f98 |
||
|
|
4b7d2ebe78 |
||
|
|
8b4d5b2981 |
||
|
|
76aacdfaf1 |
||
|
|
acaee577e7 |
||
|
|
b337a60027 |
||
|
|
be9db8c4b0 |
||
|
|
e8c5c5580e |
||
|
|
1a962a774b |
||
|
|
ccfcec2361 |
||
|
|
238eaa70df |
||
|
|
087b266408 |
||
|
|
c92b067a5a |
||
|
|
ea9da4b217 |
||
|
|
bc1a43e5d0 |
||
|
|
b6e9facf4b |
||
|
|
cb9ffc3a99 |
||
|
|
911b35793e |
||
|
|
132c47ac21 |
||
|
|
b3b2e2a4ab |
||
|
|
18f96934f2 |
||
|
|
d5aa00b753 | ||
|
|
5b40a6fb58 |
||
|
|
3ed7d96ea3 |
||
|
|
3b9bc48593 |
||
|
|
57b30e1b79 |
||
|
|
511df7cef2 |
||
|
|
8d5d9617ae | ||
|
|
6013977dea |
||
|
|
5a258796df |
||
|
|
56cc8fbe32 |
||
|
|
b8f0a4601c | ||
|
|
bb6a9657a0 | ||
|
|
bebc9db629 |
||
|
|
2c69a28ffd | ||
|
|
cb1127de74 |
||
|
|
90f8b9a817 |
||
|
|
c5f8bb20bc |
||
|
|
1d8a0e639f | ||
|
|
de028e5dd8 | ||
|
|
010ddab78e |
||
|
|
f1c84c1ede |
||
|
|
9686c0383e | ||
|
|
338b1d2642 | ||
|
|
a6a2336a6f |
||
|
|
68eeb76975 |
||
|
|
f23a27ce9d |
||
|
|
39992db5d0 |
||
|
|
f25207aa85 |
||
|
|
454d71888b |
||
|
|
0636a6f839 |
||
|
|
f028ac2ea0 |
||
|
|
7affe78e5a |
||
|
|
9f41abf246 |
||
|
|
58d617b70d |
||
|
|
9a5188930a |
||
|
|
6d6b99576f | ||
|
|
ace9adce19 | ||
|
|
33368b9ee2 |
||
|
|
1d7ceda9d4 | ||
|
|
f350ca11ca |
||
|
|
962cc973c3 |
||
|
|
a9d2f317eb |
||
|
|
b9bd90ffe5 |
||
|
|
b6fb9ef5da | ||
|
|
4db81e1f28 | ||
|
|
b6c2b201eb |
||
|
|
206802a55f |
||
|
|
7946603c54 |
||
|
|
81841bd005 |
||
|
|
f79c54adb0 |
||
|
|
01e7b88037 |
||
|
|
78f0e544db | ||
|
|
63b78ed7c4 |
||
|
|
79801af219 |
||
|
|
d087c726e0 | ||
|
|
f3b67b773b | ||
|
|
fb2cf54c38 |
||
|
|
dc6e29476a |
||
|
|
e321c90f2a |
||
|
|
8b4b8fbe9e | ||
|
|
d303246d24 |
||
|
|
aceffed61f | ||
|
|
dfcec914a6 |
||
|
|
e976719d91 |
||
|
|
9a0cb9f7bf |
||
|
|
41b1373c6c |
||
|
|
e57c7be2f2 |
||
|
|
1386d0ce9c |
||
|
|
92cd08ea55 |
||
|
|
2352ea5f92 |
||
|
|
91660f9e9e |
||
|
|
9487afdf24 |
||
|
|
29d634d9f7 |
||
|
|
c21a3d3bd9 |
||
|
|
1436985d30 |
||
|
|
1831134bca |
||
|
|
8d5feed487 |
||
|
|
02d8db74c7 |
||
|
|
509a2c9358 |
||
|
|
e3b4a8e0b5 |
||
|
|
7c00da2d33 |
||
|
|
1ebe3864f9 |
||
|
|
c06b96e90b |
||
|
|
48c76efe27 |
||
|
|
33b52611b4 |
||
|
|
1270c312d0 |
||
|
|
bad5dac3ab | ||
|
|
d7a37cfdc0 |
||
|
|
ecc1fdd799 |
||
|
|
88ce0cee99 | ||
|
|
3b9b5e26f4 |
||
|
|
7c0e2af0f3 |
||
|
|
d26db390c3 |
||
|
|
2162f4a55a | ||
|
|
da2f9c4092 |
||
|
|
2054c5cf82 |
||
|
|
9035acd05d |
||
|
|
c62665f47b | ||
|
|
4a7be7f954 |
||
|
|
6884b7462a | ||
|
|
42ab1d1402 |
||
|
|
c1ea46b430 |
||
|
|
ed06040b8e |
||
|
|
05482e952b | ||
|
|
557d4c4ddd | ||
|
|
d326435fe7 | ||
|
|
62cb252933 | ||
|
|
67a28d1e94 |
||
|
|
658dc5a3a2 | ||
|
|
45235ba7aa | ||
|
|
b93827278f |
||
|
|
79f1685d6a |
||
|
|
2745931e9e |
||
|
|
b037d597ed |
||
|
|
7f4aa36c7d |
||
|
|
6faedf7d16 |
||
|
|
16dc241cad |
||
|
|
41bd5504a0 |
||
|
|
2ba7b563fe |
||
|
|
24d94f4f29 |
||
|
|
30fd693a67 |
||
|
|
7cfd4afb48 |
||
|
|
22d2a0dc2b |
||
|
|
7306e30a60 |
||
|
|
9f02d4f956 |
||
|
|
c081fb378d |
||
|
|
66c7159521 |
||
|
|
e97c9ff265 | ||
|
|
ee8db6da8c |
||
|
|
95a2580b6e |
||
|
|
e3a000ead3 |
||
|
|
6e3421cd64 |
||
|
|
81b7f8abc3 |
||
|
|
81b5d6abdc |
||
|
|
42a02452cb |
||
|
|
cd1454e48d |
||
|
|
4c9573f8d8 |
||
|
|
593b1cb450 |
||
|
|
72fe898e60 |
||
|
|
b538360c5e | ||
|
|
a4f889d134 |
||
|
|
e0ceaf4af2 |
||
|
|
69fa473c91 |
||
|
|
c0436cde5e |
||
|
|
76deb4b2f9 |
||
|
|
d3b7a205b4 |
||
|
|
35c328b557 | ||
|
|
d0605d33b8 | ||
|
|
d6a150c5d6 |
||
|
|
bec241e7fa | ||
|
|
52ed6f77f4 |
||
|
|
05ddf16343 |
||
|
|
b57f2e9ad6 |
||
|
|
562a2cbed4 |
||
|
|
503abe985f |
||
|
|
2bb627e7f2 |
||
|
|
2156d6c41f |
||
|
|
8fa8526698 | ||
|
|
8435153de0 |
||
|
|
0e053f2f47 |
||
|
|
a39f68b4e3 |
||
|
|
dce3fb229f |
||
|
|
a27c33e696 |
||
|
|
6d2bde023f |
||
|
|
9d5de22d94 |
||
|
|
ce212e001c | ||
|
|
7c12883dc1 |
||
|
|
bb88e49dc5 |
||
|
|
5acb1d4fd5 |
||
|
|
f2ee299004 | ||
|
|
bae398235e |
||
|
|
11413a9288 |
||
|
|
621ab2ac07 |
||
|
|
9fb871c2f7 |
||
|
|
b3c282f663 |
||
|
|
aabe0824a7 |
||
|
|
8c06bd3a75 |
||
|
|
5ddb988996 |
||
|
|
8bb2185961 |
||
|
|
81913bca66 | ||
|
|
db6ff5debf |
||
|
|
4d45c5d686 |
||
|
|
0519c8a857 | ||
|
|
e14d911195 | ||
|
|
b3fcea866c |
||
|
|
6bb0f40fe5 |
||
|
|
40ec21a117 |
||
|
|
7830bfb096 | ||
|
|
932be73b42 |
||
|
|
295fe2bd09 | ||
|
|
e619579b1b |
||
|
|
113e30d03d | ||
|
|
96507f5fe7 |
||
|
|
dc27f5f241 |
||
|
|
9e62119145 |
||
|
|
94acacd21d |
||
|
|
219443ac1f |
||
|
|
e3ab1e064a |
||
|
|
7ae52d197a |
||
|
|
cea5d5ef2c |
||
|
|
0569ee2269 |
||
|
|
0490fc40e9 |
||
|
|
c803154dbb |
||
|
|
0a35000cb8 |
||
|
|
f8bb8f9d6f | ||
|
|
e950090383 |
||
|
|
a4b59fceef |
||
|
|
4d5fa57e22 |
||
|
|
9bcec3bea5 | ||
|
|
d83594daa4 |
||
|
|
5f190fc165 |
||
|
|
f3d45d6eb7 |
||
|
|
d67a4244b2 |
||
|
|
d3725937ef |
||
|
|
82a570bf20 |
||
|
|
31059131a9 |
||
|
|
2cda9ced8c |
||
|
|
61ce0e2f0b |
||
|
|
dbd6bee487 |
||
|
|
5ce3b27cdd |
||
|
|
017a338802 |
||
|
|
fad30dd80f |
||
|
|
50661b5947 | ||
|
|
0e785cf51d |
||
|
|
a2f78d0aeb |
||
|
|
05729b3803 |
||
|
|
8d5d6f0285 |
||
|
|
f53448c844 |
||
|
|
786c3d1e0b |
||
|
|
e129401747 |
||
|
|
b1dcbd3811 |
||
|
|
c9ba896d9e |
||
|
|
70a3aefd2f |
||
|
|
13e6f37eda |
||
|
|
039d1220cb |
||
|
|
174c6c744c |
||
|
|
8a71d1df70 |
||
|
|
40d2fdf5bc |
||
|
|
91fe10d25f |
||
|
|
7b1401e89e |
||
|
|
8b5147393a |
||
|
|
7ffaf48f67 |
||
|
|
f98708f3cd |
||
|
|
619b054b95 | ||
|
|
22743982da |
||
|
|
edc10cdf65 |
||
|
|
51b574d9c7 |
||
|
|
9c267d95ae |
||
|
|
92b5412d12 |
||
|
|
051eb83b02 |
||
|
|
9dc9f164cd |
||
|
|
14172fca0a |
||
|
|
da01d7a313 |
||
|
|
8e6de0d2e6 |
||
|
|
247bc507ea |
||
|
|
bf50a23df8 |
||
|
|
87d8905b65 |
||
|
|
0aa04ac042 |
||
|
|
cc7da34b44 |
||
|
|
3d28321068 |
||
|
|
cf7088ea79 |
||
|
|
2a74ace48f | ||
|
|
927da2816a |
||
|
|
5889547161 |
||
|
|
a131695388 |
||
|
|
59ff700a69 |
||
|
|
c2abc75e47 |
||
|
|
35562a9f4a |
||
|
|
c66fdcc527 |
||
|
|
7a5708714b |
||
|
|
ddca4255b7 |
||
|
|
7b926f7c32 |
||
|
|
cc4ab38356 |
||
|
|
db8aeaa827 |
||
|
|
486be09274 |
||
|
|
2b89500010 |
||
|
|
8c09be9db3 |
||
|
|
77d70d6d91 |
||
|
|
2f2830c6ce |
||
|
|
d8dcd0bd47 |
||
|
|
1acc93d566 |
||
|
|
6fa9ce3504 |
||
|
|
41253a82be |
||
|
|
e8c02d5ad2 |
||
|
|
9bee3eca5a | ||
|
|
78eae2be88 |
||
|
|
265876786c | ||
|
|
5b2de64af8 |
||
|
|
33a3aa8882 |
||
|
|
f6665da2ed |
||
|
|
e90a0da31d |
||
|
|
b8db7a2f82 |
||
|
|
fbe8a4151c |
||
|
|
f69571847a |
||
|
|
09d6dfc604 |
||
|
|
cff6d2fd9c |
||
|
|
8e48c6d768 |
||
|
|
8b1e7c0f72 |
||
|
|
8d5de36987 |
||
|
|
dc8aa04887 |
||
|
|
c999e6a8bf |
||
|
|
80d6cc78c3 |
||
|
|
ecb817b866 |
||
|
|
69e3728f5e |
||
|
|
7c1619554d |
||
|
|
61bd7f1f20 |
||
|
|
482bdf51f6 |
||
|
|
8fa4a2833f |
||
|
|
390d7c22ca |
||
|
|
91dcb7564b |
||
|
|
ee4c8d78d3 | ||
|
|
5209b12d44 | ||
|
|
8f5182b379 | ||
|
|
fd5652ed60 |
||
|
|
056a38caeb |
||
|
|
3f222c38ee |
||
|
|
cf356e3312 |
||
|
|
1ea7e490b9 |
||
|
|
01f52752ca |
||
|
|
58de64232c |
||
|
|
8fdf41a827 |
||
|
|
08e3f54a89 |
||
|
|
4b84adb834 |
||
|
|
4038240127 |
||
|
|
41f82f4510 |
||
|
|
9f79ae2f19 |
||
|
|
35ffe9d5b9 |
||
|
|
bb26da2c3a |
||
|
|
e8b3836050 | ||
|
|
3bf24047dd |
||
|
|
74e5d2c930 |
||
|
|
71dad07480 |
||
|
|
9bb8d4f385 | ||
|
|
3a11cd2dda |
||
|
|
3eca924a7c |
||
|
|
b39a4a67e2 |
||
|
|
6187b3bd0b |
||
|
|
da35ca3559 |
||
|
|
7f94a47a50 |
||
|
|
d41ca1a1f9 |
||
|
|
f58e171a69 |
||
|
|
ff98f31be2 |
||
|
|
f249747642 |
||
|
|
c8313af571 |
||
|
|
40ac64f2e4 |
||
|
|
95e6fb3a49 |
||
|
|
45b87ea905 |
||
|
|
b6b0e70046 |
||
|
|
9447b4894d | ||
|
|
f5b70092b2 |
||
|
|
28df906957 | ||
|
|
0f6fd4dac5 | ||
|
|
1f604a8556 | ||
|
|
11bd2dfa4c | ||
|
|
cdebd532ce | ||
|
|
d5f0a2481f |
||
|
|
94c7a09f55 |
||
|
|
21c4dea8a9 |
||
|
|
93fcd57b2d |
||
|
|
2bd82fec6e |
||
|
|
8c7d7163c9 |
||
|
|
7bb8089095 |
||
|
|
904664743f |
||
|
|
69c1b0a55a |
||
|
|
8873ac3524 |
||
|
|
5cd805fc44 |
||
|
|
19ac4f72aa | ||
|
|
0a8f83c2fe |
||
|
|
752eca4eef |
||
|
|
251ed60f1c |
||
|
|
c5b4f4e9c4 |
||
|
|
184f6bae09 |
||
|
|
d63f0ccc50 | ||
|
|
b5390f1ef3 |
||
|
|
c919eba4cd |
||
|
|
238e68fab9 |
||
|
|
e0fcf97777 |
||
|
|
4eb599e786 |
||
|
|
45a68d21e8 |
||
|
|
3a89935c29 | ||
|
|
a9044fadae |
||
|
|
dc6ca963f5 |
||
|
|
671dad4ed6 | ||
|
|
f9e522cc0e |
||
|
|
4109480b27 |
||
|
|
9b697b414a |
||
|
|
ada6439ae4 |
||
|
|
fc582d5351 |
||
|
|
14e982e2d8 |
||
|
|
e5fc54a3c0 |
||
|
|
0d82f8159c |
||
|
|
9126bac89b |
||
|
|
8cda22056f |
||
|
|
84c106858a |
||
|
|
a24d932102 |
||
|
|
90e55bd759 | ||
|
|
51f5cda4ff |
||
|
|
f1b88e5216 | ||
|
|
82c9c3c26a |
||
|
|
06819dc4b9 |
||
|
|
db22669050 |
||
|
|
87a995fe0b |
||
|
|
1ebbacdb2a | ||
|
|
d1a995471e |
||
|
|
46abaeb279 | ||
|
|
6089372ac7 |
||
|
|
397b5e8f47 |
||
|
|
b80a1f11f0 |
||
|
|
061fa615ce |
||
|
|
6639452c87 |
||
|
|
935db3cdd1 |
||
|
|
f35e718b2e |
||
|
|
742ba4cfdb |
||
|
|
54e502c020 | ||
|
|
8a1c2a1596 |
||
|
|
5e438b7fab |
||
|
|
7e8610bbac |
||
|
|
1d6c6dd52b |
||
|
|
dfa19bac18 |
||
|
|
1de4404bb2 |
||
|
|
4b19f79cb8 |
||
|
|
10642b8f5a |
||
|
|
f2acae4ce1 |
||
|
|
c2ce56292c |
||
|
|
d00cf72614 | ||
|
|
3e029c4e8a |
||
|
|
2379c523be |
||
|
|
bbba3df6d3 |
||
|
|
a7474dd5f0 |
||
|
|
b986025e1a |
||
|
|
5091939aa4 | ||
|
|
62e2cbe66b | ||
|
|
c95b09cbac |
||
|
|
b8d60ddaa6 | ||
|
|
bdccfd82f1 |
||
|
|
a9413dc277 | ||
|
|
5827b151ee |
||
|
|
d9abe9224e | ||
|
|
b744ff6b1e |
||
|
|
a124e7bdb8 | ||
|
|
08682d2448 | ||
|
|
36aef26ce9 |
||
|
|
9d278a1fe2 |
||
|
|
e2602b28ad |
||
|
|
0be8de738a |
||
|
|
511e612e16 |
||
|
|
c46eca1873 |
||
|
|
9a808b58e9 | ||
|
|
a637cb0632 |
||
|
|
eefe0279a1 |
||
|
|
7485850606 |
||
|
|
74cbfe973a | ||
|
|
a07b372c71 | ||
|
|
1d797af794 |
||
|
|
472d618033 | ||
|
|
6bbe569dac |
||
|
|
2cf5a0fd96 |
||
|
|
9d8d8eefa6 |
||
|
|
254e39df18 |
||
|
|
24531538fd | ||
|
|
012ca805c1 | ||
|
|
d178ac9749 | ||
|
|
f814d7369c |
||
|
|
8137ece450 |
||
|
|
bff38efd50 |
||
|
|
a97aa59689 |
||
|
|
ad3688e61d |
||
|
|
0e50cb4f06 |
||
|
|
6201df072e |
||
|
|
a81cef397c |
||
|
|
dfee1a0fb0 |
||
|
|
8e211405b1 |
||
|
|
4f018ba23b | ||
|
|
acfd493d9e |
||
|
|
1f96342590 |
||
|
|
62381a0731 |
||
|
|
65bd014a21 |
||
|
|
e11f97a2bb | ||
|
|
cf18905775 |
||
|
|
25e26a5b28 |
||
|
|
a0a2e91928 | ||
|
|
be2b81c478 |
||
|
|
4d17cfb715 |
||
|
|
5dced897d8 |
||
|
|
2640027bd8 |
||
|
|
b54862022e |
||
|
|
9b6e6efce2 |
||
|
|
063ef1a98e | ||
|
|
a267add7a4 |
||
|
|
32ccfd50af |
||
|
|
eff7618f79 |
||
|
|
7a3008aec6 | ||
|
|
ea00587c00 | ||
|
|
0d48c596c8 | ||
|
|
12cd1b2614 |
||
|
|
e420a210b9 |
||
|
|
4e2523060b | ||
|
|
663b9daef6 |
||
|
|
af11b864ce |
||
|
|
a3f88ffa5b |
||
|
|
d94da2aa09 |
||
|
|
7ea32d8a71 |
||
|
|
49babf773e |
||
|
|
e3808068af |
||
|
|
50b4fa3441 |
||
|
|
3b2173c694 | ||
|
|
825c1b8986 |
||
|
|
011bf80038 |
||
|
|
b7428c2b20 |
||
|
|
3cd8ce6514 | ||
|
|
9c4c1d6d51 | ||
|
|
b7072c8955 |
||
|
|
d2987d037c | ||
|
|
213586be95 |
||
|
|
a1a45f8c67 |
||
|
|
9d5159477c |
||
|
|
84ac5f55fe |
||
|
|
e0135d0832 |
||
|
|
e5954ec0d8 | ||
|
|
56de773614 |
||
|
|
838af985e1 |
||
|
|
59e8e1aa7e |
||
|
|
8a7212a04f |
||
|
|
730c266ea8 |
||
|
|
5f9c4878b9 |
||
|
|
0a1a4fc68a |
||
|
|
0a3ded2957 |
||
|
|
de1e6091bf |
||
|
|
36797f3594 |
||
|
|
3248f0db1e |
||
|
|
c0d712c53d | ||
|
|
01cc10dc43 |
||
|
|
4a9a58e19c |
||
|
|
cd55e7bb2a |
||
|
|
5d748a59cc |
||
|
|
8653b2a382 |
||
|
|
fe49b005da |
||
|
|
f4e0cde4e6 |
||
|
|
4d94ab4218 | ||
|
|
b0f32dc29b | ||
|
|
a6ed4f3652 |
||
|
|
a71fc83401 |
||
|
|
cd1763cbf8 |
||
|
|
b336e4f202 |
||
|
|
a663993d0b | ||
|
|
2d13eaaf6b |
||
|
|
fa66c3f23b | ||
|
|
4ee4d015cb |
||
|
|
35841b3369 | ||
|
|
c0ff2fe2b2 |
||
|
|
e5059c73ed |
||
|
|
3f26cee4d4 |
||
|
|
4824a2b535 |
||
|
|
6ee06583a9 | ||
|
|
8f7a141711 |
||
|
|
ecaf269fe1 |
||
|
|
850bfdd722 |
||
|
|
f1ad8fa8f2 |
||
|
|
a35154185a |
||
|
|
03b5e2c5db |
||
|
|
52ca445636 |
||
|
|
8fffa6b03f |
||
|
|
d0d71b309e |
||
|
|
b7ed3066c1 |
||
|
|
7037a58ca9 |
||
|
|
6f8e26bdbf | ||
|
|
b4693fb768 |
||
|
|
20abf92e4b |
||
|
|
5344768e93 |
||
|
|
0855d51e10 |
||
|
|
ab29a7ba74 |
||
|
|
24b9b93c3e |
||
|
|
4994c3bca8 |
||
|
|
20d36a0502 |
||
|
|
d0284dc1b9 | ||
|
|
000d56a96d | ||
|
|
2de022235f |
||
|
|
df006845ae |
||
|
|
e30984a13d |
||
|
|
d185a78af7 |
||
|
|
899ca6737b |
||
|
|
d4b028556a |
||
|
|
a221bf9d40 |
||
|
|
468427bfdb | ||
|
|
07fcccbda5 | ||
|
|
929047b6a5 | ||
|
|
0de90daa64 |
||
|
|
19cce592c8 |
||
|
|
0277ac1a73 |
||
|
|
a37d6478e4 |
||
|
|
67a73fb79d |
||
|
|
979eb5c6ca |
||
|
|
d39224c594 |
||
|
|
490c94b33a | ||
|
|
4890d6d477 | ||
|
|
de70e0ab0a | ||
|
|
944b8d9f34 |
||
|
|
3c110a7d30 |
||
|
|
d5e1211e28 |
||
|
|
3c1c5e0085 |
||
|
|
aa7c7c16e5 |
||
|
|
99ad9e3c24 |
||
|
|
d2e47c5d86 |
||
|
|
25dc2848ca | ||
|
|
87553a4531 |
||
|
|
a7fdea723f |
||
|
|
ce2c58848b |
||
|
|
5cfec76d3a | ||
|
|
97dfdbf7c0 | ||
|
|
42344302de | ||
|
|
d4fa6bbcb0 | ||
|
|
629ae8bfa4 | ||
|
|
15c14c6dea | ||
|
|
58ec2768ec | ||
|
|
c7c2587079 |
||
|
|
02aa47d8c6 |
||
|
|
b07ea86034 |
||
|
|
8982dd556c |
||
|
|
f7d2ca1da0 |
||
|
|
ea9961f9f6 |
||
|
|
636edf500c |
||
|
|
9267ae99cb |
||
|
|
77d4fc89e0 |
||
|
|
2f44cab13f |
||
|
|
d4f42255b3 |
||
|
|
173b831cf7 |
||
|
|
62e4077e93 |
||
|
|
9bc1860345 |
||
|
|
f93c00cd1a |
||
|
|
22a32f19eb |
||
|
|
a764e7b19a |
||
|
|
8a5d6bb340 |
||
|
|
c3bd6f5e8e |
||
|
|
f56c8fbbf1 |
||
|
|
0fa43eec4b |
||
|
|
43b5cc190e |
||
|
|
4a732c01e5 |
||
|
|
3a25134e8c |
||
|
|
e2e6fe2533 | ||
|
|
0833f143ff | ||
|
|
db4661339a |
||
|
|
8442472f37 | ||
|
|
9d5f213289 | ||
|
|
67abcb03c2 | ||
|
|
31a8055c6e | ||
|
|
8edc1be7b5 |
||
|
|
d22f0344ea |
||
|
|
bab78f9913 | ||
|
|
0294643403 | ||
|
|
f5a8f23d2b |
||
|
|
0305dadc7f |
||
|
|
4219fabca8 |
||
|
|
1e3b2ca2f7 | ||
|
|
17680b130e | ||
|
|
bdfc367c6c | ||
|
|
9ce586e21d |
||
|
|
c6f7aa2eda | ||
|
|
faeb00af25 |
||
|
|
7a9bc497fc | ||
|
|
8899446fe3 | ||
|
|
2d49e34805 | ||
|
|
f54fef4210 |
||
|
|
97b178dbdb | ||
|
|
95a435c5c9 | ||
|
|
e05b107cc1 |
||
|
|
621082b0a4 | ||
|
|
1e454ae5ed | ||
|
|
c169a9e1b8 |
||
|
|
0cd6366b5c |
||
|
|
907b92ed61 |
||
|
|
30ad035b07 | ||
|
|
db83d96527 | ||
|
|
8cb0ff6308 | ||
|
|
4c6cef76f5 | ||
|
|
4a9c3236aa |
||
|
|
45bad20bb3 |
||
|
|
d92770edb4 | ||
|
|
d4d65b5377 | ||
|
|
71a36ebee0 | ||
|
|
ea4b694ac3 | ||
|
|
7fa67de9aa |
||
|
|
fd6048c7b6 |
||
|
|
db07f2af57 | ||
|
|
efa5b03cf8 | ||
|
|
d4d516af1e | ||
|
|
55c1293b4c | ||
|
|
5aafb38624 |
||
|
|
a729d909a7 | ||
|
|
dbcf69206a | ||
|
|
1e5e9bbc57 |
||
|
|
f23c063b71 | ||
|
|
7dd6340e46 | ||
|
|
e1b6ba9f0d | ||
|
|
94f1d8dde0 | ||
|
|
ee5e82fe9a | ||
|
|
426893077f | ||
|
|
5e1beb5b46 |
||
|
|
7f11af1c13 | ||
|
|
cd34616339 | ||
|
|
9558a1af58 |
||
|
|
0c8765647a |
||
|
|
df7e62915b |
||
|
|
a71904a25e |
||
|
|
ddad808d88 | ||
|
|
1790e9f530 |
||
|
|
f1477a3608 |
||
|
|
b6881c20d2 | ||
|
|
44c5f71ac2 |
||
|
|
6c41b68bca | ||
|
|
44a94ea04d | ||
|
|
d86fe768f3 | ||
|
|
d19c00faab | ||
|
|
162b801839 | ||
|
|
77dec4da01 | ||
|
|
860e2607e4 | ||
|
|
886301e765 |
||
|
|
f50d383670 | ||
|
|
b3e57a013f | ||
|
|
4ddb5d5b7d |
||
|
|
0346f2238f |
||
|
|
2a423174b1 |
||
|
|
487d8c4a3d | ||
|
|
0f4adedeab | ||
|
|
16236ec0bf |
||
|
|
6bf4ea9f43 |
||
|
|
38f3557e0b | ||
|
|
cd81a2bcec | ||
|
|
37a35eeece |
||
|
|
e2e3a59336 |
||
|
|
1daad45f11 |
||
|
|
2da5920bda |
||
|
|
fb1aef882a | ||
|
|
8618af2ee0 | ||
|
|
14cc8b7827 | ||
|
|
2ea6446323 |
||
|
|
bf8a16b7fe | ||
|
|
a8b9d8d96c | ||
|
|
39d59edece | ||
|
|
92c4df6e9c |
||
|
|
8d7cd4ea91 |
||
|
|
02db91bcc9 | ||
|
|
e136edb6ac | ||
|
|
0ad9def514 | ||
|
|
4f85779e78 |
||
|
|
4ff5995617 |
||
|
|
bf2a80a7b5 |
||
|
|
c5564f6bff |
||
|
|
d202eaa267 |
||
|
|
f7d34739b5 | ||
|
|
dfdcb95a5d | ||
|
|
b4d5732a50 | ||
|
|
810ecd429a |
||
|
|
73b1293522 |
||
|
|
3d56e0aad0 |
||
|
|
f6b7ce7383 |
||
|
|
74f9421417 |
||
|
|
8e2cce5b43 |
||
|
|
ccb01d964c |
||
|
|
580e3c7675 | ||
|
|
590d0fc228 | ||
|
|
808b2b249b | ||
|
|
49c5e6a502 |
||
|
|
bb260c4c04 |
||
|
|
ec8e4fc458 |
||
|
|
4fe53fc170 |
||
|
|
c46160c99e |
||
|
|
619d49b669 | ||
|
|
b6ca2f08ff | ||
|
|
8b4095c0cf | ||
|
|
2e9a8dfb0d | ||
|
|
c546228c7e |
||
|
|
bfdd751db9 | ||
|
|
e1eb0e6353 |
||
|
|
b381c9161d |
||
|
|
e254f76459 |
||
|
|
a5802f492b | ||
|
|
2da5c270ff | ||
|
|
1762db8bd4 |
||
|
|
c9dc323bcc |
||
|
|
235a3dad92 |
||
|
|
3f540a8240 |
||
|
|
43c9665523 | ||
|
|
612a58eb2f | ||
|
|
0661b4aa37 |
||
|
|
31646f2a28 |
||
|
|
7ed8188f54 |
||
|
|
0797cfad84 |
||
|
|
2006902b8a |
||
|
|
3cf4e2105e |
||
|
|
62917d3d1f |
||
|
|
73b1124dce | ||
|
|
34f4d0abf4 |
||
|
|
a392a08c7a | ||
|
|
efae5fd28d |
||
|
|
0793c35eb3 | ||
|
|
0dff676021 | ||
|
|
c98b755f17 |
||
|
|
f49bf09261 | ||
|
|
f2e5511544 | ||
|
|
a242fb15a3 |
||
|
|
09482aed7b |
||
|
|
81188e8ef6 |
||
|
|
fb49f468f1 | ||
|
|
b67fdb7806 | ||
|
|
5a9233c426 |
||
|
|
2bea5f67b9 | ||
|
|
c1406adcb2 | ||
|
|
f86d73972d | ||
|
|
682d3070e9 | ||
|
|
ee6fb8c619 |
||
|
|
2b051194c8 |
||
|
|
b08b23d59e |
||
|
|
39ca8f74ae |
||
|
|
0010d4965c |
||
|
|
6f3c88663a |
||
|
|
7d1af273d1 |
||
|
|
8d755793f2 | ||
|
|
d3efa6b82f | ||
|
|
99386510d8 | ||
|
|
bb16b3e30b |
||
|
|
d55420faae |
||
|
|
ac02bd370b |
||
|
|
dbabdd83dc |
||
|
|
8d255fc331 |
||
|
|
1ccdeb015c |
||
|
|
53a2101de3 |
||
|
|
5bff07f6f6 |
||
|
|
527cac4c75 |
||
|
|
7355db94d6 |
||
|
|
ff5a231ea0 |
||
|
|
7cbc0dc047 |
||
|
|
da23d42692 | ||
|
|
07ec00c33f | ||
|
|
e83079eec4 | ||
|
|
d28a9a9253 |
||
|
|
9380230614 | ||
|
|
b230358553 | ||
|
|
6626fbcfbd |
||
|
|
bbb41a2b5b | ||
|
|
c28a5c25b9 | ||
|
|
d6e6dab565 | ||
|
|
0030f68831 | ||
|
|
fe6ac3e954 |
||
|
|
4e8cc2e164 |
||
|
|
e9123af089 |
||
|
|
d4b9b6ed32 | ||
|
|
62fb5524ae |
||
|
|
4e5c924e5c |
||
|
|
be591d016a |
||
|
|
7b13c6f076 |
||
|
|
36a62fb365 |
||
|
|
b4b571b50f |
||
|
|
260c0dd538 | ||
|
|
6b263bf43d | ||
|
|
63c84cd362 | ||
|
|
7591088982 |
||
|
|
e94301b122 |
||
|
|
f58aac6fb3 |
||
|
|
27e4994d10 | ||
|
|
946d01a61b | ||
|
|
084ed153e3 | ||
|
|
d7f4a14bfe | ||
|
|
46e7231bfa | ||
|
|
7a752081e7 |
||
|
|
316e199aa4 |
||
|
|
95dc519019 | ||
|
|
107749e91a | ||
|
|
e967859d5f | ||
|
|
09a6c8b067 |
||
|
|
153d9c6b2c |
||
|
|
205fed6992 |
||
|
|
86474bd535 |
||
|
|
357e010f39 |
||
|
|
be85a7f224 |
||
|
|
5196c1ebad |
||
|
|
7054fe2bd2 | ||
|
|
e936233ead | ||
|
|
8f838b80e7 | ||
|
|
2e964d0a7e |
||
|
|
fe08547d6b |
||
|
|
ce08201d13 |
||
|
|
faf8e525b3 |
||
|
|
45bdfe205a |
||
|
|
d34d11fbb3 | ||
|
|
1f86ace5d8 | ||
|
|
56004ec338 | ||
|
|
12cdec3f2e |
||
|
|
ac4c857146 |
||
|
|
27e715f4a4 |
||
|
|
c4be637ba8 |
||
|
|
7d057a9ca7 |
||
|
|
3d04afa4f6 |
||
|
|
9aa6f3f590 |
||
|
|
cce29fe2c5 | ||
|
|
5005d7a1f1 | ||
|
|
edba18375f |
||
|
|
a0523b3427 |
||
|
|
a2440e665f | ||
|
|
3aea9d34e9 | ||
|
|
f856229141 | ||
|
|
8ce55af55d | ||
|
|
23c969ad1b | ||
|
|
1dc356a8f2 | ||
|
|
2cfd0e1fe0 |
||
|
|
79ea700121 |
||
|
|
5738642d44 |
||
|
|
0a48b28d83 |
||
|
|
fab237d373 |
||
|
|
a222dc9237 | ||
|
|
25dee77600 | ||
|
|
1ddc617b79 | ||
|
|
d8eed4d15e |
||
|
|
950bad1d7a | ||
|
|
80c6e48b98 |
||
|
|
13768a7d28 |
||
|
|
0d9eb5d198 |
||
|
|
beb2af73b7 |
||
|
|
ca96d09a23 | ||
|
|
f82edcd7e7 | ||
|
|
3afc38efcb |
||
|
|
3886c2a82f | ||
|
|
18c82f2d06 |
||
|
|
051f6c5a7f | ||
|
|
2e7f6d4b6a | ||
|
|
51696ed813 |
||
|
|
1672de266e | ||
|
|
43be85cee9 |
||
|
|
ab74fa6e2c | ||
|
|
5ebd6e313b | ||
|
|
de59dc7d9f |
||
|
|
8194433372 |
||
|
|
819f6c5ad2 |
||
|
|
69c7b32c1d | ||
|
|
95047ba695 |
||
|
|
6f47a78afd | ||
|
|
c8b5165618 |
||
|
|
39a6ff8a2b |
||
|
|
bf77cb57fd | ||
|
|
31e97fdd0e | ||
|
|
96ea3accdf |
||
|
|
a0983ac2da |
||
|
|
0be544676b | ||
|
|
dbe81f1e59 |
||
|
|
e37ae0c559 |
||
|
|
bb4740ba76 |
||
|
|
528d2b5fb7 |
||
|
|
d7d8a51332 |
||
|
|
f6f8cd7a29 |
||
|
|
69cc8b3c89 | ||
|
|
810a6b8125 | ||
|
|
72af0a4947 | ||
|
|
3c1db1d7d6 | ||
|
|
453eab3a12 |
||
|
|
e4e570381c |
||
|
|
817141a781 | ||
|
|
97a9f7c8ff |
||
|
|
c86ea9463d |
||
|
|
d1548572d4 |
||
|
|
5804af2082 | ||
|
|
85b7c01c2d | ||
|
|
22725968e8 |
||
|
|
5d6ea4f32e |
||
|
|
7554600dd4 |
||
|
|
77a98bfd14 | ||
|
|
883b6dbef2 |
||
|
|
8ecacc9978 |
||
|
|
430d48434c |
||
|
|
9053a0bb6a | ||
|
|
01753a814c | ||
|
|
e43c591890 |
||
|
|
6195e5d8f6 | ||
|
|
039c12266a |
||
|
|
1632ae27d0 |
||
|
|
8d72cb1416 |
||
|
|
8100f98d6b |
||
|
|
6828cd1075 |
||
|
|
84f460d9b8 | ||
|
|
5bb0b198e6 | ||
|
|
ed4b6e85ac | ||
|
|
1e8c0547ec |
||
|
|
cf49a3427e |
||
|
|
4c0479fe3d |
||
|
|
c395e74e64 |
||
|
|
4c62b0d50c |
||
|
|
5339f529aa | ||
|
|
7fd200db63 | ||
|
|
97973d47f2 |
||
|
|
bf4d55564c | ||
|
|
0858938eea |
||
|
|
d3d4dce54d |
||
|
|
fdd321311e | ||
|
|
e987917c64 |
||
|
|
60a8f61040 | ||
|
|
b6cde34a08 | ||
|
|
44b9597981 | ||
|
|
7267f89377 | ||
|
|
9164f65693 |
||
|
|
103c3395dd | ||
|
|
640d8df487 |
||
|
|
49f1a01dba |
||
|
|
67f7975ced |
||
|
|
5bff3abd52 | ||
|
|
3de3d05b8a | ||
|
|
916ebcb1be | ||
|
|
c0f430e509 | ||
|
|
df04d697b1 | ||
|
|
1fbdb170e4 | ||
|
|
cdb810ebc5 |
||
|
|
3873c4def1 |
||
|
|
00bca229f0 |
||
|
|
2c499c9f6d | ||
|
|
b3f48cf926 | ||
|
|
f07a6db7ab |
||
|
|
9414989fc1 |
||
|
|
a7e928ed4f |
||
|
|
8a7408097d |
||
|
|
0af3f10997 |
||
|
|
ed67dc5de5 |
||
|
|
74ccef02bf |
||
|
|
88b7f5bc2f |
||
|
|
52fd311016 | ||
|
|
70128e22b9 | ||
|
|
351a203acd | ||
|
|
120953773e |
||
|
|
a56ad36a42 | ||
|
|
74220d8a24 | ||
|
|
7e2edc2c9f | ||
|
|
0260bcbf3b |
||
|
|
44d6c1cf6d | ||
|
|
4b239a9c9c | ||
|
|
288aaac465 |
||
|
|
0dc76e773f |
||
|
|
6a6789a3c7 |
||
|
|
be69f88ef6 |
||
|
|
43fc531a6f | ||
|
|
e345bc8ac8 | ||
|
|
5ce67ba093 | ||
|
|
191f8429c3 |
||
|
|
e9e440a625 |
||
|
|
f2e5bd1b3c |
||
|
|
da5a876f93 |
||
|
|
c28fda6b28 |
||
|
|
35e5ff2fc7 |
||
|
|
2954afd77d |
||
|
|
ab2bb2881f |
||
|
|
1a9ad1a7e3 | ||
|
|
bd619220d0 |
||
|
|
b01e01bc19 | ||
|
|
a15e922e27 | ||
|
|
6857c5dd08 |
||
|
|
6d4fb93e6b | ||
|
|
38b8cdbb5e |
||
|
|
f2fb27f6a4 |
||
|
|
ddb1a280cb | ||
|
|
5146686814 | ||
|
|
1bf8c1578d |
||
|
|
50a9ef5b3a |
||
|
|
01857a50ee | ||
|
|
71def026c9 | ||
|
|
c86c4f0362 |
||
|
|
08f2ebd373 |
||
|
|
20c9df39b1 |
||
|
|
b2eb11b5ef |
||
|
|
b5b0ebe00d |
||
|
|
49046be361 | ||
|
|
27393bb804 | ||
|
|
218e06fbd4 |
||
|
|
167101c3aa | ||
|
|
d66d5dc144 |
||
|
|
653b46e2f4 |
||
|
|
5aa9d7e1dc |
||
|
|
fc7e8339b7 |
||
|
|
047c4d0a98 |
||
|
|
f3c10bdd42 |
||
|
|
5e6e27d73f |
||
|
|
4faf669348 |
||
|
|
f105a11cd4 |
||
|
|
c816bc9834 |
||
|
|
7cad5063f2 |
||
|
|
8d4475ff84 |
||
|
|
b8531e6070 |
||
|
|
d0ac101b0d |
||
|
|
dfc4cad712 | ||
|
|
8f59ca1bec | ||
|
|
941849eaa8 | ||
|
|
731b29c059 | ||
|
|
f475cc39ef | ||
|
|
e5a6417a82 | ||
|
|
0fb462c88e | ||
|
|
8dcdfbdffa | ||
|
|
8d6b3fa335 | ||
|
|
d1648823c3 |
||
|
|
6fdd6d4f8e |
||
|
|
884984f9b4 | ||
|
|
73755ce973 | ||
|
|
589d7a9811 |
||
|
|
4a8aae1f1a |
||
|
|
68adda5821 | ||
|
|
d4a068e2de | ||
|
|
a941fe97a6 | ||
|
|
58d9490c2a | ||
|
|
fe51c6d7e7 | ||
|
|
20415a2edb |
||
|
|
2506feb1ea | ||
|
|
5c7f34bd48 | ||
|
|
d5c9fb5536 |
||
|
|
bb066ecb02 | ||
|
|
6c6168e80a |
||
|
|
c63c0f2ec8 |
||
|
|
6fa48d3acf | ||
|
|
827310a645 | ||
|
|
dd3d8f8fb3 |
||
|
|
c3ed4ebc5e |
||
|
|
6dbc6eb842 |
||
|
|
8b417fe97a | ||
|
|
85efebc6be |
||
|
|
501730f2ca | ||
|
|
4551c10a5c |
||
|
|
9188943261 | ||
|
|
11eedc3ea1 | ||
|
|
e8c64b084e |
||
|
|
9f503b6de9 | ||
|
|
97ed29e1dd | ||
|
|
e719e4ff81 | ||
|
|
cbf82a1bc7 |
||
|
|
eab823ba66 | ||
|
|
68cc9a2e28 |
||
|
|
04046719c1 |
||
|
|
894d6f162d |
||
|
|
4b7a53c5e1 | ||
|
|
6b5f6e3e79 | ||
|
|
e0b15f18e1 |
||
|
|
276266e24f | ||
|
|
9107f9e351 |
||
|
|
6aa5ab160d |
||
|
|
8deb056ecf |
||
|
|
f909e096bc |
||
|
|
f6bf4a1c94 |
||
|
|
f9f35fa498 |
||
|
|
5f0fcf5f41 |
||
|
|
92bd4ed083 |
||
|
|
5bca4b7323 | ||
|
|
f816a3972f | ||
|
|
0d02f49b9b | ||
|
|
8e6be8a4d4 | ||
|
|
fee81e78e1 | ||
|
|
59174db7e5 |
||
|
|
eaf8c35f40 | ||
|
|
7a0a1b86f6 | ||
|
|
49f2721908 | ||
|
|
7fec928ba8 | ||
|
|
2bf4416aec | ||
|
|
c485ec8c42 |
||
|
|
b613be7761 |
||
|
|
46c58dcb8d |
||
|
|
4cb6ec9eae |
||
|
|
45586f7150 |
||
|
|
f04b27b921 | ||
|
|
e4bbefd0a5 | ||
|
|
1e8ca56c65 | ||
|
|
2a9525a860 | ||
|
|
8bbca05ad6 |
||
|
|
f7a828122c |
||
|
|
9c5147ae09 | ||
|
|
15dbbf4efc | ||
|
|
5ed8cc6320 |
||
|
|
fb17107406 |
||
|
|
00681e876f |
||
|
|
61438cbe5a |
||
|
|
20a6133d73 | ||
|
|
4e67e55a7d | ||
|
|
f282a973f8 |
||
|
|
9852eb072b | ||
|
|
02593c51e2 | ||
|
|
14ef881d17 |
||
|
|
6e93bd2c9b |
||
|
|
59ec0348b6 |
||
|
|
92eb45a6f5 |
||
|
|
845b4a240a | ||
|
|
3acb14dac9 | ||
|
|
0f6d27497d |
||
|
|
aadb506b8f |
||
|
|
88cbcc0694 |
||
|
|
c051b70537 | ||
|
|
1b97c1031d | ||
|
|
be8cf925d8 | ||
|
|
9ba6b78726 | ||
|
|
22b02dff31 |
||
|
|
615353c582 |
||
|
|
3070ae098a |
||
|
|
81e1d15ee9 |
||
|
|
ee7272305a |
||
|
|
bfc8959bb9 |
||
|
|
649d60c119 |
||
|
|
25517f3ad7 |
||
|
|
fd4492ab41 |
||
|
|
02b35ab367 |
||
|
|
e26bbeccbe |
||
|
|
5d9a123827 |
||
|
|
bc1a6319a0 |
||
|
|
7a6b560303 |
||
|
|
fe4f453a34 |
||
|
|
ecede860ef |
||
|
|
2dbb150463 |
||
|
|
a9994656c3 |
||
|
|
4fe7dc0808 | ||
|
|
d798a0fe9b |
||
|
|
f18a7b2fe6 |
||
|
|
964da1487a |
||
|
|
deac669532 |
||
|
|
221aad7104 |
||
|
|
b9ca43d529 |
||
|
|
9e28c0d541 |
||
|
|
c490091346 |
||
|
|
0610784632 | ||
|
|
c07342b67b | ||
|
|
827e37f3d4 | ||
|
|
7fcf683da3 | ||
|
|
bd2907c13f | ||
|
|
7ce1f1cf9e | ||
|
|
63828b95e0 | ||
|
|
7f82b440a2 |
||
|
|
f78333a544 | ||
|
|
fedb44435d | ||
|
|
24fa5b0e38 |
||
|
|
3efd888727 |
||
|
|
5bac385a75 |
||
|
|
9c46bc5713 | ||
|
|
a24ab74c4c |
||
|
|
b91248719a | ||
|
|
ca36da8280 | ||
|
|
3074251be9 |
||
|
|
3da870be8c | ||
|
|
eebfa4bfd9 |
||
|
|
b943b12cd1 |
||
|
|
f8c894d7e1 |
||
|
|
fa4331bcd9 |
||
|
|
e03694b49c |
||
|
|
da0ab54292 |
||
|
|
59ec07be87 |
||
|
|
7e8ceaeda4 |
||
|
|
a3a5ce9aa4 | ||
|
|
a121ada239 | ||
|
|
84e524c635 | ||
|
|
906e79f39b | ||
|
|
e957a52e43 |
||
|
|
c09f06fccc | ||
|
|
863e9b0b48 |
||
|
|
5bc3ef3ed8 |
||
|
|
cf3d1e928d |
||
|
|
dfba84d811 |
||
|
|
b9df9d6981 | ||
|
|
ab15fef282 | ||
|
|
b501d648e3 | ||
|
|
acb6179b30 | ||
|
|
f6c76ff9bd |
||
|
|
25c6cb4f6f |
||
|
|
599568a428 |
||
|
|
75dda78b9c |
||
|
|
4fe946d791 |
||
|
|
8cfc7aae09 |
||
|
|
32d98c327d |
||
|
|
8d8ab049cd |
||
|
|
0602a9495a |
||
|
|
ddfd942e66 |
||
|
|
70a5fb99d4 | ||
|
|
901b0eff7d | ||
|
|
f9ca608ad5 |
||
|
|
9c4ea85041 | ||
|
|
b10962f13f | ||
|
|
f8e53d5f72 |
||
|
|
37c61e2413 | ||
|
|
2bcb9bcca1 | ||
|
|
913e88185e | ||
|
|
f646fbbd4f | ||
|
|
dc6252d3f6 | ||
|
|
1ce2792fc4 |
||
|
|
feb5071786 |
||
|
|
477e2f9cd1 |
||
|
|
c733e72e7a | ||
|
|
1453e262d1 | ||
|
|
a2195c15e4 | ||
|
|
4e63ef9764 |
||
|
|
288e8148fd |
||
|
|
7975f4debc |
||
|
|
7330dc9553 |
||
|
|
95abf830cd |
||
|
|
7724762c14 | ||
|
|
6c7ddd0f47 |
||
|
|
a94d3f1b6d |
||
|
|
51684f7a2a | ||
|
|
c7ad6d9d3d |
||
|
|
ca8df65dfd |
||
|
|
bdb6064c76 |
||
|
|
69a4207ea4 |
||
|
|
c97352905d |
||
|
|
9ecb776760 |
||
|
|
8886459be9 |
||
|
|
53fe991407 |
||
|
|
26fa60f475 |
||
|
|
a3e448acf5 | ||
|
|
0d068f34a8 |
||
|
|
8403d277b4 |
||
|
|
ab982ecc3c | ||
|
|
2685f46669 |
||
|
|
3ccdee6f00 |
||
|
|
890555785d |
||
|
|
a09b21decd |
||
|
|
40f05b837d |
||
|
|
a145b8e27c |
||
|
|
742929280d |
||
|
|
4196e627f9 | ||
|
|
559281bfb9 |
||
|
|
b82ceb162b |
||
|
|
38e8c2eb41 | ||
|
|
0044be266e |
||
|
|
558a6fba0a |
||
|
|
3cba89dc9e | ||
|
|
416f45d1e3 | ||
|
|
6013a02fc2 | ||
|
|
c432ee431d | ||
|
|
421d2b7b70 | ||
|
|
13a53706f0 |
||
|
|
c6f6e8d8d2 |
||
|
|
b57830b859 | ||
|
|
7b27e7d024 |
||
|
|
837dad2535 |
||
|
|
3144882491 |
||
|
|
5669cf40db |
||
|
|
f92aeceb22 |
||
|
|
735393fb03 |
||
|
|
44353772da | ||
|
|
9905fdcbef |
||
|
|
e448dc711c | ||
|
|
23b59ece45 |
||
|
|
c8418a638d |
||
|
|
c0442e4c99 |
||
|
|
38bc8e6782 | ||
|
|
51ee504e90 |
||
|
|
e7bcec77df |
||
|
|
a3fd86f9a9 | ||
|
|
227c9263e5 | ||
|
|
5d2453347f |
||
|
|
0d074f1cbe | ||
|
|
d1e5a8f492 | ||
|
|
116a73c8d0 | ||
|
|
3e713e8be8 |
||
|
|
93131fb542 | ||
|
|
72e5e9f237 |
||
|
|
939e2a00f3 |
||
|
|
13e81e5a41 | ||
|
|
fa993c29d5 | ||
|
|
f7449c565f | ||
|
|
7e704b2d73 | ||
|
|
865e5bb41b |
||
|
|
4639a2528d |
||
|
|
e4701be708 |
||
|
|
6d931e8dcb |
||
|
|
2c570fa9ef |
||
|
|
b5d96d215f |
||
|
|
c53015c1af | ||
|
|
0b3741859f |
||
|
|
f2ddb633b1 |
||
|
|
1988bbd149 |
||
|
|
03a99ecd60 | ||
|
|
053e2ffbbf | ||
|
|
12b5cbc40f | ||
|
|
82fc55c1f1 |
||
|
|
a15216493a |
||
|
|
12bbaef4ab | ||
|
|
9c90e94ef8 | ||
|
|
f6051d3f6b |
||
|
|
339f65295d |
||
|
|
7883feca30 |
||
|
|
14847260b6 |
||
|
|
85cb039426 | ||
|
|
aee1ec2739 | ||
|
|
99ee31d795 |
||
|
|
e28efe5e38 | ||
|
|
572c93d06b |
||
|
|
c4f6701d65 | ||
|
|
a9198b1cf6 | ||
|
|
79a2c72c06 |
||
|
|
bbaea66d11 |
||
|
|
b213e988ca |
||
|
|
72aa243666 | ||
|
|
b7ccb5a294 | ||
|
|
60777b9b1f | ||
|
|
dbb51e0f46 |
||
|
|
c0749136fc | ||
|
|
003ae7131b |
||
|
|
cb4312f410 |
||
|
|
51d5a4eff4 | ||
|
|
c6e282bffd |
||
|
|
349edad826 | ||
|
|
ec70bd91a8 |
||
|
|
91e3ca88bb |
||
|
|
335bd803af |
||
|
|
6b024191c1 |
||
|
|
6d6b5cb2a9 | ||
|
|
1904e9b0ab |
||
|
|
e5fa6e32f9 |
||
|
|
1fdd1ff0c3 | ||
|
|
b4951b1f86 | ||
|
|
93265ef830 |
||
|
|
ede0f44ca2 |
||
|
|
4f737ce5bb |
||
|
|
e6241556be | ||
|
|
b765abcb65 |
||
|
|
ffa9685b41 |
||
|
|
33dbc80dbc | ||
|
|
c605f35335 |
||
|
|
dcdc1d150f |
||
|
|
5cf24b80e6 | ||
|
|
bc05ca1c63 |
||
|
|
8f2caca6d7 |
||
|
|
72fafc5b4f | ||
|
|
869e978a99 |
||
|
|
4f5807b8fe |
||
|
|
8d124dd2a2 |
||
|
|
ff9dd3833e |
||
|
|
a9e3b551e6 |
||
|
|
90c3c3b22e |
||
|
|
43aceccb41 |
||
|
|
8057c416fb | ||
|
|
25460156cb |
||
|
|
15ea2db31b |
||
|
|
2640c2a15e |
||
|
|
46931b0a7d |
||
|
|
f7429ab80b |
||
|
|
ede37b9cb6 |
||
|
|
9e49ee28e8 |
||
|
|
73962bb835 |
||
|
|
1d7a7a89a6 |
||
|
|
275d80b412 |
||
|
|
befafade31 |
||
|
|
c809d4beaf |
||
|
|
65c6774af0 |
||
|
|
449a59ae72 |
||
|
|
2eb13801ad |
||
|
|
991ec5315d |
||
|
|
0b83ba6a40 | ||
|
|
5722bd9845 |
||
|
|
6bfd6ed473 |
||
|
|
25f02eb211 | ||
|
|
d6d54c2c56 | ||
|
|
cfca2fa155 | ||
|
|
b2eebfa65e |
||
|
|
54af12b329 |
||
|
|
25d69438b8 |
||
|
|
710ba10772 | ||
|
|
9946b918d7 | ||
|
|
432023fc7e |
||
|
|
d3cbcfcc8b | ||
|
|
33de4840c6 |
||
|
|
fda03b8362 |
||
|
|
33d82287be |
||
|
|
7a691b8e6c |
||
|
|
aac4025c59 |
||
|
|
42199eda29 |
||
|
|
10d43f27ed | ||
|
|
1aaa9391db |
||
|
|
c49239b4f1 |
||
|
|
f8c66aa512 |
||
|
|
503c1538f5 |
||
|
|
45dc52886e |
||
|
|
8d0bdfc200 | ||
|
|
3eb017caf8 |
||
|
|
d1c7613535 |
||
|
|
edaa8119af |
||
|
|
59a1a9cd36 | ||
|
|
fd6f740d36 | ||
|
|
cdd9418bb0 |
||
|
|
c4b5671aef | ||
|
|
98c3108bde |
||
|
|
40aadf7c95 |
||
|
|
6cbc2eeef3 |
||
|
|
eac092e661 | ||
|
|
31c33e43d5 | ||
|
|
e77de3315a | ||
|
|
b3702b0550 |
||
|
|
b8bd6fccbb |
||
|
|
fc39a67e10 | ||
|
|
011b086760 |
||
|
|
41d872bcaf | ||
|
|
d2388dc623 |
||
|
|
3971ecff63 |
||
|
|
d9cb640c2a |
||
|
|
52b3ef18f7 |
||
|
|
3510094665 | ||
|
|
4307bb0563 |
||
|
|
628ef7aa66 |
||
|
|
ede032a7f4 |
||
|
|
37cc576af1 | ||
|
|
dfe4b2f6d6 | ||
|
|
313a321918 |
||
|
|
0854bd359b |
||
|
|
6675ced196 | ||
|
|
a122ed5b7f |
||
|
|
068b842561 |
||
|
|
f38fa9b98e | ||
|
|
50d8bf5191 |
||
|
|
e98291dd3f | ||
|
|
9ca08ae434 |
||
|
|
49135f4d77 |
||
|
|
168bd1b3ab | ||
|
|
dafca48061 |
||
|
|
5c9e985999 |
||
|
|
0a8cccf5d7 |
||
|
|
04fee16fec |
||
|
|
dd40011967 |
||
|
|
7346866f03 |
||
|
|
f3b64958cb | ||
|
|
069cb6192f | ||
|
|
322987a204 |
||
|
|
a2b03a5de6 |
||
|
|
470f2dd814 |
||
|
|
b47538860e |
||
|
|
dbd07d55e3 |
||
|
|
3cd99816b8 |
||
|
|
48ebde6008 |
||
|
|
bd9e219d92 | ||
|
|
835236f8ac |
||
|
|
b02b47c822 |
||
|
|
dab4fc44ea |
||
|
|
349136f172 | ||
|
|
c17d7bddae |
||
|
|
9f99b165c9 |
||
|
|
84587ea0a1 | ||
|
|
d16b18de78 | ||
|
|
a392b9f35f |
||
|
|
1fc2051c1d |
||
|
|
f725e944dd |
||
|
|
3eb429dde3 |
||
|
|
238e894377 |
||
|
|
3c8fcadfe8 |
||
|
|
d2106f1782 |
||
|
|
80c11f2bde | ||
|
|
ff6d6e3e82 |
||
|
|
12dd11bd10 |
||
|
|
f81f083b24 | ||
|
|
7d49730bad |
||
|
|
7f165a7593 |
||
|
|
d1c2f0df72 |
||
|
|
7dec39a499 | ||
|
|
7c2422ee19 |
||
|
|
32a95c4d75 |
||
|
|
c14c3b73d2 |
||
|
|
4b34a093c9 |
||
|
|
57c24704af |
||
|
|
0cbe0cb24e |
||
|
|
18d135ca92 |
||
|
|
6377e0de66 | ||
|
|
7fa30980aa |
||
|
|
6c50fe72b9 |
||
|
|
685951966b |
||
|
|
9691df67e3 |
||
|
|
cbe9a4e221 |
||
|
|
df858a5aaf |
||
|
|
5490235f4d |
||
|
|
47f95c234d |
||
|
|
1063d7b1d6 |
||
|
|
a03f894888 |
||
|
|
d2b0385431 |
||
|
|
9ea3966140 |
||
|
|
522bba694b |
||
|
|
3307ced4d8 |
||
|
|
3fde87efbc |
||
|
|
148628c49f |
||
|
|
5edc8914b9 | ||
|
|
18a9a82c26 |
||
|
|
aad9033d06 |
||
|
|
06becc798f |
||
|
|
8004e051aa |
||
|
|
8ac0586464 |
||
|
|
9517360531 |
||
|
|
371c5bcac2 |
||
|
|
6d053d65e7 |
||
|
|
8652ca6968 |
||
|
|
f975426c61 |
||
|
|
84db8d8866 |
||
|
|
0ffd4d60d9 |
||
|
|
da7481c23c |
||
|
|
3f9a8d6066 |
||
|
|
98cab94f9b |
||
|
|
56b5a13abc |
||
|
|
32b4349130 | ||
|
|
31f1c2bf2b |
||
|
|
96adc56cd7 |
||
|
|
dc8c10f461 | ||
|
|
5084037cdc | ||
|
|
09b2cd7527 |
||
|
|
4a2101e3fc | ||
|
|
70f3d7d6cc |
||
|
|
2ca5b3d407 |
||
|
|
ce5d18c4fa | ||
|
|
602c50989f |
||
|
|
cbca3887ae | ||
|
|
a5dea8caa0 |
||
|
|
461d61d6f5 | ||
|
|
95ff256e68 |
||
|
|
2b7657a9e7 | ||
|
|
c06fb9a275 | ||
|
|
ada870e959 |
||
|
|
36be7565a7 |
||
|
|
daa45c13fd | ||
|
|
27b6849678 |
||
|
|
b5ad495c6d | ||
|
|
7dd4603d3c |
||
|
|
41014cdf05 |
||
|
|
af4f322bf9 |
||
|
|
5d08578a74 | ||
|
|
bf602db991 |
||
|
|
cd606b8c28 |
||
|
|
d9cc335351 |
||
|
|
ffe8f2899b |
||
|
|
80f6171bd2 |
||
|
|
721170283c |
||
|
|
0a333ad84d |
||
|
|
861d6b6642 |
||
|
|
d68c2caa26 |
||
|
|
15acab80d4 | ||
|
|
ec9abe49b2 |
||
|
|
4d2b550cf8 | ||
|
|
b9f1b9fd4d |
||
|
|
8b52825d51 |
||
|
|
d4ad60c11d | ||
|
|
fdd8ef3e6a |
||
|
|
b67e7ee3ef |
||
|
|
ccd37aba8f |
||
|
|
d68a2fa872 | ||
|
|
7dcab09516 |
||
|
|
96d3e58734 |
||
|
|
234c4a8bac |
||
|
|
ad1114ccfe | ||
|
|
ccd0a6559b | ||
|
|
e1ae339190 | ||
|
|
9a8cde189d |
||
|
|
deaf803467 |
||
|
|
826482bb5b |
||
|
|
3e708f9c0b |
||
|
|
d0f5d5025e |
||
|
|
d89112173d |
||
|
|
6547d18e7f |
||
|
|
07a01b0547 |
||
|
|
df2787d3e9 |
||
|
|
b90c224a99 |
||
|
|
6077a1ae67 |
||
|
|
aa66f284e7 |
||
|
|
aeb8f66f30 |
||
|
|
33d865501d |
||
|
|
6fb09048f6 | ||
|
|
e652ff6c7d | ||
|
|
9b75b5727a |
||
|
|
4be5ed693c |
||
|
|
ce2adb4b8d |
||
|
|
9436ba9301 |
||
|
|
493a5407a2 | ||
|
|
c0869b05a3 |
||
|
|
4e6a29db35 |
||
|
|
40af84b653 |
||
|
|
ca22da28a7 |
||
|
|
206ded55ff |
||
|
|
c2243ed7b4 |
||
|
|
a8fb892873 |
||
|
|
de98c2b13a |
||
|
|
6311176f00 | ||
|
|
fb5eb0664a | ||
|
|
c21aee3d4a |
||
|
|
c038935b96 |
||
|
|
844ca1fbe6 |
||
|
|
22237658cb |
||
|
|
f32cd57bcb |
||
|
|
ebc2d7ecc5 |
||
|
|
c562c4fd58 | ||
|
|
b391b87e29 | ||
|
|
52d5cb2311 |
||
|
|
0482747781 |
||
|
|
7bbd3d61d5 |
||
|
|
fb0f68f8a5 | ||
|
|
fb94ad4205 | ||
|
|
f9be519c2f |
||
|
|
b662764caa |
||
|
|
65423eb8fd |
||
|
|
5e48e04623 | ||
|
|
3df1e9d731 |
||
|
|
6ec1a2ee14 |
||
|
|
20af04bca0 | ||
|
|
e462ed6270 | ||
|
|
1bdbb75d4c | ||
|
|
df703dc73a | ||
|
|
15f7f2e371 |
||
|
|
db53f13865 |
||
|
|
ee6ff6a5e5 |
||
|
|
481b6fd336 | ||
|
|
2afd4d7ec4 | ||
|
|
2d0ddfb2e8 |
||
|
|
c5bda23548 |
||
|
|
b5d39b96b9 |
||
|
|
b80710ed82 |
||
|
|
4b3f22701e |
||
|
|
915f1da190 |
||
|
|
04f4bb1269 | ||
|
|
3e1c96552d | ||
|
|
683bc4ef60 |
||
|
|
f07c1bef69 |
||
|
|
0643d3b4a3 |
||
|
|
e3823803aa | ||
|
|
5fc70397a9 | ||
|
|
fe1c7612f5 | ||
|
|
552f3da67e | ||
|
|
41e121d270 | ||
|
|
2f47307437 |
||
|
|
80040dc153 | ||
|
|
f237ed1684 |
||
|
|
f277864454 |
||
|
|
8a568a3ef4 |
||
|
|
6aff2657e9 | ||
|
|
c7f6b8409f | ||
|
|
a469f22699 |
||
|
|
52b234bdfc |
||
|
|
11568d7405 | ||
|
|
9d87ce5d7a |
||
|
|
8889201081 | ||
|
|
1bca238e9b |
||
|
|
052248445c |
||
|
|
6dd568b27f |
||
|
|
89f1326ce7 |
||
|
|
7a15434d37 |
||
|
|
b2835855c0 |
||
|
|
6bb0251d57 |
||
|
|
f368dcdc69 |
||
|
|
ed8037d1f0 | ||
|
|
d770028da6 | ||
|
|
a7bd40a5b1 | ||
|
|
104e270882 |
||
|
|
f89054784f |
||
|
|
be6975cb83 |
||
|
|
bdbdb69f02 |
||
|
|
fd72179b3d |
||
|
|
2f4205d095 | ||
|
|
4ec10b922a |
||
|
|
e60bbd9548 |
||
|
|
c0d348a678 |
||
|
|
36e0ce46b4 |
||
|
|
68753ae3e8 | ||
|
|
53968bf453 |
||
|
|
a97a6de60f | ||
|
|
7de7292560 |
||
|
|
c9e340e153 |
||
|
|
c3b076ca74 |
||
|
|
561de89568 |
||
|
|
12e0087d47 | ||
|
|
624b3ebc18 |
||
|
|
03d6bf06ee |
||
|
|
c9da29f2fc |
||
|
|
7daf566025 |
||
|
|
f98deaba70 | ||
|
|
74521225ba |
||
|
|
b53f1719eb |
||
|
|
e5862a9303 |
||
|
|
39e99fcd29 |
||
|
|
5855099d5b | ||
|
|
4e2eed2023 |
||
|
|
3d31fa4686 | ||
|
|
4e45820235 |
||
|
|
ba002cca64 |
||
|
|
6fc812d272 |
||
|
|
0dbdd4732b |
||
|
|
52f8934d9b |
||
|
|
2fcb89a779 | ||
|
|
b25884c5bd |
||
|
|
2a8feaa454 |
||
|
|
287c3a8223 |
||
|
|
823ed0153f |
||
|
|
d9d4672d75 | ||
|
|
ddc7ace78d |
||
|
|
0e52b133af | ||
|
|
d9efaef369 |
||
|
|
7f16ad649d |
||
|
|
6862c2e6b0 | ||
|
|
e3515d2ea1 | ||
|
|
645b3c13b3 | ||
|
|
c5a530fe31 | ||
|
|
5ffeb04343 |
||
|
|
eaeb3375db |
||
|
|
9a3c9a7ab0 |
||
|
|
f092c2393c | ||
|
|
d99030aff5 |
||
|
|
9eb79d675c |
||
|
|
553c24c909 |
||
|
|
727021be68 | ||
|
|
8585dbc6c4 |
||
|
|
14f08ea271 |
||
|
|
2f84b01427 | ||
|
|
15a52ccec3 | ||
|
|
2fefe0e4f3 | ||
|
|
5c6c34de83 | ||
|
|
7fb92fee64 |
||
|
|
aca23257ed |
||
|
|
643a8222a6 |
||
|
|
c053011611 | ||
|
|
28b084af69 | ||
|
|
ca389c914f |
||
|
|
a10f147c36 |
||
|
|
f73132e226 |
||
|
|
3f06acfa6e |
||
|
|
c268d91b4e | ||
|
|
827f1dab96 |
||
|
|
f40edc7dc2 |
||
|
|
489bb8e395 |
||
|
|
01f524b7c5 | ||
|
|
adab03f730 | ||
|
|
e86a155ec2 | ||
|
|
c30f4aaaeb | ||
|
|
dbc6b099fe | ||
|
|
90861effb4 | ||
|
|
8d88779918 | ||
|
|
e85e00ebed | ||
|
|
711b5e1d91 | ||
|
|
55d1625ab2 | ||
|
|
2a6b255aee | ||
|
|
303dddb12e | ||
|
|
0e55dafd8a | ||
|
|
055bd5dca5 |
||
|
|
1e4c39a87c |
||
|
|
57289396da |
||
|
|
0402554563 | ||
|
|
8791a17fc4 | ||
|
|
da5a5c7175 |
||
|
|
0f75c1a138 | ||
|
|
56a9ad184d |
||
|
|
74dcc52657 |
||
|
|
f2d9ef62cf |
||
|
|
3af9a45087 |
||
|
|
c5de1b3dbe |
||
|
|
36ab3ffc8d |
||
|
|
257b6d320b |
||
|
|
5ffa91c964 |
||
|
|
5c6b51a05b |
||
|
|
7d8b9b7e7c |
||
|
|
35f615187f |
||
|
|
ef1c59072c |
||
|
|
29557a1f3b |
||
|
|
a813d1a78f |
||
|
|
1c18ed9775 |
||
|
|
71cdbfff53 | ||
|
|
1dc92d8934 |
||
|
|
f6be0e0ee0 |
||
|
|
f0a9fdb658 |
||
|
|
67957f15fc |
||
|
|
6af282e6e8 |
||
|
|
e5cded0ad6 | ||
|
|
1ead5fd53a |
||
|
|
e1aac141e0 |
||
|
|
bd3f60a480 |
||
|
|
56fbc1197c |
||
|
|
aa9e4aa8b4 |
||
|
|
95c9d37d2a |
||
|
|
290cbc1617 |
||
|
|
4888ffea3b |
||
|
|
1906bae5f5 |
||
|
|
1f950878d7 |
||
|
|
91691bd747 |
||
|
|
21badddc38 |
||
|
|
1e7b398903 |
||
|
|
c9039115bb |
||
|
|
59fe513142 | ||
|
|
9ff398dac3 | ||
|
|
5fd493bdc2 |
||
|
|
4034861d2d |
||
|
|
af060bd2d0 | ||
|
|
760c8046b2 | ||
|
|
48b6a136d1 |
||
|
|
329a27d5c6 |
||
|
|
9ff974b702 | ||
|
|
feb7d9d4bb | ||
|
|
5c4e1e55a6 |
||
|
|
f8af23c64a |
||
|
|
10dab7afac |
||
|
|
6b40ee1b77 | ||
|
|
8062609c86 |
||
|
|
fc3e08c50f |
||
|
|
9161a09a6a |
||
|
|
3677cb4c8b | ||
|
|
41f50f9218 | ||
|
|
56fc211628 |
||
|
|
f53348decb | ||
|
|
cc5e6a9a90 |
||
|
|
843d34304e |
||
|
|
15e9ea5001 |
||
|
|
27d35e6c80 | ||
|
|
e83ba807f4 |
||
|
|
a7666a5d56 |
||
|
|
14e2174d44 | ||
|
|
81e3e88391 |
||
|
|
a60d7e31d9 |
||
|
|
65b1aa973f |
||
|
|
aaa8200028 | ||
|
|
8a66c51b64 | ||
|
|
38c31a74a2 | ||
|
|
40722601a2 | ||
|
|
7e081b83ba | ||
|
|
e6979fb5fd | ||
|
|
61b955ca78 | ||
|
|
2f025586ce | ||
|
|
2d5d7b4df9 | ||
|
|
3d31e1e5d3 | ||
|
|
3447817ad7 | ||
|
|
463606756d | ||
|
|
fee445245f | ||
|
|
0569f31e9c | ||
|
|
4317d4014e | ||
|
|
ff922d4f92 | ||
|
|
862d75b8ab | ||
|
|
72e5844b8b | ||
|
|
449976b0f9 | ||
|
|
57613c10ca | ||
|
|
554c89f9a5 | ||
|
|
ded79a184d | ||
|
|
f542e69b99 | ||
|
|
4535636855 | ||
|
|
3f2a017583 | ||
|
|
8d0d31d7b6 | ||
|
|
9541a0da49 | ||
|
|
5615544090 | ||
|
|
f1750d1cb7 | ||
|
|
a263c3eab9 | ||
|
|
4d544143b4 | ||
|
|
b59954eee0 | ||
|
|
d675297405 | ||
|
|
5f52e31e9d | ||
|
|
e3ee3f7788 | ||
|
|
d626c146c4 | ||
|
|
50504ed09b | ||
|
|
afc594a655 | ||
|
|
b2985a2c2a | ||
|
|
280eea970a | ||
|
|
c2f1edcbde | ||
|
|
9b06560899 | ||
|
|
3661d8cff9 | ||
|
|
d1d871f351 | ||
|
|
11b72223a7 | ||
|
|
e2e594d4f5 | ||
|
|
638d50c5e7 |
||
|
|
6dde4780b8 | ||
|
|
9bc3144511 |
||
|
|
5c45321cca | ||
|
|
9cf85328ed | ||
|
|
d79ede5087 |
||
|
|
59ad0ba863 | ||
|
|
5243a6e42f | ||
|
|
69ef6831b9 |
||
|
|
9a859474de |
||
|
|
b6963d98a4 | ||
|
|
01bd59e3ca | ||
|
|
75bc45f354 |
||
|
|
09878a5568 |
||
|
|
672617c8ad |
||
|
|
c2681a9f6e | ||
|
|
44acc5cb00 | ||
|
|
586a486418 | ||
|
|
530d71764f | ||
|
|
4bc8b9ed0c | ||
|
|
9a8c1b2fd0 | ||
|
|
0d6ff2535e | ||
|
|
d59becbf53 | ||
|
|
9a348e3eeb | ||
|
|
7f285e847f |
||
|
|
21cdf11350 |
||
|
|
960a6d42e1 | ||
|
|
3244ec91e8 | ||
|
|
3890aaad6b | ||
|
|
f18d019383 | ||
|
|
271324fa6f | ||
|
|
54cffb4712 |
||
|
|
b11a96d828 | ||
|
|
99c8e1c04a |
||
|
|
fc0af518c6 |
||
|
|
24f9467084 |
||
|
|
50d49a2ec2 | ||
|
|
d5d3cb0851 | ||
|
|
737c75638e | ||
|
|
38584e2f8e | ||
|
|
08ee28e99b | ||
|
|
a93adcbdfa |
||
|
|
0cc9c4166b | ||
|
|
d87e174644 | ||
|
|
0d21843283 | ||
|
|
c3ccfb092a | ||
|
|
82b7b3549d | ||
|
|
947f78eb9c | ||
|
|
7b7e167594 | ||
|
|
bc6868b123 | ||
|
|
bdf93481e6 | ||
|
|
1c4d9351d5 | ||
|
|
fd21c84a76 | ||
|
|
1e10cc66a6 |
||
|
|
da31317156 | ||
|
|
0b16026fe2 | ||
|
|
ad92a6d9b9 | ||
|
|
408fbbfbc1 | ||
|
|
28b662ddae |
||
|
|
45312b0c76 |
||
|
|
120e1df75f |
||
|
|
04d9b52113 |
||
|
|
0bf47ff06a | ||
|
|
5728424c75 |
||
|
|
3fc0f4e686 | ||
|
|
7cfd8d90c1 | ||
|
|
2f15ab3999 | ||
|
|
833bdfa2aa | ||
|
|
d7e6db92b5 | ||
|
|
0f1919a4fd | ||
|
|
46e8ec9ca9 |
||
|
|
80ef01c180 | ||
|
|
08edc434ca | ||
|
|
09cd9ce33a | ||
|
|
7c9eedb820 | ||
|
|
f1c40aa8de | ||
|
|
90ae5dbefc | ||
|
|
2c0fc67ab9 |
||
|
|
77714accca | ||
|
|
5e4a4d1d0b |
||
|
|
8268eb03c6 | ||
|
|
75b927b4a3 |
||
|
|
4dc3769b18 |
||
|
|
713d2c14e5 |
||
|
|
f85686bcb2 | ||
|
|
b456ab997c | ||
|
|
cddeb51109 | ||
|
|
203907f706 | ||
|
|
ede56398f6 | ||
|
|
3eaf12cc36 |
||
|
|
e165d51e60 | ||
|
|
649e9c3192 | ||
|
|
01eda0aac3 | ||
|
|
75555d90ee | ||
|
|
416ebf97ff | ||
|
|
1c065ad1b6 | ||
|
|
8f275c6a55 | ||
|
|
637949ea55 | ||
|
|
d97b844e29 | ||
|
|
a181d79544 | ||
|
|
1dae995631 | ||
|
|
48cc810455 | ||
|
|
6041e492ee | ||
|
|
21c9919fa1 | ||
|
|
6cfe60e4d9 | ||
|
|
935c5b309a | ||
|
|
532f55cb86 | ||
|
|
cffa957e34 | ||
|
|
05fc00d9be | ||
|
|
0c0df1efc9 | ||
|
|
c7433eca95 | ||
|
|
26eba56ab8 | ||
|
|
b844b0a782 | ||
|
|
55fe080eaa | ||
|
|
4e776f7a5f | ||
|
|
3ac15f97f1 | ||
|
|
2ea57a4133 | ||
|
|
aa377ee59b |
||
|
|
82c4898715 |
||
|
|
64cc4927b3 |
||
|
|
b8399471b3 |
||
|
|
2e2e201784 | ||
|
|
59d2f93f61 |
||
|
|
90cb79ac91 | ||
|
|
163cfaba3c | ||
|
|
4f38bc9077 | ||
|
|
cb1b6db14e |
||
|
|
948f304bb9 | ||
|
|
ec6307d55f | ||
|
|
aace1aab05 | ||
|
|
59a5f6237c | ||
|
|
3bf6ba5802 | ||
|
|
90d6916b10 | ||
|
|
474fbc0a9c | ||
|
|
c4ebd141c5 | ||
|
|
9e008a76b7 | ||
|
|
17dd18a605 | ||
|
|
79eb83d82f | ||
|
|
e4c6d78762 | ||
|
|
676cc01203 | ||
|
|
171449c81e | ||
|
|
f2fb05e7c9 | ||
|
|
4431616822 | ||
|
|
f26c2dad0f |
||
|
|
92f8c2bfef |
||
|
|
6708261368 | ||
|
|
376e7a60fb | ||
|
|
c63dfc66b3 | ||
|
|
6d08b8d087 |
||
|
|
0e40d7f82f | ||
|
|
32da456c84 | ||
|
|
14cac93e10 | ||
|
|
712bdafd9e | ||
|
|
706943af26 | ||
|
|
15b922595f | ||
|
|
103b315ccf | ||
|
|
519b5de4ef | ||
|
|
b2a3b3c0f3 | ||
|
|
bc9faa733a | ||
|
|
8de7f44b98 | ||
|
|
17127e9fc2 | ||
|
|
41b9ffb5e7 | ||
|
|
17af195994 | ||
|
|
c3f141f82e | ||
|
|
a921d7bf04 |
||
|
|
b33ea0f567 |
||
|
|
2d61c018b0 | ||
|
|
2a510c38a2 | ||
|
|
bb432497be |
||
|
|
a44eea61a1 | ||
|
|
67da82c151 | ||
|
|
9d7d5a110c | ||
|
|
f2bc5adf7b | ||
|
|
d82f4007ec |
||
|
|
3b049f174c | ||
|
|
4d8fae1ab8 | ||
|
|
c81a74a20c | ||
|
|
86289f0a6e | ||
|
|
9a981a526e | ||
|
|
8304f5da81 | ||
|
|
87d6918358 | ||
|
|
65e4e386da | ||
|
|
e2a122c3ca | ||
|
|
3d2d8495f5 | ||
|
|
07de177b0e | ||
|
|
845db70969 | ||
|
|
f885ce456e | ||
|
|
d80efdfe23 | ||
|
|
6b1be79026 | ||
|
|
099fb058c6 | ||
|
|
477736a231 | ||
|
|
32e1a36980 | ||
|
|
435e14669b | ||
|
|
803cff92c8 | ||
|
|
00e54e49ac | ||
|
|
12ba10f688 | ||
|
|
cfa6db10c7 | ||
|
|
d7d185d06a | ||
|
|
c845d5723d | ||
|
|
704dae9dec | ||
|
|
7ee808169d | ||
|
|
43c8f6fd96 | ||
|
|
f9757786be | ||
|
|
d543123095 | ||
|
|
bb1e3ee917 | ||
|
|
94d40256d9 | ||
|
|
77e9cb65d5 | ||
|
|
e2db8b23a4 |
||
|
|
838eca8fe5 |
||
|
|
629592d641 |
||
|
|
72a534f42b |
||
|
|
08fbcd4958 | ||
|
|
f18ba07be6 | ||
|
|
111453aeaf | ||
|
|
9b19024ba5 | ||
|
|
9e1296d303 | ||
|
|
f65fb6573b | ||
|
|
4a6b79d147 | ||
|
|
220af0e180 | ||
|
|
1c1faf2dd9 | ||
|
|
ad8ec4b1e6 |
||
|
|
4381395ca6 | ||
|
|
05aa5da6e0 | ||
|
|
918b7382b8 | ||
|
|
271948b0fb | ||
|
|
b79a6cce0c | ||
|
|
bed6053cec | ||
|
|
a53169627e | ||
|
|
1e54018f15 |
||
|
|
0d57df81af | ||
|
|
d814abd1cf | ||
|
|
5821247b3d | ||
|
|
2920f5f24a | ||
|
|
05f8bfe906 | ||
|
|
53ebee195c | ||
|
|
d8f2d7fc10 | ||
|
|
19710b90c0 | ||
|
|
c8a78d447a | ||
|
|
0929358b9a | ||
|
|
99400421ef | ||
|
|
099d9219fd | ||
|
|
19f3cbd10e | ||
|
|
c55c338c72 | ||
|
|
495d8c3e04 | ||
|
|
f7ecd0a053 | ||
|
|
234fd31411 |
||
|
|
252dca3d3f |
||
|
|
330bb9a84e | ||
|
|
9c5071ec98 |
||
|
|
45eef67163 | ||
|
|
327c0fdb6a | ||
|
|
6ba2b8a3d1 | ||
|
|
e00b28d201 | ||
|
|
f56c748015 | ||
|
|
d4e02d96fa | ||
|
|
4a3026e11b | ||
|
|
1e2d35f206 | ||
|
|
5785af6738 | ||
|
|
60e69a83fc | ||
|
|
06741a1e1c | ||
|
|
70ea6e73a0 | ||
|
|
f8663ed28b | ||
|
|
0e332ec19d |
||
|
|
fa021da7cf |
||
|
|
1c8aa7a88f |
||
|
|
e292ef2bed | ||
|
|
ed68ff4a34 |
||
|
|
cdbefd3905 |
||
|
|
684f1a641d |
||
|
|
aa49856446 |
||
|
|
f221121998 |
||
|
|
90842267e8 |
||
|
|
9811f2be02 | ||
|
|
c24e48bdb5 | ||
|
|
1bf2be1b51 | ||
|
|
45d974a0c3 | ||
|
|
91ecd99c84 | ||
|
|
51bd46b871 |
||
|
|
fcd9782eb7 | ||
|
|
3b79a3df90 | ||
|
|
b0b2518e10 | ||
|
|
9917b9259f | ||
|
|
64ea29f46d | ||
|
|
36f2f644ea | ||
|
|
0ac3ba28e1 | ||
|
|
a99ea14dc0 | ||
|
|
e5ee53fc1f |
||
|
|
5d77bcb7c8 |
||
|
|
ac3ce74343 |
||
|
|
98bfe349d9 |
||
|
|
1e9910f899 | ||
|
|
0c0d45ff4f | ||
|
|
f2f350c0c5 | ||
|
|
a7f336507a | ||
|
|
1314ecd77d | ||
|
|
ae297e4269 | ||
|
|
43cc2792c9 | ||
|
|
14eb0c0d7f | ||
|
|
533fe25db4 |
||
|
|
3e39bdb329 | ||
|
|
3c117a6b88 | ||
|
|
e8b83b85c2 | ||
|
|
697ae217b9 | ||
|
|
10d38e107c | ||
|
|
8343f3cdcf |
||
|
|
8dd6b0f25a | ||
|
|
047be5d638 | ||
|
|
21aba63e09 | ||
|
|
e6200f1461 | ||
|
|
bc2a37913f | ||
|
|
aef1bb3acc | ||
|
|
507cc61d5c |
||
|
|
5e7a7a3870 |
||
|
|
f93634df39 | ||
|
|
270fd8d4f9 | ||
|
|
5299d976c0 | ||
|
|
6ce46a63bf | ||
|
|
e4ee3fbb3c | ||
|
|
28e32dc558 | ||
|
|
bd530552c2 | ||
|
|
43a8604b32 |
||
|
|
853e646670 |
||
|
|
b508783101 |
||
|
|
0cdc2a0a04 |
||
|
|
3190fd00bf | ||
|
|
499dfdfe92 |
||
|
|
d87662482b | ||
|
|
16cde4fcb2 | ||
|
|
dff94cc6fd | ||
|
|
f688e8df6c | ||
|
|
66ea26f4bd | ||
|
|
e5ce2f2688 | ||
|
|
5bb500f384 | ||
|
|
6f32e7ae75 | ||
|
|
d39e8ba9f8 | ||
|
|
e394224868 | ||
|
|
536498c53c | ||
|
|
d63de95b4e | ||
|
|
3822c11cc2 | ||
|
|
6b00cdeba9 | ||
|
|
40c129dbb7 | ||
|
|
053a0d0723 | ||
|
|
fa2bd86d72 | ||
|
|
25ac4d9e69 | ||
|
|
ff85b616f0 | ||
|
|
b529a99cf6 |
||
|
|
80e3302947 | ||
|
|
c9e70ab94c | ||
|
|
5a6591112b | ||
|
|
2df29db6e5 | ||
|
|
df6c6a3ab7 | ||
|
|
1749279c0c |
||
|
|
99db108d9a | ||
|
|
b280eeaf86 |
||
|
|
1629983a5d |
||
|
|
45276e3958 | ||
|
|
497676c39d | ||
|
|
2b23f630f1 | ||
|
|
c14f7da1b2 | ||
|
|
30e167c42c | ||
|
|
f19581b41c | ||
|
|
8e4582f3f5 |
||
|
|
769fdab252 |
||
|
|
db9e34da8d |
||
|
|
613e2ffda8 | ||
|
|
b02db5f1c6 | ||
|
|
df698ab3ea | ||
|
|
81ac90f2c1 | ||
|
|
1bcf59f704 |
||
|
|
5e9d203f3e |
||
|
|
5ccc184509 | ||
|
|
8115dea4bf | ||
|
|
9f9b300d26 |
||
|
|
365bed34a3 |
||
|
|
8aa89d7da2 | ||
|
|
157289258a |
||
|
|
e34da8a9f3 | ||
|
|
5ffecac70d | ||
|
|
dddfc78279 | ||
|
|
60bf17fe91 |
||
|
|
1f6ac656aa | ||
|
|
30a1bb7b9b | ||
|
|
45a2d074fb | ||
|
|
334768d5d5 | ||
|
|
2a7dadd6ee | ||
|
|
8a7b0c1440 |
||
|
|
8259b0924b | ||
|
|
8dcb4700e3 | ||
|
|
5b60bb0b0c |
||
|
|
60b531a174 |
||
|
|
2a81af6949 | ||
|
|
dc33b3ea95 | ||
|
|
b9f17b77ab |
||
|
|
52d85559aa | ||
|
|
30cf980506 | ||
|
|
e73f82e46c | ||
|
|
2102c79835 | ||
|
|
f35fc4acf3 | ||
|
|
30f40ee448 | ||
|
|
ed9bfcf2fa | ||
|
|
74ca130d51 | ||
|
|
f642a3c776 | ||
|
|
7d981d60d8 | ||
|
|
48d367e379 | ||
|
|
d06c279f02 | ||
|
|
7af573fd96 | ||
|
|
900d41bf47 |
||
|
|
1c8ea0b75c |
||
|
|
a13c08a45b |
||
|
|
d839a9e64a | ||
|
|
e0d6e11d4c | ||
|
|
4e7eed7958 | ||
|
|
9a5f577ae8 | ||
|
|
4a56870c08 | ||
|
|
e01e21cdbe | ||
|
|
f88d1e3bef | ||
|
|
03d6f284fb | ||
|
|
c6dd350a0b | ||
|
|
f0e6e397a4 | ||
|
|
70d09841d9 | ||
|
|
488dd7aeb4 | ||
|
|
ff757d4386 | ||
|
|
06d0189237 | ||
|
|
f35a2809a7 | ||
|
|
5fb34ce93f |
||
|
|
e5bace1794 |
||
|
|
ce0e460368 | ||
|
|
8727957710 | ||
|
|
2fdd4ab16e | ||
|
|
58cf481d86 | ||
|
|
511b173b6f |
||
|
|
9735f2b7a1 | ||
|
|
b7a1700908 | ||
|
|
a010973766 |
||
|
|
1ee67abd72 | ||
|
|
dedb3d0381 | ||
|
|
bcf11eca55 | ||
|
|
265673674d | ||
|
|
e2c5468d27 | ||
|
|
4837699d5c | ||
|
|
7869f09949 | ||
|
|
244d0f84f8 | ||
|
|
46b7296b87 | ||
|
|
1110097664 |
||
|
|
d2a3287800 |
||
|
|
48bae15939 | ||
|
|
43f1b6b627 | ||
|
|
0b68b07e3b | ||
|
|
6823fc2c29 | ||
|
|
a3d81adbd2 | ||
|
|
2e3101547e | ||
|
|
9937fcdbf9 |
||
|
|
50dc3cd01a | ||
|
|
20e4368865 | ||
|
|
a8643ea5af | ||
|
|
fb703004b1 | ||
|
|
df883c509e | ||
|
|
35c9e88c86 | ||
|
|
199bc459b4 | ||
|
|
841d3cd29e | ||
|
|
2d853f20aa | ||
|
|
865900111d | ||
|
|
590a63ae31 | ||
|
|
fabfaa4cbb | ||
|
|
7fb527847e |
||
|
|
28200830ed |
||
|
|
64ebe0f437 |
||
|
|
c2f2c69e91 | ||
|
|
b1fa78665c | ||
|
|
1af06eab4c | ||
|
|
82cba0b06f |
||
|
|
336cc6f44b | ||
|
|
cff76d033d |
||
|
|
e08f18f447 |
||
|
|
1b4f4ee11e | ||
|
|
69d931750b | ||
|
|
f06de70c40 | ||
|
|
f3a4ed8e52 | ||
|
|
23b2f7197c | ||
|
|
fc231bca76 | ||
|
|
5ffcbed632 | ||
|
|
818a21b52e | ||
|
|
c7ed85a9a4 | ||
|
|
43522ee20a | ||
|
|
a96b071fb3 |
||
|
|
a7ddfbc8a3 | ||
|
|
81e9a205c6 | ||
|
|
1ba8f10440 |
||
|
|
ec5b5ef565 |
||
|
|
72ca99009b | ||
|
|
398dbcad53 | ||
|
|
2e82400d80 | ||
|
|
c62bc2fa35 | ||
|
|
17978af710 | ||
|
|
8c8d683348 | ||
|
|
5831745958 | ||
|
|
622c91b25a | ||
|
|
14b2ad7938 | ||
|
|
c7def1c1b6 | ||
|
|
be47af7ea8 | ||
|
|
e7d480c645 | ||
|
|
58b2d8c788 | ||
|
|
c70f1d0d95 | ||
|
|
97e35e9dd4 | ||
|
|
a3744cf81b | ||
|
|
da8cec4067 | ||
|
|
20b24a39df | ||
|
|
80df7f3836 | ||
|
|
19b4d7bea2 | ||
|
|
6f9d7b0771 | ||
|
|
ad10777684 | ||
|
|
f516d21f7b | ||
|
|
e967759802 | ||
|
|
77667e7a3f | ||
|
|
cfd71cab02 | ||
|
|
e0ed1195fa | ||
|
|
aee518365d | ||
|
|
a7d81d55bd | ||
|
|
5abc4c8c2a | ||
|
|
147a36adde | ||
|
|
0239fdd2fb | ||
|
|
bb7abcdaae | ||
|
|
e3a5c1b01c | ||
|
|
6f8ebcc7e1 | ||
|
|
c6b90690a7 | ||
|
|
f4cfa17c73 | ||
|
|
e43dd6ffdd | ||
|
|
a32a39e96b |
||
|
|
0bd07edab4 |
||
|
|
5d65f447b5 | ||
|
|
c9f63fc699 | ||
|
|
3480e091fa | ||
|
|
df7dfd77e3 | ||
|
|
e76019b82c | ||
|
|
25fefd5e54 | ||
|
|
ddc076e894 | ||
|
|
87c4e6f9bb | ||
|
|
358852cecb | ||
|
|
96e6ac8111 | ||
|
|
74da20ab6a | ||
|
|
015a9fb6ef | ||
|
|
23c7f7296c | ||
|
|
3fd2849a37 | ||
|
|
714580e94a | ||
|
|
eb4a4c7dfe | ||
|
|
f55d765aae | ||
|
|
2e59735c19 | ||
|
|
c1068164f5 | ||
|
|
138111b16e | ||
|
|
8dcc63fd6a | ||
|
|
b9d7739927 | ||
|
|
57fd0562a2 | ||
|
|
b0efbf8a1e | ||
|
|
16bc465cfd | ||
|
|
04adcac0e0 | ||
|
|
6f9acb0b94 | ||
|
|
da87482b66 | ||
|
|
0b85582744 | ||
|
|
ead4a7f791 | ||
|
|
d09ac7ae13 | ||
|
|
3fa9efc0bc | ||
|
|
28528dc865 | ||
|
|
2091a5b41b | ||
|
|
04c67de1af | ||
|
|
f90c355c8e | ||
|
|
5cbf847c4a | ||
|
|
211d2e5460 |
||
|
|
a403dac8ef |
||
|
|
32a73d49f1 | ||
|
|
16795cf2b7 | ||
|
|
f6dd616d5e | ||
|
|
edc106dadb | ||
|
|
ef5b0d9e16 | ||
|
|
2917699b44 | ||
|
|
1a6f2fc387 | ||
|
|
cba87582a5 | ||
|
|
53ffcb5960 | ||
|
|
fd983a7f6b | ||
|
|
7ec0dcfec8 |
||
|
|
518160a1fa |
||
|
|
8bfce28d68 | ||
|
|
866409bfb0 | ||
|
|
b31c2a935b | ||
|
|
eacef7e175 | ||
|
|
93bc406c2e | ||
|
|
ad091a1d36 | ||
|
|
2dd71a1d15 | ||
|
|
fa09098b4a | ||
|
|
972115320a | ||
|
|
3304681e7a |
||
|
|
f77f54942b |
||
|
|
7349945b0e |
||
|
|
f5c0a7f7fc |
||
|
|
5a3b9cf969 | ||
|
|
797321b91a | ||
|
|
81bdf7d33e | ||
|
|
ca54c40d0f | ||
|
|
a2147fe6e1 | ||
|
|
c88a0875b6 | ||
|
|
52a560dc71 | ||
|
|
74dc5a6864 | ||
|
|
92bbe19a00 | ||
|
|
f505a4ef80 | ||
|
|
5a5bf823a0 | ||
|
|
9db2befb3b |
||
|
|
3c494d0674 |
||
|
|
7ea3ae484e |
||
|
|
550546741b |
||
|
|
2d046748fe | ||
|
|
b054cae078 | ||
|
|
7f73712694 |
||
|
|
c040bb4355 |
||
|
|
5e9ea333eb |
||
|
|
571818d4c2 | ||
|
|
22c00aca95 | ||
|
|
bd9e41f2c1 | ||
|
|
b46f92c7d8 | ||
|
|
16fb118d02 | ||
|
|
acf5dd5351 | ||
|
|
2cbf85cf4f | ||
|
|
24419cb8e8 | ||
|
|
1f1b025b81 |
||
|
|
4654ad42b1 |
||
|
|
085feb606e | ||
|
|
3e54569624 |
||
|
|
035d10005a | ||
|
|
07e4f4d1a4 | ||
|
|
0480275ab9 | ||
|
|
895fcc4066 | ||
|
|
54a9637ed4 | ||
|
|
45df995d7f |
||
|
|
4cbfc38106 |
||
|
|
3b665fb68f |
||
|
|
614085cc30 | ||
|
|
83f01f6f6c | ||
|
|
691f628e48 | ||
|
|
7ae364e360 | ||
|
|
fe77563cdb | ||
|
|
57d7616d1b | ||
|
|
4a75ca57e0 | ||
|
|
66e03bed79 | ||
|
|
89f6fb9047 | ||
|
|
783a8567e2 | ||
|
|
222075dd9d | ||
|
|
ae42f81d6f | ||
|
|
9766c3e254 | ||
|
|
328c61920e | ||
|
|
a4806e423f | ||
|
|
c5e215f439 | ||
|
|
6e3b5f9837 | ||
|
|
979d992bd1 | ||
|
|
aa850e9b99 | ||
|
|
e85cf24587 | ||
|
|
700d3c1ff2 | ||
|
|
60bf4b22b0 |
||
|
|
d6ca06b12b | ||
|
|
0981605fae |
||
|
|
c86b23e790 | ||
|
|
2831de8271 | ||
|
|
6d1eef836a |
||
|
|
381ea326f4 | ||
|
|
f7b7248ff7 | ||
|
|
7e72120c41 | ||
|
|
29cd9b80ef | ||
|
|
c91c01188c | ||
|
|
e2b55c491e | ||
|
|
8ad92ed7b7 | ||
|
|
dd48ba4e87 | ||
|
|
14ea84988f | ||
|
|
0b645d54c6 | ||
|
|
0773bf1ecb | ||
|
|
70655120cb | ||
|
|
45b23f73cf | ||
|
|
a3810dea06 | ||
|
|
131b1a6409 | ||
|
|
26ccd2f1f8 | ||
|
|
d6d7df62fe | ||
|
|
b9ead20fc7 | ||
|
|
927c40739e | ||
|
|
5eb5b99115 | ||
|
|
81a5615c9a | ||
|
|
06af65cf01 | ||
|
|
586dde7761 | ||
|
|
dd5e50fc68 | ||
|
|
6c8d1616fd | ||
|
|
5c3e15e17c |
||
|
|
e000ba45df |
||
|
|
29d8bc9d3d |
||
|
|
7229e0dda4 |
||
|
|
785842cde5 |
||
|
|
e3bd30b05f | ||
|
|
ba20d4f70e | ||
|
|
b03d01b6eb | ||
|
|
d0987719ce | ||
|
|
ac9101ed61 | ||
|
|
b1159eff9b | ||
|
|
443b1e8048 | ||
|
|
b69ba5e4b1 | ||
|
|
e45cfbf02c | ||
|
|
eca52bf8e3 | ||
|
|
fe07bf6637 | ||
|
|
5ab79effd2 | ||
|
|
413ab234d6 | ||
|
|
326f1ac476 | ||
|
|
c583d6edf9 | ||
|
|
b1e9a7ffda | ||
|
|
cfa9da17a7 | ||
|
|
6a26014b81 | ||
|
|
36a7cf4007 | ||
|
|
30bf20eb12 | ||
|
|
b45946ff2f | ||
|
|
37bfd91944 | ||
|
|
2cfc9119cb | ||
|
|
98e3bd0ca2 | ||
|
|
e6961c5aab | ||
|
|
57acf0f5ce | ||
|
|
184dd177a6 | ||
|
|
3c60a67a11 | ||
|
|
2e286849fc | ||
|
|
52814113a9 | ||
|
|
ad41a6f76e | ||
|
|
d1ecdb6b52 | ||
|
|
7522847ecc | ||
|
|
78221fc50b | ||
|
|
934400f5ee | ||
|
|
1e504f4383 | ||
|
|
a1bdd6f740 | ||
|
|
761dfbb33c | ||
|
|
5fabf2f61a | ||
|
|
648cfd12db | ||
|
|
bc5b03d2fc | ||
|
|
060627487a | ||
|
|
751d6690ff | ||
|
|
999e419636 | ||
|
|
6c0cf7cfcc | ||
|
|
fa1aecdd9e | ||
|
|
03e3444a35 | ||
|
|
90f4a94bb2 | ||
|
|
5b4c00d8ca | ||
|
|
eb1360c3af | ||
|
|
0b85ded53f | ||
|
|
ef8a2bab18 | ||
|
|
05d363d9a5 | ||
|
|
adfd99c92c | ||
|
|
b4310dbc03 | ||
|
|
cc85b2143c | ||
|
|
f9de811df1 | ||
|
|
8658f15751 | ||
|
|
9072fda146 | ||
|
|
5bab511c42 | ||
|
|
4de632ba3c | ||
|
|
f16823393a | ||
|
|
f5dc6cc28f | ||
|
|
5a274f3316 | ||
|
|
764ac831d4 | ||
|
|
8aa6f9c500 | ||
|
|
a900abc2a4 | ||
|
|
b750da3f9d | ||
|
|
4938878d10 | ||
|
|
fb04f9fcbf | ||
|
|
955aada1cf | ||
|
|
f185c86a47 | ||
|
|
5c336d3789 | ||
|
|
53e419355d | ||
|
|
cc0962ba12 | ||
|
|
6ae6600518 | ||
|
|
5bc1be1f39 | ||
|
|
fa51a2c281 | ||
|
|
d093a7f4c2 | ||
|
|
8627bbb713 | ||
|
|
ecb60e3c78 | ||
|
|
3140c66b9e | ||
|
|
1e2e8a82db | ||
|
|
8020c3c817 | ||
|
|
507bf05d24 | ||
|
|
fce3d11e74 | ||
|
|
3e9678d9cf | ||
|
|
f645c32cb9 | ||
|
|
725d8431f7 | ||
|
|
b7d353b620 | ||
|
|
5890a3def1 | ||
|
|
5b721c1b99 | ||
|
|
5d36b29aa8 | ||
|
|
c0a7ae9d92 | ||
|
|
d2331558c3 | ||
|
|
bc16e716f7 | ||
|
|
bcbd29cd22 | ||
|
|
7c5f631319 | ||
|
|
ba2aa7a852 | ||
|
|
8c987e7a28 | ||
|
|
8744d754ff | ||
|
|
23981e8de0 | ||
|
|
b8c49463a9 | ||
|
|
fe7c570cc9 | ||
|
|
bb24bc645c | ||
|
|
2c8e7a8cab | ||
|
|
c066f25b17 | ||
|
|
dce42df050 | ||
|
|
d842517c4e | ||
|
|
6a273d825a | ||
|
|
10bafb8b5d | ||
|
|
e1ff04174f | ||
|
|
e211d948d7 | ||
|
|
09eaf80f8c | ||
|
|
855092aa44 | ||
|
|
78b17f37f9 | ||
|
|
332047c0dc | ||
|
|
f11ee8eaf7 | ||
|
|
1b32bf6820 | ||
|
|
2d9aa35c06 | ||
|
|
c0e364e1c2 | ||
|
|
44f71bb93e | ||
|
|
4d592a6a40 | ||
|
|
110c0f0c87 | ||
|
|
152da11256 | ||
|
|
001f96035b | ||
|
|
953325aca3 | ||
|
|
e54cc451ce | ||
|
|
900b93ae07 | ||
|
|
c767ce9fb6 | ||
|
|
5923e48dda | ||
|
|
212703d162 | ||
|
|
332bbad2fd | ||
|
|
5ce8d93410 | ||
|
|
0a06874a97 | ||
|
|
6186abd550 | ||
|
|
4363ef4abe | ||
|
|
6b641059c1 | ||
|
|
443a50b1db | ||
|
|
e62da5b1ea | ||
|
|
abd155e881 | ||
|
|
fb672ab57f | ||
|
|
35b6b47de3 | ||
|
|
41e3975674 | ||
|
|
92349976cb | ||
|
|
2b85315b39 | ||
|
|
2c6f5251d8 | ||
|
|
da0a52e3cb | ||
|
|
d5d3cb605b | ||
|
|
9bf1e6e0d5 | ||
|
|
3f031ba6ff | ||
|
|
b98208317d | ||
|
|
7b2c284245 | ||
|
|
4a68b78fd5 | ||
|
|
d287dede49 | ||
|
|
2bcbb62af0 | ||
|
|
1f01da21ff | ||
|
|
b66afb6939 | ||
|
|
7487c1c1ac | ||
|
|
03fe53e87f | ||
|
|
993a7bcbde | ||
|
|
8fc811bf6b | ||
|
|
1126a68ebd | ||
|
|
755dc3480d | ||
|
|
272a259966 | ||
|
|
9bee548915 | ||
|
|
8bc66e8e5e | ||
|
|
ed3b4faa62 | ||
|
|
642442c041 | ||
|
|
c6ed95e555 | ||
|
|
7175dc1706 | ||
|
|
3b2e3fc08c | ||
|
|
3318acd16b | ||
|
|
d9358d555f | ||
|
|
777f477135 | ||
|
|
dcefcd19cb | ||
|
|
52cc3ee909 | ||
|
|
aac6980eb5 | ||
|
|
c409328ddf | ||
|
|
ae5f768dd1 | ||
|
|
ff72ebbb74 | ||
|
|
8ef99d7ad8 | ||
|
|
908f816c43 | ||
|
|
06f734753f | ||
|
|
972e1eefa7 | ||
|
|
1c732ffc5b | ||
|
|
8cd8ba6101 | ||
|
|
bdf4a93200 | ||
|
|
65a218d9de | ||
|
|
9997aafec7 | ||
|
|
f2e43b84be | ||
|
|
23599fc39b | ||
|
|
2f1cc97631 | ||
|
|
7c25a9e7f0 | ||
|
|
e00fa06db7 | ||
|
|
885bb16228 | ||
|
|
c1fc185643 | ||
|
|
0f9b12f2b8 | ||
|
|
7806579009 | ||
|
|
764019bade | ||
|
|
8a6d3851e5 | ||
|
|
24bd39361b | ||
|
|
c1a0796082 | ||
|
|
30b86a8b40 | ||
|
|
9f053dfaae | ||
|
|
1c96919551 | ||
|
|
a85b7a789e | ||
|
|
a3c0512143 | ||
|
|
86ed0b6e59 | ||
|
|
cd3ce55dcf | ||
|
|
7fec8ebc72 | ||
|
|
48bfa30acd | ||
|
|
d3d374c129 | ||
|
|
535a2e577f | ||
|
|
7c4050590c | ||
|
|
ffa0740b50 | ||
|
|
77721c0982 | ||
|
|
54b26891a8 | ||
|
|
3d17b12f58 | ||
|
|
eec8dcd130 | ||
|
|
db5afa0c1d | ||
|
|
f21a67b6f5 | ||
|
|
213a4b0f13 | ||
|
|
2025aff7eb | ||
|
|
78bb2edf1a | ||
|
|
b140a1c452 | ||
|
|
e0111ed587 | ||
|
|
4786085ec4 | ||
|
|
b7b12b9f75 | ||
|
|
3127de7a81 | ||
|
|
e53c3ebe11 | ||
|
|
00a3e3f14c | ||
|
|
d2323e12e2 | ||
|
|
097d7377f9 | ||
|
|
8d3c201a11 | ||
|
|
dc3ebcf1bb | ||
|
|
5e046a963b | ||
|
|
ebfa3f936d | ||
|
|
7848c3b82e | ||
|
|
2dbb56040c | ||
|
|
15daca2685 | ||
|
|
9e4708012d | ||
|
|
dfbe42b590 | ||
|
|
4b6b072c16 | ||
|
|
839ba62650 | ||
|
|
6755524be1 | ||
|
|
e9b4dd7c35 | ||
|
|
ef0128fb00 | ||
|
|
b30450141e | ||
|
|
e2abbff7b0 | ||
|
|
c5f16a4f41 | ||
|
|
a579b9acdc | ||
|
|
166e43ec39 | ||
|
|
0619646765 | ||
|
|
a0e0666e33 | ||
|
|
62c265337c | ||
|
|
a8a1af4b62 | ||
|
|
a74133ec69 | ||
|
|
a2ed979f08 | ||
|
|
410eff428b | ||
|
|
b73696a8bf | ||
|
|
7fb3b0dfb0 | ||
|
|
616396bc04 | ||
|
|
bf7630c96e | ||
|
|
7d910f305f | ||
|
|
69bb8412cc | ||
|
|
0b9c8a3432 | ||
|
|
4918fe6996 | ||
|
|
674141ff80 | ||
|
|
a8b93edd86 | ||
|
|
b0f5edd24d | ||
|
|
87b9c50aaa | ||
|
|
dcc98c3efb | ||
|
|
41b7e0e077 | ||
|
|
ba165de94e | ||
|
|
91ae814ead | ||
|
|
36893c6d7c | ||
|
|
bc8b699437 | ||
|
|
c7e80006b9 | ||
|
|
552fa3fae8 | ||
|
|
b67836ffe0 | ||
|
|
7d69648e11 | ||
|
|
05b11e7a3d | ||
|
|
cca908f87f | ||
|
|
d8f1690904 | ||
|
|
a8dd136168 | ||
|
|
ba0b89d2bb | ||
|
|
0d8b58425e | ||
|
|
c8848ea7b1 | ||
|
|
3e82994ae2 | ||
|
|
df4b14d546 | ||
|
|
a5cb6abf72 | ||
|
|
280d145502 | ||
|
|
7108a412f1 | ||
|
|
d97b3ad414 | ||
|
|
e0e607f543 | ||
|
|
259a7d8973 | ||
|
|
4c4d9610f3 | ||
|
|
3f4797f4a9 | ||
|
|
45bb0a7cf2 | ||
|
|
ba00738f1f | ||
|
|
c75771f13d | ||
|
|
d9cde04af6 | ||
|
|
8d0b75c674 | ||
|
|
ac99828298 | ||
|
|
3240faddc9 | ||
|
|
056aba8a9e | ||
|
|
f65a5a8c89 | ||
|
|
e45edff613 | ||
|
|
8fd1d161ea | ||
|
|
fb87bd3a58 | ||
|
|
6466680a8e | ||
|
|
51aea5ff14 | ||
|
|
c0fa58a8b0 | ||
|
|
da2e286ff8 | ||
|
|
7ae11babcb | ||
|
|
0489d0f2ae | ||
|
|
391f3cb59b | ||
|
|
783c8d41a3 | ||
|
|
3017e67f3a | ||
|
|
1d4fe979b1 | ||
|
|
4dd79af9bb | ||
|
|
555902bfba | ||
|
|
3e6a3651e9 | ||
|
|
4501c9f4f1 | ||
|
|
b07ec94bc3 | ||
|
|
d0dee0c47a | ||
|
|
46689b96b3 | ||
|
|
c37d2b435e | ||
|
|
3a3eebd61d | ||
|
|
55f1f84478 | ||
|
|
6887b66f67 | ||
|
|
094de1ff9b | ||
|
|
bf71c4b9bd | ||
|
|
7709847267 | ||
|
|
b9047d98a2 | ||
|
|
d24eb44991 | ||
|
|
06cba0af81 | ||
|
|
8fff5ee829 | ||
|
|
3d6e4a38c6 | ||
|
|
3c8179fda1 | ||
|
|
7933b2453d | ||
|
|
3d0e1fd9f0 | ||
|
|
770ede0587 | ||
|
|
8ba34f12de | ||
|
|
e56d0c3be0 | ||
|
|
2d09e09223 | ||
|
|
347440bfc3 | ||
|
|
95f83525f0 | ||
|
|
a9f692a2d3 | ||
|
|
6c546b2098 | ||
|
|
baf3348cda | ||
|
|
1065f17441 | ||
|
|
86358c619b | ||
|
|
d4554d2fa7 | ||
|
|
c20813609b | ||
|
|
a8926e2ced | ||
|
|
5213853524 | ||
|
|
909f59c48f | ||
|
|
2d1b33b930 | ||
|
|
485fab6cd4 | ||
|
|
778db968b3 | ||
|
|
bafe23b4b7 | ||
|
|
9c8b2cb8d1 | ||
|
|
bc01d6ccd1 | ||
|
|
b01517861d | ||
|
|
28056d678e | ||
|
|
303fab8519 | ||
|
|
b1478c1ae1 | ||
|
|
cc763bd47a | ||
|
|
3e5a4c9c38 | ||
|
|
4fe3c5e96a | ||
|
|
463a63aed3 | ||
|
|
62d4cd8fe8 | ||
|
|
a5ad573b2d | ||
|
|
085ede43df | ||
|
|
9260f6b845 | ||
|
|
69999f9190 | ||
|
|
9e249a1d2f | ||
|
|
0c1156a4d8 | ||
|
|
10fefab279 | ||
|
|
c160852042 | ||
|
|
a6f0dfbe47 | ||
|
|
0d54879e45 | ||
|
|
8d31a0b312 | ||
|
|
aa8e0ae2c2 | ||
|
|
06ecf625c6 | ||
|
|
d695c64c04 | ||
|
|
93f0f6942e | ||
|
|
c954d9c9a3 | ||
|
|
adf93f9fad | ||
|
|
d6545fd307 | ||
|
|
d8ef28c1ae | ||
|
|
ee801f8cd0 | ||
|
|
e51d82b0d9 | ||
|
|
00548e65d7 | ||
|
|
4078abb085 | ||
|
|
708788338c | ||
|
|
f8e616ce25 | ||
|
|
2f363c9803 | ||
|
|
6023035838 | ||
|
|
f24f707119 | ||
|
|
b5db0abc18 | ||
|
|
6e1cdb370b | ||
|
|
b7ff814d8b | ||
|
|
49c473c85c | ||
|
|
2f77d6981b | ||
|
|
9dd8a794e0 | ||
|
|
dff1a48e05 | ||
|
|
e21ec8b447 | ||
|
|
837f78f1ae | ||
|
|
e9b118ea71 | ||
|
|
0bef9e9cde | ||
|
|
ee1a629be9 | ||
|
|
ddaf7ff300 | ||
|
|
61a582f71f | ||
|
|
8ec6d969d1 | ||
|
|
c85e4070b1 | ||
|
|
f09cb0abfb | ||
|
|
1a4974b7df | ||
|
|
35af3b1710 | ||
|
|
b93fa12494 | ||
|
|
5ff1496061 | ||
|
|
3c3436e3e2 | ||
|
|
3231097e0d | ||
|
|
dba885be47 | ||
|
|
d82a894b7b | ||
|
|
c5e0dee3a3 | ||
|
|
f3f933ed44 | ||
|
|
45554ec7b7 | ||
|
|
dec9a173bf | ||
|
|
1ff011dfaf | ||
|
|
04f23704b8 | ||
|
|
427a630094 | ||
|
|
f5af8a4986 | ||
|
|
794b5e4734 | ||
|
|
79e20c83d5 | ||
|
|
a1f56c7395 | ||
|
|
b2a0cae626 | ||
|
|
38efe89f56 | ||
|
|
09f2d069de | ||
|
|
b503b8cb4f | ||
|
|
5598da73d9 | ||
|
|
e905c139d7 | ||
|
|
089c315a8e | ||
|
|
93053d497d | ||
|
|
bfeaeee873 | ||
|
|
3711aefc2d | ||
|
|
db1dc3675a | ||
|
|
a58b7839a7 | ||
|
|
3a0017583b | ||
|
|
ebd70824f0 | ||
|
|
4f5bb55951 | ||
|
|
93c4c14b72 | ||
|
|
b7814bc571 | ||
|
|
6dac7b1897 | ||
|
|
aabdf562a6 | ||
|
|
99218341ec | ||
|
|
0d058a5ef7 | ||
|
|
310ab8f43c | ||
|
|
7e39ae045f | ||
|
|
aa02fd5180 | ||
|
|
6b597d3b2f | ||
|
|
8a4100cd01 | ||
|
|
b28bba6dd4 | ||
|
|
e416d74f57 | ||
|
|
caa46042bf | ||
|
|
56fb04c81e | ||
|
|
3b8a478e34 | ||
|
|
e9d7d77cbd | ||
|
|
45ff1c0a00 | ||
|
|
8f3f1ca0b1 | ||
|
|
1e864e266e | ||
|
|
2e82c6b5c6 | ||
|
|
5b6f5d5dce | ||
|
|
ac3992bcdb | ||
|
|
94781c98d4 | ||
|
|
da215d5465 | ||
|
|
41525ec20c | ||
|
|
565e37e873 | ||
|
|
cd87df955e | ||
|
|
024369d4c3 | ||
|
|
beb6d1ea5b | ||
|
|
4328946f80 | ||
|
|
12839af684 | ||
|
|
2cbe0caf40 | ||
|
|
4e1d89f567 | ||
|
|
743d4b61d5 | ||
|
|
cd90d50a37 | ||
|
|
fc03a338fc | ||
|
|
63f4fc39c9 | ||
|
|
52a0552bc4 | ||
|
|
f2c4d08801 | ||
|
|
28b5fdb8ce | ||
|
|
ad4a79cbca | ||
|
|
3b8e4f36eb | ||
|
|
345aac1a27 | ||
|
|
65ba8af660 | ||
|
|
4541309988 | ||
|
|
7c02ef53b0 | ||
|
|
fd9edbfd69 | ||
|
|
b8c62377e6 | ||
|
|
7858504c4c | ||
|
|
1d08e909cc | ||
|
|
396a9cffb1 | ||
|
|
e568452528 | ||
|
|
98b7908c2b | ||
|
|
7b15235489 | ||
|
|
38175b1386 | ||
|
|
937cc4ebed | ||
|
|
eaa05f2473 | ||
|
|
6092ac9ec4 | ||
|
|
213384ded9 | ||
|
|
e9ab8e3b1f | ||
|
|
1dc3e74f7f | ||
|
|
e6bf20de2f | ||
|
|
bdb4d0de6a | ||
|
|
62ee13833b | ||
|
|
2bb782fe81 | ||
|
|
62edd07c23 | ||
|
|
f7466cb556 | ||
|
|
a85d645890 | ||
|
|
3dbfd12add | ||
|
|
4439303f90 | ||
|
|
8bd5d800d0 | ||
|
|
813572de47 | ||
|
|
ae2b27ba5f | ||
|
|
f1c3e376c5 | ||
|
|
578328d208 | ||
|
|
701e333d78 | ||
|
|
c090ab065f | ||
|
|
71f0455c91 | ||
|
|
4d24061481 | ||
|
|
8be62e4f55 | ||
|
|
ba8ddd88c0 | ||
|
|
87dfe2cc22 | ||
|
|
3b84e29e44 | ||
|
|
687a5846b6 | ||
|
|
99640e07d6 | ||
|
|
2b3b4ea924 | ||
|
|
8d838aa08d | ||
|
|
3b20b1bcab | ||
|
|
4be6a6e462 | ||
|
|
204e5e4ee4 | ||
|
|
dfe967b2b1 | ||
|
|
d5f234bdb5 | ||
|
|
6d72f023fa | ||
|
|
b153d568a0 | ||
|
|
c4cfd7e4b5 | ||
|
|
34036a4b7a | ||
|
|
7f6dfe1652 | ||
|
|
5332909c89 | ||
|
|
b66c538a45 | ||
|
|
d0ed6826be | ||
|
|
2af0435ee6 | ||
|
|
0c3dc31e31 | ||
|
|
76cafeef23 | ||
|
|
727e7deb5e | ||
|
|
eabc808565 | ||
|
|
ed9c257e4f | ||
|
|
66f6a623d8 | ||
|
|
3334713223 | ||
|
|
7217655110 | ||
|
|
f509e9fe5a | ||
|
|
40b8f0c293 | ||
|
|
36a21bacac | ||
|
|
d48830a1fd | ||
|
|
a959e0ae44 | ||
|
|
f2a0bc5d23 | ||
|
|
f824036225 | ||
|
|
4ddaa9a69a | ||
|
|
bec0c74772 | ||
|
|
11654ff3c1 | ||
|
|
dd02f0f029 | ||
|
|
14782a56b7 | ||
|
|
cf64cb04c4 | ||
|
|
facf306045 | ||
|
|
e6800c5ace | ||
|
|
1fb14771ef | ||
|
|
a6ffffa715 | ||
|
|
83d1a99608 | ||
|
|
e99bf9ac0a | ||
|
|
2041c936b2 | ||
|
|
d42ac23c55 |
||
|
|
987474cfc1 | ||
|
|
0f439545d4 | ||
|
|
1d47290ada | ||
|
|
0eef5d9240 |
534 changed files with 57117 additions and 41563 deletions
1
.browserslistrc
Normal file
1
.browserslistrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
last 2 year, firefox esr
|
||||
|
|
@ -6,6 +6,7 @@ root = true
|
|||
|
||||
[*]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
|
|
@ -15,10 +16,6 @@ insert_final_newline = true
|
|||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{json,yml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[.eslintrc]
|
||||
[*.{json,md,yml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
|
|
|||
|
|
@ -1,9 +1,3 @@
|
|||
# built by tools
|
||||
client/js/libs.min.js
|
||||
client/js/lounge.templates.js
|
||||
|
||||
# third party
|
||||
client/js/libs/jquery/*.js
|
||||
client/js/libs/*.js
|
||||
|
||||
public/
|
||||
coverage/
|
||||
dist/
|
||||
|
|
|
|||
193
.eslintrc.cjs
Normal file
193
.eslintrc.cjs
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
// @ts-check
|
||||
const {defineConfig} = require("eslint-define-config");
|
||||
|
||||
const projects = defineConfig({
|
||||
parserOptions: {
|
||||
project: [
|
||||
"./tsconfig.json",
|
||||
"./client/tsconfig.json",
|
||||
"./server/tsconfig.json",
|
||||
"./shared/tsconfig.json",
|
||||
"./test/tsconfig.json",
|
||||
],
|
||||
},
|
||||
}).parserOptions.project;
|
||||
|
||||
const baseRules = defineConfig({
|
||||
rules: {
|
||||
"block-scoped-var": "error",
|
||||
curly: ["error", "all"],
|
||||
"dot-notation": "error",
|
||||
eqeqeq: "error",
|
||||
"handle-callback-err": "error",
|
||||
"no-alert": "error",
|
||||
"no-catch-shadow": "error",
|
||||
"no-control-regex": "off",
|
||||
"no-console": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-else-return": "error",
|
||||
"no-implicit-globals": "error",
|
||||
"no-restricted-globals": ["error", "event", "fdescribe"],
|
||||
"no-template-curly-in-string": "error",
|
||||
"no-unsafe-negation": "error",
|
||||
"no-useless-computed-key": "error",
|
||||
"no-useless-constructor": "error",
|
||||
"no-useless-return": "error",
|
||||
"no-use-before-define": [
|
||||
"error",
|
||||
{
|
||||
functions: false,
|
||||
},
|
||||
],
|
||||
"no-var": "error",
|
||||
"object-shorthand": [
|
||||
"error",
|
||||
"methods",
|
||||
{
|
||||
avoidExplicitReturnArrows: true,
|
||||
},
|
||||
],
|
||||
"padding-line-between-statements": [
|
||||
"error",
|
||||
{
|
||||
blankLine: "always",
|
||||
prev: ["block", "block-like"],
|
||||
next: "*",
|
||||
},
|
||||
{
|
||||
blankLine: "always",
|
||||
prev: "*",
|
||||
next: ["block", "block-like"],
|
||||
},
|
||||
],
|
||||
"prefer-const": "error",
|
||||
"prefer-rest-params": "error",
|
||||
"prefer-spread": "error",
|
||||
"spaced-comment": ["error", "always"],
|
||||
strict: "off",
|
||||
yoda: "error",
|
||||
},
|
||||
}).rules;
|
||||
|
||||
const vueRules = defineConfig({
|
||||
rules: {
|
||||
"import/no-default-export": 0,
|
||||
"import/unambiguous": 0, // vue SFC can miss script tags
|
||||
"@typescript-eslint/prefer-readonly": 0, // can be used in template
|
||||
"vue/component-tags-order": [
|
||||
"error",
|
||||
{
|
||||
order: ["template", "style", "script"],
|
||||
},
|
||||
],
|
||||
"vue/multi-word-component-names": "off",
|
||||
"vue/no-mutating-props": "off",
|
||||
"vue/no-v-html": "off",
|
||||
"vue/require-default-prop": "off",
|
||||
"vue/v-slot-style": ["error", "longform"],
|
||||
},
|
||||
}).rules;
|
||||
|
||||
const tsRules = defineConfig({
|
||||
rules: {
|
||||
// note you must disable the base rule as it can report incorrect errors
|
||||
"no-shadow": "off",
|
||||
"@typescript-eslint/no-shadow": ["error"],
|
||||
"@typescript-eslint/no-redundant-type-constituents": "off",
|
||||
},
|
||||
}).rules;
|
||||
|
||||
const tsRulesTemp = defineConfig({
|
||||
rules: {
|
||||
// TODO: eventually remove these
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-this-alias": "off",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "off",
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
},
|
||||
}).rules;
|
||||
|
||||
const tsTestRulesTemp = defineConfig({
|
||||
rules: {
|
||||
// TODO: remove these
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/restrict-plus-operands": "off",
|
||||
},
|
||||
}).rules;
|
||||
|
||||
module.exports = defineConfig({
|
||||
root: true,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["**/*.ts", "**/*.vue"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: projects,
|
||||
extraFileExtensions: [".vue"],
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
"prettier",
|
||||
],
|
||||
rules: {
|
||||
...baseRules,
|
||||
...tsRules,
|
||||
...tsRulesTemp,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["**/*.vue"],
|
||||
parser: "vue-eslint-parser",
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
parser: "@typescript-eslint/parser",
|
||||
tsconfigRootDir: __dirname,
|
||||
project: projects,
|
||||
},
|
||||
plugins: ["vue"],
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/vue3-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
"prettier",
|
||||
],
|
||||
rules: {...baseRules, ...tsRules, ...tsRulesTemp, ...vueRules},
|
||||
},
|
||||
{
|
||||
files: ["./tests/**/*.ts"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
rules: {
|
||||
...baseRules,
|
||||
...tsRules,
|
||||
...tsRulesTemp,
|
||||
...tsTestRulesTemp,
|
||||
},
|
||||
},
|
||||
],
|
||||
env: {
|
||||
es6: true,
|
||||
browser: true,
|
||||
mocha: true,
|
||||
node: true,
|
||||
},
|
||||
extends: ["eslint:recommended", "prettier"],
|
||||
rules: baseRules,
|
||||
});
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
---
|
||||
|
||||
root: true
|
||||
|
||||
env:
|
||||
browser: true
|
||||
mocha: true
|
||||
node: true
|
||||
|
||||
rules:
|
||||
block-spacing: [2, always]
|
||||
brace-style: [2, 1tbs]
|
||||
comma-dangle: 0
|
||||
curly: [2, all]
|
||||
eqeqeq: 2
|
||||
indent: [2, tab]
|
||||
key-spacing: [2, {beforeColon: false, afterColon: true}]
|
||||
keyword-spacing: [2, {before: true, after: true}]
|
||||
linebreak-style: [2, unix]
|
||||
no-console: 0
|
||||
no-control-regex: 0
|
||||
no-inner-declarations: 2
|
||||
no-invalid-regexp: 2
|
||||
no-irregular-whitespace: 2
|
||||
no-trailing-spaces: 2
|
||||
no-unexpected-multiline: 2
|
||||
no-unreachable: 2
|
||||
object-curly-spacing: [2, never]
|
||||
quotes: [2, double, avoid-escape]
|
||||
semi: [2, always]
|
||||
space-before-blocks: 2
|
||||
space-infix-ops: 2
|
||||
spaced-comment: [2, always]
|
||||
|
||||
globals:
|
||||
log: false
|
||||
$: false
|
||||
Favico: false
|
||||
Handlebars: false
|
||||
io: false
|
||||
Mousetrap: false
|
||||
|
||||
extends: eslint:recommended
|
||||
24
CONTRIBUTING.md → .github/CONTRIBUTING.md
vendored
24
CONTRIBUTING.md → .github/CONTRIBUTING.md
vendored
|
|
@ -3,24 +3,20 @@
|
|||
Welcome to The Lounge, it's great to have you here! We thank you in advance for
|
||||
your contributions.
|
||||
|
||||
### I have a question
|
||||
|
||||
- Find us on the Freenode channel `#thelounge`. You might not get an answer
|
||||
right away, but this channel is full of nice people who will be happy to
|
||||
help you.
|
||||
|
||||
### I want to report a bug
|
||||
|
||||
- Look at the [open and closed
|
||||
issues](https://github.com/thelounge/lounge/issues?q=is%3Aissue) to see if
|
||||
issues](https://github.com/thelounge/thelounge/issues?q=is%3Aissue) to see if
|
||||
this was not already discussed before. If you can't see any, feel free to
|
||||
[open a new issue](https://github.com/thelounge/lounge/issues/new).
|
||||
[open a new issue](https://github.com/thelounge/thelounge/issues/new).
|
||||
- If you think you discovered a security vulnerability, **do not open a public
|
||||
issue on GitHub.** Refer to our [security guidelines](/SECURITY.md) instead.
|
||||
|
||||
### I want to contribute to the code
|
||||
|
||||
- Make sure to discuss your ideas with the community in an
|
||||
[issue](https://github.com/thelounge/lounge/issues) or on the IRC channel.
|
||||
- Take a look at the open issues labeled as [`help wanted`](https://github.com/thelounge/lounge/issues?q=is%3Aopen+is%3Aissue+label%3Abug+label%3A%22help+wanted%22)
|
||||
[issue](https://github.com/thelounge/thelounge/issues) or on the IRC channel.
|
||||
- Take a look at the open issues labeled as [`help wanted`](https://github.com/thelounge/thelounge/labels/help%20wanted)
|
||||
if you want to help without having a specific idea in mind.
|
||||
- Make sure that your PRs do not contain unnecessary commits or merge commits.
|
||||
[Squash commits](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History)
|
||||
|
|
@ -32,6 +28,10 @@ your contributions.
|
|||
Pope's guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
|
||||
- Each PR will be reviewed by at least two different project maintainers. You
|
||||
can read more about this in the [maintainers'
|
||||
corner](https://github.com/thelounge/lounge/wiki/Maintainers'-corner).
|
||||
corner](https://github.com/thelounge/thelounge/wiki/Maintainers'-corner).
|
||||
- Please document any relevant changes in the documentation that can be found
|
||||
[in its own repository](https://github.com/thelounge/thelounge.github.io).
|
||||
[in its own repository](https://github.com/thelounge/thelounge.chat).
|
||||
- Note that we use prettier on the project. You can set up IDE plugins to format
|
||||
on save ([see VS Code one here](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)).
|
||||
- We have a git hook to automatically run prettier before commit, in case you don't install the plugin.
|
||||
- If for any reason, prettier does not work for you, you can run `yarn format:prettier` and that should format everything.
|
||||
14
.github/ISSUE_TEMPLATE/Bug_Report.md
vendored
Normal file
14
.github/ISSUE_TEMPLATE/Bug_Report.md
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
name: Bug Report
|
||||
about: Create a bug report
|
||||
labels: "Type: Bug"
|
||||
---
|
||||
|
||||
<!-- Have a question? Join #thelounge on Libera.Chat -->
|
||||
|
||||
- _Node version:_
|
||||
- _Browser version:_
|
||||
- _Device, operating system:_
|
||||
- _The Lounge version:_
|
||||
|
||||
---
|
||||
10
.github/ISSUE_TEMPLATE/Feature_Request.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/Feature_Request.md
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
name: Feature Request
|
||||
about: Request a new feature
|
||||
labels: "Type: Feature"
|
||||
---
|
||||
|
||||
<!-- Have a question? Join #thelounge on Libera.Chat. -->
|
||||
<!-- Make sure to check the existing issues prior to submitting your suggestion. -->
|
||||
|
||||
### Feature Description
|
||||
16
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
16
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
contact_links:
|
||||
- name: Docker container issues
|
||||
url: https://github.com/thelounge/thelounge-docker/issues
|
||||
about: Report issues related to the Docker container here
|
||||
|
||||
- name: Debian package issues
|
||||
url: https://github.com/thelounge/thelounge-deb/issues
|
||||
about: Report issues related to the Debian package here
|
||||
|
||||
- name: Arch Linux package issues
|
||||
url: https://github.com/thelounge/thelounge-archlinux/issues
|
||||
about: Report issues related to the Arch Linux package here
|
||||
|
||||
- name: General support
|
||||
url: https://demo.thelounge.chat/?join=%23thelounge
|
||||
about: "Join #thelounge on Libera.Chat to ask a question before creating an issue"
|
||||
11
.github/SUPPORT.md
vendored
Normal file
11
.github/SUPPORT.md
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
## Support
|
||||
|
||||
Welcome to The Lounge, it's great to have you here! If you have a question, or
|
||||
need help, you have a few options:
|
||||
|
||||
- Check out [existing questions on Stack Overflow](https://stackoverflow.com/questions/tagged/thelounge)
|
||||
to see if yours has been answered before. If not, feel free to [ask for a new question](https://stackoverflow.com/questions/ask?tags=thelounge)
|
||||
(using `thelounge` tag so that other people can easily find it).
|
||||
- Find us on the Libera.Chat channel `#thelounge`. You might not get an answer
|
||||
right away, but this channel is full of nice people who will be happy to
|
||||
help you.
|
||||
48
.github/workflows/build.yml
vendored
Normal file
48
.github/workflows/build.yml
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
name: Build
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Node ${{ matrix.node_version }} on ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
# EOL: April 2025
|
||||
- os: macOS-latest
|
||||
node_version: 18.x
|
||||
- os: windows-latest
|
||||
node_version: 18.x
|
||||
- os: ubuntu-latest
|
||||
node_version: 18.x
|
||||
# EOL: April 2026
|
||||
- os: ubuntu-latest
|
||||
node_version: 20.x
|
||||
# EOL: April June 2024
|
||||
- os: ubuntu-latest
|
||||
node_version: 21.x
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node_version }}
|
||||
|
||||
- name: Install
|
||||
run: yarn --frozen-lockfile --non-interactive
|
||||
|
||||
- name: Build
|
||||
run: yarn build
|
||||
env:
|
||||
NODE_ENV: production
|
||||
|
||||
- name: Test
|
||||
run: yarn test
|
||||
53
.github/workflows/release.yml
vendored
Normal file
53
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
name: Release
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: v*
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release workflow
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "latest"
|
||||
registry-url: "https://registry.npmjs.org/"
|
||||
|
||||
- name: Install
|
||||
run: yarn --frozen-lockfile --non-interactive
|
||||
|
||||
- name: Build
|
||||
run: yarn build
|
||||
env:
|
||||
NODE_ENV: production
|
||||
|
||||
- name: Test
|
||||
run: yarn test
|
||||
|
||||
- name: Publish latest
|
||||
if: "!contains(github.ref, '-')"
|
||||
run: npm publish --tag latest --provenance
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
|
||||
|
||||
- name: Publish next
|
||||
if: contains(github.ref, '-')
|
||||
run: npm publish --tag next --provenance
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
|
||||
|
||||
- name: Remove next tag
|
||||
if: "!contains(github.ref, '-')"
|
||||
run: npm dist-tag rm thelounge next || true
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
|
|
@ -1,11 +1,9 @@
|
|||
node_modules/
|
||||
npm-debug.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
package-lock.json
|
||||
|
||||
coverage/
|
||||
|
||||
# Built assets created at npm install/prepublish time
|
||||
# See https://docs.npmjs.com/misc/scripts
|
||||
client/fonts/
|
||||
client/js/libs.min.js.map
|
||||
client/js/libs.min.js
|
||||
client/js/lounge.templates.js
|
||||
public/
|
||||
dist/
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
instrumentation:
|
||||
include-all-sources: true
|
||||
excludes:
|
||||
- Gruntfile.js
|
||||
- client/js/libs/*.js
|
||||
- client/js/libs/jquery/*.js
|
||||
- client/js/libs.min.js
|
||||
- client/js/lounge.js
|
||||
19
.npmignore
19
.npmignore
|
|
@ -1,19 +0,0 @@
|
|||
# This file must not contain generated assets listed in .gitignore.
|
||||
# npm-debug.log and node_modules/ are ignored by default.
|
||||
# See https://docs.npmjs.com/misc/developers#keeping-files-out-of-your-package
|
||||
|
||||
client/views/
|
||||
coverage/
|
||||
scripts/
|
||||
test/
|
||||
.editorconfig
|
||||
.eslintignore
|
||||
.eslintrc.yml
|
||||
.gitattributes
|
||||
.gitignore
|
||||
.istanbul.yml
|
||||
.npmignore
|
||||
.stylelintrc
|
||||
.travis.yml
|
||||
appveyor.yml
|
||||
Gruntfile.js
|
||||
2
.npmrc
Normal file
2
.npmrc
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
save-exact = true
|
||||
sign-git-tag = true
|
||||
28
.prettierignore
Normal file
28
.prettierignore
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
coverage/
|
||||
public/
|
||||
dist/
|
||||
test/fixtures/.thelounge/logs/
|
||||
test/fixtures/.thelounge/certificates/
|
||||
test/fixtures/.thelounge/storage/
|
||||
test/fixtures/.thelounge/sts-policies.json
|
||||
*.log
|
||||
*.png
|
||||
*.svg
|
||||
*.ico
|
||||
*.wav
|
||||
*.tpl
|
||||
*.sh
|
||||
*.opts
|
||||
*.txt
|
||||
yarn.lock
|
||||
.gitignore
|
||||
.npmrc
|
||||
.npmignore
|
||||
.prettierignore
|
||||
.thelounge_home
|
||||
.editorconfig
|
||||
.eslintignore
|
||||
.gitattributes
|
||||
.browserslistrc
|
||||
|
||||
*.css
|
||||
74
.stylelintrc
74
.stylelintrc
|
|
@ -1,74 +0,0 @@
|
|||
{
|
||||
"ignoreFiles": [
|
||||
"client/css/bootstrap.css",
|
||||
"coverage/**/*.css"
|
||||
],
|
||||
"rules": {
|
||||
"at-rule-empty-line-before": ["always", {
|
||||
"except": ["blockless-group", "first-nested"],
|
||||
"ignore": ["after-comment"]
|
||||
}],
|
||||
"block-closing-brace-newline-after": "always",
|
||||
"block-closing-brace-newline-before": "always-multi-line",
|
||||
"block-closing-brace-space-before": "always-single-line",
|
||||
"block-no-empty": true,
|
||||
"block-opening-brace-newline-after": "always-multi-line",
|
||||
"block-opening-brace-space-after": "always-single-line",
|
||||
"block-opening-brace-space-before": "always",
|
||||
"color-hex-case": "lower",
|
||||
"color-hex-length": "short",
|
||||
"color-no-invalid-hex": true,
|
||||
"comment-empty-line-before": ["always", {
|
||||
"except": ["first-nested"],
|
||||
"ignore": ["stylelint-commands"]
|
||||
}],
|
||||
"comment-whitespace-inside": "always",
|
||||
"declaration-bang-space-after": "never",
|
||||
"declaration-bang-space-before": "always",
|
||||
"declaration-block-semicolon-newline-after": "always-multi-line",
|
||||
"declaration-block-semicolon-space-after": "always-single-line",
|
||||
"declaration-block-semicolon-space-before": "never",
|
||||
"declaration-block-single-line-max-declarations": 1,
|
||||
"declaration-colon-newline-after": "always-multi-line",
|
||||
"declaration-colon-space-after": "always-single-line",
|
||||
"declaration-colon-space-before": "never",
|
||||
"function-calc-no-unspaced-operator": true,
|
||||
"function-comma-newline-after": "always-multi-line",
|
||||
"function-comma-space-after": "always-single-line",
|
||||
"function-comma-space-before": "never",
|
||||
"function-parentheses-newline-inside": "always-multi-line",
|
||||
"function-parentheses-space-inside": "never-single-line",
|
||||
"function-whitespace-after": "always",
|
||||
"function-url-quotes": "always",
|
||||
"indentation": "tab",
|
||||
"max-empty-lines": 1,
|
||||
"media-feature-colon-space-after": "always",
|
||||
"media-feature-colon-space-before": "never",
|
||||
"media-feature-range-operator-space-after": "always",
|
||||
"media-feature-range-operator-space-before": "always",
|
||||
"media-query-list-comma-newline-after": "always-multi-line",
|
||||
"media-query-list-comma-space-after": "always-single-line",
|
||||
"media-query-list-comma-space-before": "never",
|
||||
"media-query-parentheses-space-inside": "never",
|
||||
"no-eol-whitespace": true,
|
||||
"no-missing-eof-newline": true,
|
||||
"number-leading-zero": "never",
|
||||
"number-no-trailing-zeros": true,
|
||||
"length-zero-no-unit": true,
|
||||
"declaration-block-no-duplicate-properties": true,
|
||||
"declaration-block-no-shorthand-property-overrides": true,
|
||||
"rule-non-nested-empty-line-before": ["always-multi-line", {
|
||||
"ignore": ["after-comment"]
|
||||
}],
|
||||
"declaration-block-trailing-semicolon": "always",
|
||||
"selector-combinator-space-after": "always",
|
||||
"selector-combinator-space-before": "always",
|
||||
"selector-list-comma-newline-after": "always",
|
||||
"selector-list-comma-space-before": "never",
|
||||
"selector-pseudo-element-colon-notation": "single",
|
||||
"string-quotes": "double",
|
||||
"value-list-comma-newline-after": "always-multi-line",
|
||||
"value-list-comma-space-after": "always-single-line",
|
||||
"value-list-comma-space-before": "never"
|
||||
}
|
||||
}
|
||||
1
.thelounge_home
Normal file
1
.thelounge_home
Normal file
|
|
@ -0,0 +1 @@
|
|||
~/.thelounge
|
||||
30
.travis.yml
30
.travis.yml
|
|
@ -1,30 +0,0 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- 6
|
||||
- 4
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- os: osx
|
||||
node_js: 4
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
|
||||
notifications:
|
||||
email:
|
||||
on_success: never
|
||||
on_failure: always
|
||||
|
||||
deploy:
|
||||
provider: npm
|
||||
email:
|
||||
secure: Eb/dO3VEnuG5CFSJbiTBDZ4X29o1bTITqfzc4SZJqkSKHLZ5/l0VHyd1In7T2U9yBtysnmm+dsOWYFwnH5NMt5kvGkkX754HBDz0QXO//IqADA/1cH1MMXuzJjRvHNrtbq3c6Iv0vO827kXfvqwkfGTmXfreT5w+xF7Y+0SjF8pfu2d/Z5omrmoy9J9SF/kfmahKYZwakc3h8p29JPmnFMUAR0JiZS/2gLSHQnGA3mCcnlO+U3bQuTVW3Z9RhiG51f/EMFfNZ8pBttM6CgE2Zth3AT50jbKjRgYdYN2ee/Z3qUJIoA6dfPALC7B+Z2UekqTiKx4SCk+9vZJJXqT8J+Fe67Dki/FgNWnEZaTn8eFs+Gfh2nnokNZUMd/2mMT0y0KbRaOYQarn6lFw+/Cn9hD6e8uRCqY0+YspMvGtV3LuHFy+br6YphlG6YKxJzExtGDvrwlDD70xJtqcgnlET3XOdzvfCpRSskh7FmVJMoL39f/j9r4FzWVDmfnRnDT6Cac2dSdbQM0Ldw3+65l/57K/Km7NeHbLA3LsnjSJqXuysYwosd6iUOQen59Dy+TvwKafEfAGXWcZNguFURIMf2LRZ4rwTZl6pp30nj23U6rmkWm3JTRZC95i/O4yP2rVoljNUEuMlHVts63r3lwXtuGQVo3+lQCYErK4Ceo7cQc=
|
||||
api_key:
|
||||
secure: I9iN31GWI+Mz0xPw81N7qh1M6uidB+3BmiPUXt8QigX45zwp9EhvfZ0U/AIdUyQwzK2RK1zLRQSt+2/1jyeVi+U+AAsRRmaAUx8iqKaQPAkPnQtElolgRP04WSgo7fvNejfM7zS939bQNKG3RlSm04yPgu+ke2igf799p2bpFe2LtyoEeIiUfrUkBiMSpMguN9XF8a7jqCyIouTKjXHR24RmzJ9r7ZoMV27yQauS7XlD81bontzNRZxTytDKdJpZ+sxGIT9mbbtM4LUFX8MeNe3p/bjWavEhrO0ZIpkbOfS/L/w1375YDoNPXxCs288lnGUH+NbGNAEfn+BTz8cmUp7jI7QWR/kNACPeopdAX4OdZxT8wfQcfQZrfCuSpKciOMC7vGgPpQqjQ61t1RKcKs9VUnwC0SwWjyo8LlzkFKnP1ks0eDGYsSoPLdpC9+76UmePkQdxMhscO8TOgkOCcsTMLiyt6ABGOGKu2iE5SsjUYtPiSiRzSBAQENoO560+xBSVTKwqvvhzUAIt4AuAQSgsFjAylDdyzKoObHX12hBdALrqSOOSVwwIQ5/jTgNAsilURHo7KPD407PhRnLOsvumL0qg4sr9S1hjuUKnNla5dg9GY8FVjJ+b2t0A2vgfG1pR1e3vrJRXrpkfRorhmjvKAk2o5you5pQ1Itty7rM=
|
||||
on:
|
||||
node: '4'
|
||||
tags: true
|
||||
repo: thelounge/lounge
|
||||
10
.vscode/extensions.json
vendored
Normal file
10
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"EditorConfig.EditorConfig",
|
||||
"esbenp.prettier-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"Vue.volar",
|
||||
"Vue.vscode-typescript-vue-plugin"
|
||||
],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node-terminal",
|
||||
"name": "Run Dev",
|
||||
"request": "launch",
|
||||
"command": "yarn dev",
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
.vscode/settings.json
vendored
Normal file
10
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"editor.formatOnSave": true,
|
||||
"prettier.useEditorConfig": true,
|
||||
"prettier.requireConfig": true,
|
||||
"prettier.disableLanguages": [],
|
||||
"eslint.packageManager": "yarn",
|
||||
"eslint.codeActionsOnSave.mode": "all",
|
||||
"[typescript]": {"editor.defaultFormatter": "esbenp.prettier-vscode"},
|
||||
"[vue]": {"editor.defaultFormatter": "esbenp.prettier-vscode"}
|
||||
}
|
||||
4568
CHANGELOG.md
4568
CHANGELOG.md
File diff suppressed because it is too large
Load diff
26
Gruntfile.js
26
Gruntfile.js
|
|
@ -1,26 +0,0 @@
|
|||
module.exports = function(grunt) {
|
||||
var libs = "client/js/libs/**/*.js";
|
||||
grunt.initConfig({
|
||||
watch: {
|
||||
files: libs,
|
||||
tasks: ["uglify"]
|
||||
},
|
||||
uglify: {
|
||||
options: {
|
||||
sourceMap: true,
|
||||
compress: false
|
||||
},
|
||||
js: {
|
||||
files: {
|
||||
"client/js/libs.min.js": libs
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
grunt.loadNpmTasks("grunt-contrib-uglify");
|
||||
grunt.loadNpmTasks("grunt-contrib-watch");
|
||||
grunt.registerTask(
|
||||
"default",
|
||||
["uglify"]
|
||||
);
|
||||
};
|
||||
124
README.md
124
README.md
|
|
@ -1,75 +1,95 @@
|
|||
[](https://avatar.playat.ch:1000/)
|
||||
[](https://www.npmjs.org/package/thelounge)
|
||||
[](https://travis-ci.org/thelounge/lounge)
|
||||
[](https://ci.appveyor.com/project/astorije/lounge/branch/master)
|
||||
[](https://david-dm.org/thelounge/lounge)
|
||||
[](https://david-dm.org/thelounge/lounge#info=devDependencies)
|
||||
<h1 align="center">
|
||||
<img
|
||||
width="300"
|
||||
alt="The Lounge"
|
||||
src="https://raw.githubusercontent.com/thelounge/thelounge/master/client/img/logo-vertical-transparent-bg.svg?sanitize=true">
|
||||
</h1>
|
||||
|
||||
# The Lounge
|
||||
<h3 align="center">
|
||||
Modern web IRC client designed for self-hosting
|
||||
</h3>
|
||||
|
||||
__What is it?__
|
||||
<p align="center">
|
||||
<strong>
|
||||
<a href="https://thelounge.chat/">Website</a>
|
||||
•
|
||||
<a href="https://thelounge.chat/docs">Docs</a>
|
||||
•
|
||||
<a href="https://demo.thelounge.chat/">Demo</a>
|
||||
•
|
||||
<a href="https://github.com/thelounge/thelounge-docker">Docker</a>
|
||||
</strong>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://demo.thelounge.chat/"><img
|
||||
alt="#thelounge IRC channel on Libera.Chat"
|
||||
src="https://img.shields.io/badge/Libera.Chat-%23thelounge-415364.svg?colorA=ff9e18"></a>
|
||||
<a href="https://yarn.pm/thelounge"><img
|
||||
alt="npm version"
|
||||
src="https://img.shields.io/npm/v/thelounge.svg?colorA=333a41&maxAge=3600"></a>
|
||||
<a href="https://github.com/thelounge/thelounge/actions"><img
|
||||
alt="Build Status"
|
||||
src="https://github.com/thelounge/thelounge/workflows/Build/badge.svg"></a>
|
||||
</p>
|
||||
|
||||
The Lounge is a web IRC client that you host on your own server.
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/thelounge/thelounge.github.io/master/img/thelounge-screenshot.png" width="550">
|
||||
</p>
|
||||
|
||||
*This is the official, community-managed fork of @erming's great initiative, the [Shout](https://github.com/erming/shout) project.*
|
||||
## Overview
|
||||
|
||||
__What features does it have?__
|
||||
- **Modern features brought to IRC.** Push notifications, link previews, new message markers, and more bring IRC to the 21st century.
|
||||
- **Always connected.** Remains connected to IRC servers while you are offline.
|
||||
- **Cross platform.** It doesn't matter what OS you use, it just works wherever Node.js runs.
|
||||
- **Responsive interface.** The client works smoothly on every desktop, smartphone and tablet.
|
||||
- **Synchronized experience.** Always resume where you left off no matter what device.
|
||||
|
||||
- Multiple user support
|
||||
- Stays connected even when you close the browser
|
||||
- Connect from multiple devices at once
|
||||
- Responsive layout — works well on your smartphone
|
||||
- _.. and more!_
|
||||
To learn more about configuration, usage and features of The Lounge, take a look at [the website](https://thelounge.chat).
|
||||
|
||||
__Why the fork?__
|
||||
The Lounge is the official and community-managed fork of [Shout](https://github.com/erming/shout), by [Mattias Erming](https://github.com/erming).
|
||||
|
||||
We felt that the original [Shout](https://github.com/erming/shout) project
|
||||
"stagnated" a little because its original author wanted it to remain his pet
|
||||
project (which is a perfectly fine thing!).
|
||||
## Installation and usage
|
||||
|
||||
A bunch of people, excited about doing things a bit differently than the upstream
|
||||
project forked it under a new name: “The Lounge”.
|
||||
The Lounge requires latest [Node.js](https://nodejs.org/) LTS version or more recent.
|
||||
The [Yarn package manager](https://yarnpkg.com/) is also recommended.
|
||||
If you want to install with npm, `--unsafe-perm` is required for a correct install.
|
||||
|
||||
This fork aims to be community managed, meaning that the decisions are taken
|
||||
in a collegial fashion, and that a bunch of maintainers should be able to make
|
||||
the review process quicker and more streamlined.
|
||||
### Running stable releases
|
||||
|
||||
## Install
|
||||
Please refer to the [install and upgrade documentation on our website](https://thelounge.chat/docs/install-and-upgrade) for all available installation methods.
|
||||
|
||||
To use The Lounge you must have [Node.js](https://nodejs.org/en/download/) installed.
|
||||
The oldest Node.js version we support is 4.2.0.
|
||||
### Running from source
|
||||
|
||||
If you still use 0.10 or 0.12 we strongly advise you to upgrade before installing The Lounge.
|
||||
For more information on how to upgrade, read the [documentation](https://nodejs.org/en/download/package-manager/).
|
||||
The following commands install and run the development version of The Lounge:
|
||||
|
||||
```
|
||||
sudo npm install -g thelounge
|
||||
```sh
|
||||
git clone https://github.com/thelounge/thelounge.git
|
||||
cd thelounge
|
||||
yarn install
|
||||
NODE_ENV=production yarn build
|
||||
yarn start
|
||||
```
|
||||
|
||||
## Usage
|
||||
When installed like this, `thelounge` executable is not created. Use `node index <command>` to run commands.
|
||||
|
||||
When the install is complete, go ahead and run this in your terminal:
|
||||
|
||||
```
|
||||
lounge --help
|
||||
```
|
||||
|
||||
For more information, read the [documentation](https://thelounge.github.io/docs/).
|
||||
⚠️ While it is the most recent codebase, this is not production-ready! Run at
|
||||
your own risk. It is also not recommended to run this as root.
|
||||
|
||||
## Development setup
|
||||
|
||||
To run the app from source, just clone the code and run this in your terminal:
|
||||
Simply follow the instructions to run The Lounge from source above, on your own
|
||||
fork.
|
||||
|
||||
```
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
Before submitting any change, make sure to:
|
||||
|
||||
You will have to run `npm run build` if you change or add anything in
|
||||
`client/js/libs` or `client/views`.
|
||||
- Read the [Contributing instructions](https://github.com/thelounge/thelounge/blob/master/.github/CONTRIBUTING.md#contributing)
|
||||
- Run `yarn test` to execute linters and the test suite
|
||||
- Run `yarn format:prettier` if linting fails
|
||||
- Run `yarn build:client` if you change or add anything in `client/js` or `client/components`
|
||||
- The built files will be output to `public/` by webpack
|
||||
- Run `yarn build:server` if you change anything in `server/`
|
||||
- The built files will be output to `dist/` by tsc
|
||||
- `yarn dev` can be used to start The Lounge with hot module reloading
|
||||
|
||||
## License
|
||||
|
||||
Available under the [MIT License](LICENSE).
|
||||
|
||||
Some fonts licensed under [SIL OFL](http://scripts.sil.org/OFL) and the [Apache License](http://www.apache.org/licenses/).
|
||||
To ensure that you don't commit files that fail the linting, you can install a pre-commit git hook.
|
||||
Execute `yarn githooks-install` to do so.
|
||||
|
|
|
|||
9
SECURITY.md
Normal file
9
SECURITY.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Responsible Disclosure of Security Vulnerabilities
|
||||
|
||||
- ⚠️ **Do not open public issues on GitHub to report security vulnerabilities.**
|
||||
- Contact us privately first, in a
|
||||
[responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure)
|
||||
manner.
|
||||
- On IRC, send a private message to any voiced user on our Libera.Chat channel,
|
||||
`#thelounge`.
|
||||
- By email, send us your report at <security@thelounge.chat>.
|
||||
29
appveyor.yml
29
appveyor.yml
|
|
@ -1,29 +0,0 @@
|
|||
---
|
||||
# http://www.appveyor.com/docs/appveyor-yml
|
||||
|
||||
# Build version format
|
||||
version: "{build}"
|
||||
|
||||
# Do not build on tags (GitHub only)
|
||||
skip_tags: true
|
||||
|
||||
environment:
|
||||
nodejs_version: '4'
|
||||
|
||||
install:
|
||||
- ps: Install-Product node $env:nodejs_version
|
||||
- npm install
|
||||
- npm install mocha-appveyor-reporter
|
||||
- echo --reporter mocha-appveyor-reporter >> test/mocha.opts
|
||||
|
||||
test_script:
|
||||
- node --version
|
||||
- npm --version
|
||||
- npm test
|
||||
|
||||
# cache npm modules
|
||||
cache:
|
||||
- node_modules
|
||||
|
||||
# Don't actually build
|
||||
build: off
|
||||
4
babel.config.cjs
Normal file
4
babel.config.cjs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
module.exports = {
|
||||
presets: [["@babel/preset-env", {bugfixes: true}], "babel-preset-typescript-vue3"],
|
||||
plugins: ["@babel/plugin-transform-runtime"],
|
||||
};
|
||||
Binary file not shown.
BIN
client/audio/pop.wav
Normal file
BIN
client/audio/pop.wav
Normal file
Binary file not shown.
195
client/components/App.vue
Normal file
195
client/components/App.vue
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
<template>
|
||||
<div id="viewport" :class="viewportClasses" role="tablist">
|
||||
<Sidebar v-if="store.state.appLoaded" :overlay="overlay" />
|
||||
<div
|
||||
id="sidebar-overlay"
|
||||
ref="overlay"
|
||||
aria-hidden="true"
|
||||
@click="store.commit('sidebarOpen', false)"
|
||||
/>
|
||||
<router-view ref="loungeWindow"></router-view>
|
||||
<Mentions />
|
||||
<ImageViewer ref="imageViewer" />
|
||||
<ContextMenu ref="contextMenu" />
|
||||
<ConfirmDialog ref="confirmDialog" />
|
||||
<div id="upload-overlay"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import constants from "../js/constants";
|
||||
import eventbus from "../js/eventbus";
|
||||
import Mousetrap, {ExtendedKeyboardEvent} from "mousetrap";
|
||||
import throttle from "lodash/throttle";
|
||||
import storage from "../js/localStorage";
|
||||
import isIgnoredKeybind from "../js/helpers/isIgnoredKeybind";
|
||||
|
||||
import Sidebar from "./Sidebar.vue";
|
||||
import ImageViewer from "./ImageViewer.vue";
|
||||
import ContextMenu from "./ContextMenu.vue";
|
||||
import ConfirmDialog from "./ConfirmDialog.vue";
|
||||
import Mentions from "./Mentions.vue";
|
||||
import {
|
||||
computed,
|
||||
provide,
|
||||
defineComponent,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
ref,
|
||||
Ref,
|
||||
InjectionKey,
|
||||
} from "vue";
|
||||
import {useStore} from "../js/store";
|
||||
import type {DebouncedFunc} from "lodash";
|
||||
|
||||
export const imageViewerKey = Symbol() as InjectionKey<Ref<typeof ImageViewer | null>>;
|
||||
const contextMenuKey = Symbol() as InjectionKey<Ref<typeof ContextMenu | null>>;
|
||||
const confirmDialogKey = Symbol() as InjectionKey<Ref<typeof ConfirmDialog | null>>;
|
||||
|
||||
export default defineComponent({
|
||||
name: "App",
|
||||
components: {
|
||||
Sidebar,
|
||||
ImageViewer,
|
||||
ContextMenu,
|
||||
ConfirmDialog,
|
||||
Mentions,
|
||||
},
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const overlay = ref(null);
|
||||
const loungeWindow = ref(null);
|
||||
const imageViewer = ref(null);
|
||||
const contextMenu = ref(null);
|
||||
const confirmDialog = ref(null);
|
||||
|
||||
provide(imageViewerKey, imageViewer);
|
||||
provide(contextMenuKey, contextMenu);
|
||||
provide(confirmDialogKey, confirmDialog);
|
||||
|
||||
const viewportClasses = computed(() => {
|
||||
return {
|
||||
notified: store.getters.highlightCount > 0,
|
||||
"menu-open": store.state.appLoaded && store.state.sidebarOpen,
|
||||
"menu-dragging": store.state.sidebarDragging,
|
||||
"userlist-open": store.state.userlistOpen,
|
||||
};
|
||||
});
|
||||
|
||||
const debouncedResize = ref<DebouncedFunc<() => void>>();
|
||||
const dayChangeTimeout = ref<any>();
|
||||
|
||||
const escapeKey = () => {
|
||||
eventbus.emit("escapekey");
|
||||
};
|
||||
|
||||
const toggleSidebar = (e: ExtendedKeyboardEvent) => {
|
||||
if (isIgnoredKeybind(e)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
store.commit("toggleSidebar");
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const toggleUserList = (e: ExtendedKeyboardEvent) => {
|
||||
if (isIgnoredKeybind(e)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
store.commit("toggleUserlist");
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const toggleMentions = () => {
|
||||
if (store.state.networks.length !== 0) {
|
||||
eventbus.emit("mentions:toggle");
|
||||
}
|
||||
};
|
||||
|
||||
const msUntilNextDay = () => {
|
||||
// Compute how many milliseconds are remaining until the next day starts
|
||||
const today = new Date();
|
||||
const tommorow = new Date(
|
||||
today.getFullYear(),
|
||||
today.getMonth(),
|
||||
today.getDate() + 1
|
||||
).getTime();
|
||||
|
||||
return tommorow - today.getTime();
|
||||
};
|
||||
|
||||
const prepareOpenStates = () => {
|
||||
const viewportWidth = window.innerWidth;
|
||||
let isUserlistOpen = storage.get("thelounge.state.userlist");
|
||||
|
||||
if (viewportWidth > constants.mobileViewportPixels) {
|
||||
store.commit("sidebarOpen", storage.get("thelounge.state.sidebar") !== "false");
|
||||
}
|
||||
|
||||
// If The Lounge is opened on a small screen (less than 1024px), and we don't have stored
|
||||
// user list state, close it by default
|
||||
if (viewportWidth >= 1024 && isUserlistOpen !== "true" && isUserlistOpen !== "false") {
|
||||
isUserlistOpen = "true";
|
||||
}
|
||||
|
||||
store.commit("userlistOpen", isUserlistOpen === "true");
|
||||
};
|
||||
|
||||
prepareOpenStates();
|
||||
|
||||
onMounted(() => {
|
||||
Mousetrap.bind("esc", escapeKey);
|
||||
Mousetrap.bind("alt+u", toggleUserList);
|
||||
Mousetrap.bind("alt+s", toggleSidebar);
|
||||
Mousetrap.bind("alt+m", toggleMentions);
|
||||
|
||||
debouncedResize.value = throttle(() => {
|
||||
eventbus.emit("resize");
|
||||
}, 100);
|
||||
|
||||
window.addEventListener("resize", debouncedResize.value, {passive: true});
|
||||
|
||||
// Emit a daychange event every time the day changes so date markers know when to update themselves
|
||||
const emitDayChange = () => {
|
||||
eventbus.emit("daychange");
|
||||
// This should always be 24h later but re-computing exact value just in case
|
||||
dayChangeTimeout.value = setTimeout(emitDayChange, msUntilNextDay());
|
||||
};
|
||||
|
||||
dayChangeTimeout.value = setTimeout(emitDayChange, msUntilNextDay());
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
Mousetrap.unbind("esc");
|
||||
Mousetrap.unbind("alt+u");
|
||||
Mousetrap.unbind("alt+s");
|
||||
Mousetrap.unbind("alt+m");
|
||||
|
||||
if (debouncedResize.value) {
|
||||
window.removeEventListener("resize", debouncedResize.value);
|
||||
}
|
||||
|
||||
if (dayChangeTimeout.value) {
|
||||
clearTimeout(dayChangeTimeout.value);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
viewportClasses,
|
||||
escapeKey,
|
||||
toggleSidebar,
|
||||
toggleUserList,
|
||||
toggleMentions,
|
||||
store,
|
||||
overlay,
|
||||
loungeWindow,
|
||||
imageViewer,
|
||||
contextMenu,
|
||||
confirmDialog,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
65
client/components/Channel.vue
Normal file
65
client/components/Channel.vue
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<template>
|
||||
<!-- TODO: investigate -->
|
||||
<ChannelWrapper ref="wrapper" v-bind="$props">
|
||||
<span class="name">{{ channel.name }}</span>
|
||||
<span
|
||||
v-if="channel.unread"
|
||||
:class="{highlight: channel.highlight && !channel.muted}"
|
||||
class="badge"
|
||||
>{{ unreadCount }}</span
|
||||
>
|
||||
<template v-if="channel.type === 'channel'">
|
||||
<span
|
||||
v-if="channel.state === 0"
|
||||
class="parted-channel-tooltip tooltipped tooltipped-w"
|
||||
aria-label="Not currently joined"
|
||||
>
|
||||
<span class="parted-channel-icon" />
|
||||
</span>
|
||||
<span class="close-tooltip tooltipped tooltipped-w" aria-label="Leave">
|
||||
<button class="close" aria-label="Leave" @click.stop="close" />
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="close-tooltip tooltipped tooltipped-w" aria-label="Close">
|
||||
<button class="close" aria-label="Close" @click.stop="close" />
|
||||
</span>
|
||||
</template>
|
||||
</ChannelWrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {PropType, defineComponent, computed} from "vue";
|
||||
import roundBadgeNumber from "../js/helpers/roundBadgeNumber";
|
||||
import useCloseChannel from "../js/hooks/use-close-channel";
|
||||
import {ClientChan, ClientNetwork} from "../js/types";
|
||||
import ChannelWrapper from "./ChannelWrapper.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Channel",
|
||||
components: {
|
||||
ChannelWrapper,
|
||||
},
|
||||
props: {
|
||||
network: {
|
||||
type: Object as PropType<ClientNetwork>,
|
||||
required: true,
|
||||
},
|
||||
channel: {
|
||||
type: Object as PropType<ClientChan>,
|
||||
required: true,
|
||||
},
|
||||
active: Boolean,
|
||||
isFiltering: Boolean,
|
||||
},
|
||||
setup(props) {
|
||||
const unreadCount = computed(() => roundBadgeNumber(props.channel.unread));
|
||||
const close = useCloseChannel(props.channel);
|
||||
|
||||
return {
|
||||
unreadCount,
|
||||
close,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
112
client/components/ChannelWrapper.vue
Normal file
112
client/components/ChannelWrapper.vue
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<template>
|
||||
<!-- TODO: move closed style to it's own class -->
|
||||
<div
|
||||
v-if="isChannelVisible"
|
||||
ref="element"
|
||||
:class="[
|
||||
'channel-list-item',
|
||||
{active: active},
|
||||
{'parted-channel': channel.type === 'channel' && channel.state === 0},
|
||||
{'has-draft': channel.pendingMessage},
|
||||
{'has-unread': channel.unread},
|
||||
{'has-highlight': channel.highlight},
|
||||
{
|
||||
'not-secure':
|
||||
channel.type === 'lobby' && network.status.connected && !network.status.secure,
|
||||
},
|
||||
{'not-connected': channel.type === 'lobby' && !network.status.connected},
|
||||
{'is-muted': channel.muted},
|
||||
]"
|
||||
:aria-label="getAriaLabel()"
|
||||
:title="getAriaLabel()"
|
||||
:data-name="channel.name"
|
||||
:data-type="channel.type"
|
||||
:aria-controls="'#chan-' + channel.id"
|
||||
:aria-selected="active"
|
||||
:style="channel.closed ? {transition: 'none', opacity: 0.4} : undefined"
|
||||
role="tab"
|
||||
@click="click"
|
||||
@contextmenu.prevent="openContextMenu"
|
||||
>
|
||||
<slot :network="network" :channel="channel" :active-channel="activeChannel" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import eventbus from "../js/eventbus";
|
||||
import isChannelCollapsed from "../js/helpers/isChannelCollapsed";
|
||||
import {ClientNetwork, ClientChan} from "../js/types";
|
||||
import {computed, defineComponent, PropType} from "vue";
|
||||
import {useStore} from "../js/store";
|
||||
import {switchToChannel} from "../js/router";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ChannelWrapper",
|
||||
props: {
|
||||
network: {
|
||||
type: Object as PropType<ClientNetwork>,
|
||||
required: true,
|
||||
},
|
||||
channel: {
|
||||
type: Object as PropType<ClientChan>,
|
||||
required: true,
|
||||
},
|
||||
active: Boolean,
|
||||
isFiltering: Boolean,
|
||||
},
|
||||
setup(props) {
|
||||
const store = useStore();
|
||||
const activeChannel = computed(() => store.state.activeChannel);
|
||||
const isChannelVisible = computed(
|
||||
() => props.isFiltering || !isChannelCollapsed(props.network, props.channel)
|
||||
);
|
||||
|
||||
const getAriaLabel = () => {
|
||||
const extra: string[] = [];
|
||||
const type = props.channel.type;
|
||||
|
||||
if (props.channel.unread > 0) {
|
||||
if (props.channel.unread > 1) {
|
||||
extra.push(`${props.channel.unread} unread messages`);
|
||||
} else {
|
||||
extra.push(`${props.channel.unread} unread message`);
|
||||
}
|
||||
}
|
||||
|
||||
if (props.channel.highlight > 0) {
|
||||
if (props.channel.highlight > 1) {
|
||||
extra.push(`${props.channel.highlight} mentions`);
|
||||
} else {
|
||||
extra.push(`${props.channel.highlight} mention`);
|
||||
}
|
||||
}
|
||||
|
||||
return `${type}: ${props.channel.name} ${extra.length ? `(${extra.join(", ")})` : ""}`;
|
||||
};
|
||||
|
||||
const click = () => {
|
||||
if (props.isFiltering) {
|
||||
return;
|
||||
}
|
||||
|
||||
switchToChannel(props.channel);
|
||||
};
|
||||
|
||||
const openContextMenu = (event: MouseEvent) => {
|
||||
eventbus.emit("contextmenu:channel", {
|
||||
event: event,
|
||||
channel: props.channel,
|
||||
network: props.network,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
activeChannel,
|
||||
isChannelVisible,
|
||||
getAriaLabel,
|
||||
click,
|
||||
openContextMenu,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
274
client/components/Chat.vue
Normal file
274
client/components/Chat.vue
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
<template>
|
||||
<div id="chat-container" class="window" :data-current-channel="channel.name" lang="">
|
||||
<div
|
||||
id="chat"
|
||||
:class="{
|
||||
'hide-motd': !store.state.settings.motd,
|
||||
'time-seconds': store.state.settings.showSeconds,
|
||||
'time-12h': store.state.settings.use12hClock,
|
||||
'colored-nicks': true, // TODO temporarily fixes themes, to be removed in next major version
|
||||
}"
|
||||
>
|
||||
<div
|
||||
:id="'chan-' + channel.id"
|
||||
class="chat-view"
|
||||
:data-type="channel.type"
|
||||
:aria-label="channel.name"
|
||||
role="tabpanel"
|
||||
>
|
||||
<div class="header">
|
||||
<SidebarToggle />
|
||||
<span class="title" :aria-label="'Currently open ' + channel.type">{{
|
||||
channel.name
|
||||
}}</span>
|
||||
<div v-if="channel.editTopic === true" class="topic-container">
|
||||
<input
|
||||
ref="topicInput"
|
||||
:value="channel.topic"
|
||||
class="topic-input"
|
||||
placeholder="Set channel topic"
|
||||
enterkeyhint="done"
|
||||
@keyup.enter="saveTopic"
|
||||
@keyup.esc="channel.editTopic = false"
|
||||
/>
|
||||
<span aria-label="Save topic" class="save-topic" @click="saveTopic">
|
||||
<span type="button" aria-label="Save topic"></span>
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
v-else
|
||||
:title="channel.topic"
|
||||
:class="{topic: true, empty: !channel.topic}"
|
||||
@dblclick="editTopic"
|
||||
><ParsedMessage
|
||||
v-if="channel.topic"
|
||||
:network="network"
|
||||
:text="channel.topic"
|
||||
/></span>
|
||||
<MessageSearchForm
|
||||
v-if="
|
||||
store.state.settings.searchEnabled &&
|
||||
['channel', 'query'].includes(channel.type)
|
||||
"
|
||||
:network="network"
|
||||
:channel="channel"
|
||||
/>
|
||||
<button
|
||||
class="mentions"
|
||||
aria-label="Open your mentions"
|
||||
@click="openMentions"
|
||||
/>
|
||||
<button
|
||||
class="menu"
|
||||
aria-label="Open the context menu"
|
||||
@click="openContextMenu"
|
||||
/>
|
||||
<span
|
||||
v-if="channel.type === 'channel'"
|
||||
class="rt-tooltip tooltipped tooltipped-w"
|
||||
aria-label="Toggle user list"
|
||||
>
|
||||
<button
|
||||
class="rt"
|
||||
aria-label="Toggle user list"
|
||||
@click="store.commit('toggleUserlist')"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="channel.type === 'special'" class="chat-content">
|
||||
<div class="chat">
|
||||
<div class="messages">
|
||||
<div class="msg">
|
||||
<component
|
||||
:is="specialComponent"
|
||||
:network="network"
|
||||
:channel="channel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="chat-content">
|
||||
<div
|
||||
:class="[
|
||||
'scroll-down tooltipped tooltipped-w tooltipped-no-touch',
|
||||
{'scroll-down-shown': !channel.scrolledToBottom},
|
||||
]"
|
||||
aria-label="Jump to recent messages"
|
||||
@click="messageList?.jumpToBottom()"
|
||||
>
|
||||
<div class="scroll-down-arrow" />
|
||||
</div>
|
||||
<ChatUserList v-if="channel.type === 'channel'" :channel="channel" />
|
||||
<MessageList
|
||||
ref="messageList"
|
||||
:network="network"
|
||||
:channel="channel"
|
||||
:focused="focused"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="store.state.currentUserVisibleError"
|
||||
id="user-visible-error"
|
||||
@click="hideUserVisibleError"
|
||||
>
|
||||
{{ store.state.currentUserVisibleError }}
|
||||
</div>
|
||||
<ChatInput :network="network" :channel="channel" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import socket from "../js/socket";
|
||||
import eventbus from "../js/eventbus";
|
||||
import ParsedMessage from "./ParsedMessage.vue";
|
||||
import MessageList from "./MessageList.vue";
|
||||
import ChatInput from "./ChatInput.vue";
|
||||
import ChatUserList from "./ChatUserList.vue";
|
||||
import SidebarToggle from "./SidebarToggle.vue";
|
||||
import MessageSearchForm from "./MessageSearchForm.vue";
|
||||
import ListBans from "./Special/ListBans.vue";
|
||||
import ListInvites from "./Special/ListInvites.vue";
|
||||
import ListChannels from "./Special/ListChannels.vue";
|
||||
import ListIgnored from "./Special/ListIgnored.vue";
|
||||
import {defineComponent, PropType, ref, computed, watch, nextTick, onMounted, Component} from "vue";
|
||||
import type {ClientNetwork, ClientChan} from "../js/types";
|
||||
import {useStore} from "../js/store";
|
||||
import {SpecialChanType, ChanType} from "../../shared/types/chan";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Chat",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
MessageList,
|
||||
ChatInput,
|
||||
ChatUserList,
|
||||
SidebarToggle,
|
||||
MessageSearchForm,
|
||||
},
|
||||
props: {
|
||||
network: {type: Object as PropType<ClientNetwork>, required: true},
|
||||
channel: {type: Object as PropType<ClientChan>, required: true},
|
||||
focused: Number,
|
||||
},
|
||||
emits: ["channel-changed"],
|
||||
setup(props, {emit}) {
|
||||
const store = useStore();
|
||||
|
||||
const messageList = ref<typeof MessageList>();
|
||||
const topicInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const specialComponent = computed(() => {
|
||||
switch (props.channel.special) {
|
||||
case SpecialChanType.BANLIST:
|
||||
return ListBans as Component;
|
||||
case SpecialChanType.INVITELIST:
|
||||
return ListInvites as Component;
|
||||
case SpecialChanType.CHANNELLIST:
|
||||
return ListChannels as Component;
|
||||
case SpecialChanType.IGNORELIST:
|
||||
return ListIgnored as Component;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const channelChanged = () => {
|
||||
// Triggered when active channel is set or changed
|
||||
emit("channel-changed", props.channel);
|
||||
|
||||
socket.emit("open", props.channel.id);
|
||||
|
||||
if (props.channel.usersOutdated) {
|
||||
props.channel.usersOutdated = false;
|
||||
|
||||
socket.emit("names", {
|
||||
target: props.channel.id,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const hideUserVisibleError = () => {
|
||||
store.commit("currentUserVisibleError", null);
|
||||
};
|
||||
|
||||
const editTopic = () => {
|
||||
if (props.channel.type === ChanType.CHANNEL) {
|
||||
props.channel.editTopic = true;
|
||||
}
|
||||
};
|
||||
|
||||
const saveTopic = () => {
|
||||
props.channel.editTopic = false;
|
||||
|
||||
if (!topicInput.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newTopic = topicInput.value.value;
|
||||
|
||||
if (props.channel.topic !== newTopic) {
|
||||
const target = props.channel.id;
|
||||
const text = `/topic ${newTopic}`;
|
||||
socket.emit("input", {target, text});
|
||||
}
|
||||
};
|
||||
|
||||
const openContextMenu = (event: any) => {
|
||||
eventbus.emit("contextmenu:channel", {
|
||||
event: event,
|
||||
channel: props.channel,
|
||||
network: props.network,
|
||||
});
|
||||
};
|
||||
|
||||
const openMentions = (event: any) => {
|
||||
eventbus.emit("mentions:toggle", {
|
||||
event: event,
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.channel,
|
||||
() => {
|
||||
channelChanged();
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.channel.editTopic,
|
||||
(newTopic) => {
|
||||
if (newTopic) {
|
||||
void nextTick(() => {
|
||||
topicInput.value?.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
channelChanged();
|
||||
|
||||
if (props.channel.editTopic) {
|
||||
void nextTick(() => {
|
||||
topicInput.value?.focus();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
store,
|
||||
messageList,
|
||||
topicInput,
|
||||
specialComponent,
|
||||
hideUserVisibleError,
|
||||
editTopic,
|
||||
saveTopic,
|
||||
openContextMenu,
|
||||
openMentions,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
358
client/components/ChatInput.vue
Normal file
358
client/components/ChatInput.vue
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
<template>
|
||||
<form id="form" method="post" action="" @submit.prevent="onSubmit">
|
||||
<span id="upload-progressbar" />
|
||||
<span id="nick">{{ network.nick }}</span>
|
||||
<textarea
|
||||
id="input"
|
||||
ref="input"
|
||||
dir="auto"
|
||||
class="mousetrap"
|
||||
enterkeyhint="send"
|
||||
:value="channel.pendingMessage"
|
||||
:placeholder="getInputPlaceholder(channel)"
|
||||
:aria-label="getInputPlaceholder(channel)"
|
||||
@input="setPendingMessage"
|
||||
@keypress.enter.exact.prevent="onSubmit"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
<span
|
||||
v-if="store.state.serverConfiguration?.fileUpload"
|
||||
id="upload-tooltip"
|
||||
class="tooltipped tooltipped-w tooltipped-no-touch"
|
||||
aria-label="Upload file"
|
||||
@click="openFileUpload"
|
||||
>
|
||||
<input
|
||||
id="upload-input"
|
||||
ref="uploadInput"
|
||||
type="file"
|
||||
aria-labelledby="upload"
|
||||
multiple
|
||||
@change="onUploadInputChange"
|
||||
/>
|
||||
<button
|
||||
id="upload"
|
||||
type="button"
|
||||
aria-label="Upload file"
|
||||
:disabled="!store.state.isConnected"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
id="submit-tooltip"
|
||||
class="tooltipped tooltipped-w tooltipped-no-touch"
|
||||
aria-label="Send message"
|
||||
>
|
||||
<button
|
||||
id="submit"
|
||||
type="submit"
|
||||
aria-label="Send message"
|
||||
:disabled="!store.state.isConnected"
|
||||
/>
|
||||
</span>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Mousetrap from "mousetrap";
|
||||
import {wrapCursor} from "undate";
|
||||
import autocompletion from "../js/autocompletion";
|
||||
import {commands} from "../js/commands/index";
|
||||
import socket from "../js/socket";
|
||||
import upload from "../js/upload";
|
||||
import eventbus from "../js/eventbus";
|
||||
import {watch, defineComponent, nextTick, onMounted, PropType, ref, onUnmounted} from "vue";
|
||||
import type {ClientNetwork, ClientChan} from "../js/types";
|
||||
import {useStore} from "../js/store";
|
||||
import {ChanType} from "../../shared/types/chan";
|
||||
|
||||
const formattingHotkeys = {
|
||||
"mod+k": "\x03",
|
||||
"mod+b": "\x02",
|
||||
"mod+u": "\x1F",
|
||||
"mod+i": "\x1D",
|
||||
"mod+o": "\x0F",
|
||||
"mod+s": "\x1e",
|
||||
"mod+m": "\x11",
|
||||
};
|
||||
|
||||
// Autocomplete bracket and quote characters like in a modern IDE
|
||||
// For example, select `text`, press `[` key, and it becomes `[text]`
|
||||
const bracketWraps = {
|
||||
'"': '"',
|
||||
"'": "'",
|
||||
"(": ")",
|
||||
"<": ">",
|
||||
"[": "]",
|
||||
"{": "}",
|
||||
"*": "*",
|
||||
"`": "`",
|
||||
"~": "~",
|
||||
_: "_",
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: "ChatInput",
|
||||
props: {
|
||||
network: {type: Object as PropType<ClientNetwork>, required: true},
|
||||
channel: {type: Object as PropType<ClientChan>, required: true},
|
||||
},
|
||||
setup(props) {
|
||||
const store = useStore();
|
||||
const input = ref<HTMLTextAreaElement>();
|
||||
const uploadInput = ref<HTMLInputElement>();
|
||||
const autocompletionRef = ref<ReturnType<typeof autocompletion>>();
|
||||
|
||||
const setInputSize = () => {
|
||||
void nextTick(() => {
|
||||
if (!input.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const style = window.getComputedStyle(input.value);
|
||||
const lineHeight = parseFloat(style.lineHeight) || 1;
|
||||
|
||||
// Start by resetting height before computing as scrollHeight does not
|
||||
// decrease when deleting characters
|
||||
input.value.style.height = "";
|
||||
|
||||
// Use scrollHeight to calculate how many lines there are in input, and ceil the value
|
||||
// because some browsers tend to incorrently round the values when using high density
|
||||
// displays or using page zoom feature
|
||||
input.value.style.height = `${
|
||||
Math.ceil(input.value.scrollHeight / lineHeight) * lineHeight
|
||||
}px`;
|
||||
});
|
||||
};
|
||||
|
||||
const setPendingMessage = (e: Event) => {
|
||||
props.channel.pendingMessage = (e.target as HTMLInputElement).value;
|
||||
props.channel.inputHistoryPosition = 0;
|
||||
setInputSize();
|
||||
};
|
||||
|
||||
const getInputPlaceholder = (channel: ClientChan) => {
|
||||
if (channel.type === ChanType.CHANNEL || channel.type === ChanType.QUERY) {
|
||||
return `Write to ${channel.name}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
if (!input.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Triggering click event opens the virtual keyboard on mobile
|
||||
// This can only be called from another interactive event (e.g. button click)
|
||||
input.value.click();
|
||||
input.value.focus();
|
||||
|
||||
if (!store.state.isConnected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const target = props.channel.id;
|
||||
const text = props.channel.pendingMessage;
|
||||
|
||||
if (text.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (autocompletionRef.value) {
|
||||
autocompletionRef.value.hide();
|
||||
}
|
||||
|
||||
props.channel.inputHistoryPosition = 0;
|
||||
props.channel.pendingMessage = "";
|
||||
input.value.value = "";
|
||||
setInputSize();
|
||||
|
||||
// Store new message in history if last message isn't already equal
|
||||
if (props.channel.inputHistory[1] !== text) {
|
||||
props.channel.inputHistory.splice(1, 0, text);
|
||||
}
|
||||
|
||||
// Limit input history to a 100 entries
|
||||
if (props.channel.inputHistory.length > 100) {
|
||||
props.channel.inputHistory.pop();
|
||||
}
|
||||
|
||||
if (text[0] === "/") {
|
||||
const args = text.substring(1).split(" ");
|
||||
const cmd = args.shift()?.toLowerCase();
|
||||
|
||||
if (!cmd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(commands, cmd) && commands[cmd](args)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
socket.emit("input", {target, text});
|
||||
};
|
||||
|
||||
const onUploadInputChange = () => {
|
||||
if (!uploadInput.value || !uploadInput.value.files) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = Array.from(uploadInput.value.files);
|
||||
upload.triggerUpload(files);
|
||||
uploadInput.value.value = ""; // Reset <input> element so you can upload the same file
|
||||
};
|
||||
|
||||
const openFileUpload = () => {
|
||||
uploadInput.value?.click();
|
||||
};
|
||||
|
||||
const blurInput = () => {
|
||||
input.value?.blur();
|
||||
};
|
||||
|
||||
const onBlur = () => {
|
||||
if (autocompletionRef.value) {
|
||||
autocompletionRef.value.hide();
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.channel.id,
|
||||
() => {
|
||||
if (autocompletionRef.value) {
|
||||
autocompletionRef.value.hide();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.channel.pendingMessage,
|
||||
() => {
|
||||
setInputSize();
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
eventbus.on("escapekey", blurInput);
|
||||
|
||||
if (store.state.settings.autocomplete) {
|
||||
if (!input.value) {
|
||||
throw new Error("ChatInput autocomplete: input element is not available");
|
||||
}
|
||||
|
||||
autocompletionRef.value = autocompletion(input.value);
|
||||
}
|
||||
|
||||
const inputTrap = Mousetrap(input.value);
|
||||
|
||||
inputTrap.bind(Object.keys(formattingHotkeys), function (e, key) {
|
||||
const modifier = formattingHotkeys[key];
|
||||
|
||||
if (!e.target) {
|
||||
return;
|
||||
}
|
||||
|
||||
wrapCursor(
|
||||
e.target as HTMLTextAreaElement,
|
||||
modifier,
|
||||
(e.target as HTMLTextAreaElement).selectionStart ===
|
||||
(e.target as HTMLTextAreaElement).selectionEnd
|
||||
? ""
|
||||
: modifier
|
||||
);
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
inputTrap.bind(Object.keys(bracketWraps), function (e, key) {
|
||||
if (
|
||||
(e.target as HTMLTextAreaElement)?.selectionStart !==
|
||||
(e.target as HTMLTextAreaElement).selectionEnd
|
||||
) {
|
||||
wrapCursor(e.target as HTMLTextAreaElement, key, bracketWraps[key]);
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
inputTrap.bind(["up", "down"], (e, key) => {
|
||||
if (
|
||||
store.state.isAutoCompleting ||
|
||||
(e.target as HTMLTextAreaElement).selectionStart !==
|
||||
(e.target as HTMLTextAreaElement).selectionEnd ||
|
||||
!input.value
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onRow = (
|
||||
input.value.value.slice(undefined, input.value.selectionStart).match(/\n/g) ||
|
||||
[]
|
||||
).length;
|
||||
const totalRows = (input.value.value.match(/\n/g) || []).length;
|
||||
|
||||
const {channel} = props;
|
||||
|
||||
if (channel.inputHistoryPosition === 0) {
|
||||
channel.inputHistory[channel.inputHistoryPosition] = channel.pendingMessage;
|
||||
}
|
||||
|
||||
if (key === "up" && onRow === 0) {
|
||||
if (channel.inputHistoryPosition < channel.inputHistory.length - 1) {
|
||||
channel.inputHistoryPosition++;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else if (
|
||||
key === "down" &&
|
||||
channel.inputHistoryPosition > 0 &&
|
||||
onRow === totalRows
|
||||
) {
|
||||
channel.inputHistoryPosition--;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
channel.pendingMessage = channel.inputHistory[channel.inputHistoryPosition];
|
||||
input.value.value = channel.pendingMessage;
|
||||
setInputSize();
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (store.state.serverConfiguration?.fileUpload) {
|
||||
upload.mounted();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
eventbus.off("escapekey", blurInput);
|
||||
|
||||
if (autocompletionRef.value) {
|
||||
autocompletionRef.value.destroy();
|
||||
autocompletionRef.value = undefined;
|
||||
}
|
||||
|
||||
upload.unmounted();
|
||||
upload.abort();
|
||||
});
|
||||
|
||||
return {
|
||||
store,
|
||||
input,
|
||||
uploadInput,
|
||||
onUploadInputChange,
|
||||
openFileUpload,
|
||||
blurInput,
|
||||
onBlur,
|
||||
setInputSize,
|
||||
upload,
|
||||
getInputPlaceholder,
|
||||
onSubmit,
|
||||
setPendingMessage,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
255
client/components/ChatUserList.vue
Normal file
255
client/components/ChatUserList.vue
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
<template>
|
||||
<aside
|
||||
ref="userlist"
|
||||
class="userlist"
|
||||
:aria-label="'User list for ' + channel.name"
|
||||
@mouseleave="removeHoverUser"
|
||||
>
|
||||
<div class="count">
|
||||
<input
|
||||
ref="input"
|
||||
:value="userSearchInput"
|
||||
:placeholder="
|
||||
channel.users.length + ' user' + (channel.users.length === 1 ? '' : 's')
|
||||
"
|
||||
type="search"
|
||||
class="search"
|
||||
aria-label="Search among the user list"
|
||||
tabindex="-1"
|
||||
@input="setUserSearchInput"
|
||||
@keydown.up="navigateUserList($event, -1)"
|
||||
@keydown.down="navigateUserList($event, 1)"
|
||||
@keydown.page-up="navigateUserList($event, -10)"
|
||||
@keydown.page-down="navigateUserList($event, 10)"
|
||||
@keydown.enter="selectUser"
|
||||
/>
|
||||
</div>
|
||||
<div class="names">
|
||||
<div
|
||||
v-for="(users, mode) in groupedUsers"
|
||||
:key="mode"
|
||||
:class="['user-mode', getModeClass(String(mode))]"
|
||||
>
|
||||
<template v-if="userSearchInput.length > 0">
|
||||
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
|
||||
<Username
|
||||
v-for="user in users"
|
||||
:key="user.original.nick + '-search'"
|
||||
:on-hover="hoverUser"
|
||||
:active="user.original === activeUser"
|
||||
:user="user.original"
|
||||
v-html="user.string"
|
||||
/>
|
||||
<!-- eslint-enable -->
|
||||
</template>
|
||||
<template v-else>
|
||||
<Username
|
||||
v-for="user in users"
|
||||
:key="user.nick"
|
||||
:on-hover="hoverUser"
|
||||
:active="user === activeUser"
|
||||
:user="user"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {filter as fuzzyFilter} from "fuzzy";
|
||||
import {computed, defineComponent, nextTick, PropType, ref} from "vue";
|
||||
import type {UserInMessage} from "../../shared/types/msg";
|
||||
import type {ClientChan, ClientUser} from "../js/types";
|
||||
import Username from "./Username.vue";
|
||||
|
||||
const modes = {
|
||||
"~": "owner",
|
||||
"&": "admin",
|
||||
"!": "admin",
|
||||
"@": "op",
|
||||
"%": "half-op",
|
||||
"+": "voice",
|
||||
"": "normal",
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: "ChatUserList",
|
||||
components: {
|
||||
Username,
|
||||
},
|
||||
props: {
|
||||
channel: {type: Object as PropType<ClientChan>, required: true},
|
||||
},
|
||||
setup(props) {
|
||||
const userSearchInput = ref("");
|
||||
const activeUser = ref<UserInMessage | null>();
|
||||
const userlist = ref<HTMLDivElement>();
|
||||
const filteredUsers = computed(() => {
|
||||
if (!userSearchInput.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
return fuzzyFilter(userSearchInput.value, props.channel.users, {
|
||||
pre: "<b>",
|
||||
post: "</b>",
|
||||
extract: (u) => u.nick,
|
||||
});
|
||||
});
|
||||
|
||||
const groupedUsers = computed(() => {
|
||||
const groups = {};
|
||||
|
||||
if (userSearchInput.value && filteredUsers.value) {
|
||||
const result = filteredUsers.value;
|
||||
|
||||
for (const user of result) {
|
||||
const mode: string = user.original.modes[0] || "";
|
||||
|
||||
if (!groups[mode]) {
|
||||
groups[mode] = [];
|
||||
}
|
||||
|
||||
// Prepend user mode to search result
|
||||
user.string = mode + user.string;
|
||||
|
||||
groups[mode].push(user);
|
||||
}
|
||||
} else {
|
||||
for (const user of props.channel.users) {
|
||||
const mode = user.modes[0] || "";
|
||||
|
||||
if (!groups[mode]) {
|
||||
groups[mode] = [user];
|
||||
} else {
|
||||
groups[mode].push(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return groups as {
|
||||
[mode: string]: (ClientUser & {
|
||||
original: UserInMessage;
|
||||
string: string;
|
||||
})[];
|
||||
};
|
||||
});
|
||||
|
||||
const setUserSearchInput = (e: Event) => {
|
||||
userSearchInput.value = (e.target as HTMLInputElement).value;
|
||||
};
|
||||
|
||||
const getModeClass = (mode: string) => {
|
||||
return modes[mode] as typeof modes;
|
||||
};
|
||||
|
||||
const selectUser = () => {
|
||||
// Simulate a click on the active user to open the context menu.
|
||||
// Coordinates are provided to position the menu correctly.
|
||||
if (!activeUser.value || !userlist.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const el = userlist.value.querySelector(".active");
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
const ev = new MouseEvent("click", {
|
||||
view: window,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clientX: rect.left,
|
||||
clientY: rect.top + rect.height,
|
||||
});
|
||||
el.dispatchEvent(ev);
|
||||
};
|
||||
|
||||
const hoverUser = (user: UserInMessage) => {
|
||||
activeUser.value = user;
|
||||
};
|
||||
|
||||
const removeHoverUser = () => {
|
||||
activeUser.value = null;
|
||||
};
|
||||
|
||||
const scrollToActiveUser = () => {
|
||||
// Scroll the list if needed after the active class is applied
|
||||
void nextTick(() => {
|
||||
const el = userlist.value?.querySelector(".active");
|
||||
el?.scrollIntoView({block: "nearest", inline: "nearest"});
|
||||
});
|
||||
};
|
||||
|
||||
const navigateUserList = (event: Event, direction: number) => {
|
||||
// Prevent propagation to stop global keybind handler from capturing pagedown/pageup
|
||||
// and redirecting it to the message list container for scrolling
|
||||
event.stopImmediatePropagation();
|
||||
event.preventDefault();
|
||||
|
||||
let users = props.channel.users;
|
||||
|
||||
// Only using filteredUsers when we have to avoids filtering when it's not needed
|
||||
if (userSearchInput.value && filteredUsers.value) {
|
||||
users = filteredUsers.value.map((result) => result.original);
|
||||
}
|
||||
|
||||
// Bail out if there's no users to select
|
||||
if (!users.length) {
|
||||
activeUser.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const abort = () => {
|
||||
activeUser.value = direction ? users[0] : users[users.length - 1];
|
||||
scrollToActiveUser();
|
||||
};
|
||||
|
||||
// If there's no active user select the first or last one depending on direction
|
||||
if (!activeUser.value) {
|
||||
abort();
|
||||
return;
|
||||
}
|
||||
|
||||
let currentIndex = users.indexOf(activeUser.value as ClientUser);
|
||||
|
||||
if (currentIndex === -1) {
|
||||
abort();
|
||||
return;
|
||||
}
|
||||
|
||||
currentIndex += direction;
|
||||
|
||||
// Wrap around the list if necessary. Normaly each loop iterates once at most,
|
||||
// but might iterate more often if pgup or pgdown are used in a very short user list
|
||||
while (currentIndex < 0) {
|
||||
currentIndex += users.length;
|
||||
}
|
||||
|
||||
while (currentIndex > users.length - 1) {
|
||||
currentIndex -= users.length;
|
||||
}
|
||||
|
||||
activeUser.value = users[currentIndex];
|
||||
scrollToActiveUser();
|
||||
};
|
||||
|
||||
return {
|
||||
filteredUsers,
|
||||
groupedUsers,
|
||||
userSearchInput,
|
||||
activeUser,
|
||||
userlist,
|
||||
|
||||
setUserSearchInput,
|
||||
getModeClass,
|
||||
selectUser,
|
||||
hoverUser,
|
||||
removeHoverUser,
|
||||
navigateUserList,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
102
client/components/ConfirmDialog.vue
Normal file
102
client/components/ConfirmDialog.vue
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<template>
|
||||
<div id="confirm-dialog-overlay" :class="{opened: !!data}">
|
||||
<div v-if="data !== null" id="confirm-dialog">
|
||||
<div class="confirm-text">
|
||||
<div class="confirm-text-title">{{ data?.title }}</div>
|
||||
<p>{{ data?.text }}</p>
|
||||
</div>
|
||||
<div class="confirm-buttons">
|
||||
<button class="btn btn-cancel" @click="close(false)">Cancel</button>
|
||||
<button class="btn btn-danger" @click="close(true)">{{ data?.button }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#confirm-dialog {
|
||||
background: var(--body-bg-color);
|
||||
color: #fff;
|
||||
margin: 10px;
|
||||
border-radius: 5px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
#confirm-dialog .confirm-text {
|
||||
padding: 15px;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
#confirm-dialog .confirm-text-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#confirm-dialog .confirm-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 15px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#confirm-dialog .confirm-buttons .btn {
|
||||
margin-bottom: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#confirm-dialog .confirm-buttons .btn-cancel {
|
||||
border-color: transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import eventbus from "../js/eventbus";
|
||||
import {defineComponent, onMounted, onUnmounted, ref} from "vue";
|
||||
|
||||
type ConfirmDialogData = {
|
||||
title: string;
|
||||
text: string;
|
||||
button: string;
|
||||
};
|
||||
|
||||
type ConfirmDialogCallback = {
|
||||
(confirmed: boolean): void;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: "ConfirmDialog",
|
||||
setup() {
|
||||
const data = ref<ConfirmDialogData>();
|
||||
const callback = ref<ConfirmDialogCallback>();
|
||||
|
||||
const open = (incoming: ConfirmDialogData, cb: ConfirmDialogCallback) => {
|
||||
data.value = incoming;
|
||||
callback.value = cb;
|
||||
};
|
||||
|
||||
const close = (result: boolean) => {
|
||||
data.value = undefined;
|
||||
|
||||
if (callback.value) {
|
||||
callback.value(!!result);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
eventbus.on("escapekey", close);
|
||||
eventbus.on("confirm-dialog", open);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
eventbus.off("escapekey", close);
|
||||
eventbus.off("confirm-dialog", open);
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
close,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
286
client/components/ContextMenu.vue
Normal file
286
client/components/ContextMenu.vue
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
id="context-menu-container"
|
||||
:class="{passthrough}"
|
||||
@click="containerClick"
|
||||
@contextmenu.prevent="containerClick"
|
||||
@keydown.exact.up.prevent="navigateMenu(-1)"
|
||||
@keydown.exact.down.prevent="navigateMenu(1)"
|
||||
@keydown.exact.tab.prevent="navigateMenu(1)"
|
||||
@keydown.shift.tab.prevent="navigateMenu(-1)"
|
||||
>
|
||||
<ul
|
||||
id="context-menu"
|
||||
ref="contextMenu"
|
||||
role="menu"
|
||||
:style="{
|
||||
top: style.top + 'px',
|
||||
left: style.left + 'px',
|
||||
}"
|
||||
tabindex="-1"
|
||||
:aria-activedescendant="activeItem > -1 ? `context-menu-item-${activeItem}` : undefined"
|
||||
@mouseleave="activeItem = -1"
|
||||
@keydown.enter.prevent="clickActiveItem"
|
||||
>
|
||||
<!-- TODO: type -->
|
||||
<template v-for="(item, id) of (items as any)" :key="item.name">
|
||||
<li
|
||||
:id="`context-menu-item-${id}`"
|
||||
:class="[
|
||||
'context-menu-' + item.type,
|
||||
item.class ? 'context-menu-' + item.class : null,
|
||||
{active: id === activeItem},
|
||||
]"
|
||||
role="menuitem"
|
||||
@mouseenter="hoverItem(id)"
|
||||
@click="clickItem(item)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
generateUserContextMenu,
|
||||
generateChannelContextMenu,
|
||||
generateInlineChannelContextMenu,
|
||||
ContextMenuItem,
|
||||
} from "../js/helpers/contextMenu";
|
||||
import eventbus from "../js/eventbus";
|
||||
import {defineComponent, nextTick, onMounted, onUnmounted, PropType, ref} from "vue";
|
||||
import {ClientChan, ClientMessage, ClientNetwork, ClientUser} from "../js/types";
|
||||
import {useStore} from "../js/store";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ContextMenu",
|
||||
props: {
|
||||
message: {
|
||||
required: false,
|
||||
type: Object as PropType<ClientMessage>,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const router = useRouter();
|
||||
|
||||
const isOpen = ref(false);
|
||||
const passthrough = ref(false);
|
||||
|
||||
const contextMenu = ref<HTMLUListElement | null>();
|
||||
const previousActiveElement = ref<HTMLElement | null>();
|
||||
const items = ref<ContextMenuItem[]>([]);
|
||||
const activeItem = ref(-1);
|
||||
const style = ref({
|
||||
top: 0,
|
||||
left: 0,
|
||||
});
|
||||
|
||||
const close = () => {
|
||||
if (!isOpen.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
isOpen.value = false;
|
||||
items.value = [];
|
||||
|
||||
if (previousActiveElement.value) {
|
||||
previousActiveElement.value.focus();
|
||||
previousActiveElement.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const enablePointerEvents = () => {
|
||||
passthrough.value = false;
|
||||
document.body.removeEventListener("pointerup", enablePointerEvents);
|
||||
};
|
||||
|
||||
const containerClick = (event: MouseEvent) => {
|
||||
if (event.currentTarget === event.target) {
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
const positionContextMenu = (event: MouseEvent) => {
|
||||
const element = event.target as HTMLElement;
|
||||
|
||||
if (!contextMenu.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const menuWidth = contextMenu.value?.offsetWidth;
|
||||
const menuHeight = contextMenu.value?.offsetHeight;
|
||||
|
||||
if (element && element.classList.contains("menu")) {
|
||||
return {
|
||||
left: element.getBoundingClientRect().left - (menuWidth - element.offsetWidth),
|
||||
top: element.getBoundingClientRect().top + element.offsetHeight,
|
||||
};
|
||||
}
|
||||
|
||||
const offset = {left: event.pageX, top: event.pageY};
|
||||
|
||||
if (window.innerWidth - offset.left < menuWidth) {
|
||||
offset.left = window.innerWidth - menuWidth;
|
||||
}
|
||||
|
||||
if (window.innerHeight - offset.top < menuHeight) {
|
||||
offset.top = window.innerHeight - menuHeight;
|
||||
}
|
||||
|
||||
return offset;
|
||||
};
|
||||
|
||||
const hoverItem = (id: number) => {
|
||||
activeItem.value = id;
|
||||
};
|
||||
|
||||
const clickItem = (item: ContextMenuItem) => {
|
||||
close();
|
||||
|
||||
if ("action" in item && item.action) {
|
||||
item.action();
|
||||
} else if ("link" in item && item.link) {
|
||||
router.push(item.link).catch(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to navigate to", item.link);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const clickActiveItem = () => {
|
||||
if (items.value[activeItem.value]) {
|
||||
clickItem(items.value[activeItem.value]);
|
||||
}
|
||||
};
|
||||
|
||||
const open = (event: MouseEvent, newItems: ContextMenuItem[]) => {
|
||||
event.preventDefault();
|
||||
|
||||
previousActiveElement.value = document.activeElement as HTMLElement;
|
||||
items.value = newItems;
|
||||
activeItem.value = 0;
|
||||
isOpen.value = true;
|
||||
|
||||
// Position the menu and set the focus on the first item after it's size has updated
|
||||
nextTick(() => {
|
||||
const pos = positionContextMenu(event);
|
||||
|
||||
if (!pos) {
|
||||
return;
|
||||
}
|
||||
|
||||
style.value.left = pos.left;
|
||||
style.value.top = pos.top;
|
||||
contextMenu.value?.focus();
|
||||
}).catch((e) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
|
||||
const openChannelContextMenu = (data: {
|
||||
event: MouseEvent;
|
||||
channel: ClientChan;
|
||||
network: ClientNetwork;
|
||||
}) => {
|
||||
if (data.event.type === "contextmenu") {
|
||||
// Pass through all pointer events to allow the network list's
|
||||
// dragging events to continue triggering.
|
||||
passthrough.value = true;
|
||||
document.body.addEventListener("pointerup", enablePointerEvents, {
|
||||
passive: true,
|
||||
});
|
||||
}
|
||||
|
||||
const newItems = generateChannelContextMenu(data.channel, data.network);
|
||||
open(data.event, newItems);
|
||||
};
|
||||
|
||||
const openInlineChannelContextMenu = (data: {channel: string; event: MouseEvent}) => {
|
||||
const {network} = store.state.activeChannel;
|
||||
const newItems = generateInlineChannelContextMenu(store, data.channel, network);
|
||||
|
||||
open(data.event, newItems);
|
||||
};
|
||||
|
||||
const openUserContextMenu = (data: {
|
||||
user: Pick<ClientUser, "nick" | "modes">;
|
||||
event: MouseEvent;
|
||||
}) => {
|
||||
const {network, channel} = store.state.activeChannel;
|
||||
|
||||
const newItems = generateUserContextMenu(
|
||||
store,
|
||||
channel,
|
||||
network,
|
||||
channel.users.find((u) => u.nick === data.user.nick) || {
|
||||
nick: data.user.nick,
|
||||
modes: [],
|
||||
}
|
||||
);
|
||||
open(data.event, newItems);
|
||||
};
|
||||
|
||||
const navigateMenu = (direction: number) => {
|
||||
let currentIndex = activeItem.value;
|
||||
|
||||
currentIndex += direction;
|
||||
|
||||
const nextItem = items.value[currentIndex];
|
||||
|
||||
// If the next item we would select is a divider, skip over it
|
||||
if (nextItem && "type" in nextItem && nextItem.type === "divider") {
|
||||
currentIndex += direction;
|
||||
}
|
||||
|
||||
if (currentIndex < 0) {
|
||||
currentIndex += items.value.length;
|
||||
}
|
||||
|
||||
if (currentIndex > items.value.length - 1) {
|
||||
currentIndex -= items.value.length;
|
||||
}
|
||||
|
||||
activeItem.value = currentIndex;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
eventbus.on("escapekey", close);
|
||||
eventbus.on("contextmenu:cancel", close);
|
||||
eventbus.on("contextmenu:user", openUserContextMenu);
|
||||
eventbus.on("contextmenu:channel", openChannelContextMenu);
|
||||
eventbus.on("contextmenu:inline-channel", openInlineChannelContextMenu);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
eventbus.off("escapekey", close);
|
||||
eventbus.off("contextmenu:cancel", close);
|
||||
eventbus.off("contextmenu:user", openUserContextMenu);
|
||||
eventbus.off("contextmenu:channel", openChannelContextMenu);
|
||||
eventbus.off("contextmenu:inline-channel", openInlineChannelContextMenu);
|
||||
|
||||
close();
|
||||
});
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
items,
|
||||
activeItem,
|
||||
style,
|
||||
contextMenu,
|
||||
passthrough,
|
||||
close,
|
||||
containerClick,
|
||||
navigateMenu,
|
||||
hoverItem,
|
||||
clickItem,
|
||||
clickActiveItem,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
66
client/components/DateMarker.vue
Normal file
66
client/components/DateMarker.vue
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<template>
|
||||
<div :aria-label="localeDate" class="date-marker-container tooltipped tooltipped-s">
|
||||
<div class="date-marker">
|
||||
<span :aria-label="friendlyDate()" class="date-marker-text" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import dayjs from "dayjs";
|
||||
import calendar from "dayjs/plugin/calendar";
|
||||
import {computed, defineComponent, onBeforeUnmount, onMounted, PropType} from "vue";
|
||||
import eventbus from "../js/eventbus";
|
||||
import type {ClientMessage} from "../js/types";
|
||||
|
||||
dayjs.extend(calendar);
|
||||
|
||||
export default defineComponent({
|
||||
name: "DateMarker",
|
||||
props: {
|
||||
message: {
|
||||
type: Object as PropType<ClientMessage>,
|
||||
required: true,
|
||||
},
|
||||
focused: Boolean,
|
||||
},
|
||||
setup(props) {
|
||||
const localeDate = computed(() => dayjs(props.message.time).format("D MMMM YYYY"));
|
||||
|
||||
const hoursPassed = () => {
|
||||
return (Date.now() - Date.parse(props.message.time.toString())) / 3600000;
|
||||
};
|
||||
|
||||
const dayChange = () => {
|
||||
if (hoursPassed() >= 48) {
|
||||
eventbus.off("daychange", dayChange);
|
||||
}
|
||||
};
|
||||
|
||||
const friendlyDate = () => {
|
||||
// See http://momentjs.com/docs/#/displaying/calendar-time/
|
||||
return dayjs(props.message.time).calendar(null, {
|
||||
sameDay: "[Today]",
|
||||
lastDay: "[Yesterday]",
|
||||
lastWeek: "D MMMM YYYY",
|
||||
sameElse: "D MMMM YYYY",
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (hoursPassed() < 48) {
|
||||
eventbus.on("daychange", dayChange);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
eventbus.off("daychange", dayChange);
|
||||
});
|
||||
|
||||
return {
|
||||
localeDate,
|
||||
friendlyDate,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
120
client/components/Draggable.vue
Normal file
120
client/components/Draggable.vue
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
<template>
|
||||
<div ref="containerRef" :class="$props.class">
|
||||
<slot
|
||||
v-for="(item, index) of list"
|
||||
:key="item[itemKey]"
|
||||
:element="item"
|
||||
:index="index"
|
||||
name="item"
|
||||
></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, ref, PropType, watch, onUnmounted, onBeforeUnmount} from "vue";
|
||||
import Sortable from "sortablejs";
|
||||
|
||||
const Props = {
|
||||
delay: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
required: false,
|
||||
},
|
||||
delayOnTouchOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
touchStartThreshold: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
required: false,
|
||||
},
|
||||
handle: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
draggable: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
ghostClass: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
dragClass: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
group: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
class: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
itemKey: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: true,
|
||||
},
|
||||
list: {
|
||||
type: Array as PropType<any[]>,
|
||||
default: [],
|
||||
required: true,
|
||||
},
|
||||
filter: {
|
||||
type: String,
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: "Draggable",
|
||||
props: Props,
|
||||
emits: ["change", "choose", "unchoose"],
|
||||
setup(props, {emit}) {
|
||||
const containerRef = ref<HTMLElement | null>(null);
|
||||
const sortable = ref<Sortable | null>(null);
|
||||
|
||||
watch(containerRef, (newDraggable) => {
|
||||
if (newDraggable) {
|
||||
sortable.value = new Sortable(newDraggable, {
|
||||
...props,
|
||||
|
||||
onChoose(event) {
|
||||
emit("choose", event);
|
||||
},
|
||||
|
||||
onUnchoose(event) {
|
||||
emit("unchoose", event);
|
||||
},
|
||||
|
||||
onEnd(event) {
|
||||
emit("change", event);
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (sortable.value) {
|
||||
sortable.value.destroy();
|
||||
containerRef.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
containerRef,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
478
client/components/ImageViewer.vue
Normal file
478
client/components/ImageViewer.vue
Normal file
|
|
@ -0,0 +1,478 @@
|
|||
<template>
|
||||
<div
|
||||
id="image-viewer"
|
||||
ref="viewer"
|
||||
:class="{opened: link !== null}"
|
||||
@wheel="onMouseWheel"
|
||||
@touchstart.passive="onTouchStart"
|
||||
@click="onClick"
|
||||
>
|
||||
<template v-if="link !== null">
|
||||
<button class="close-btn" aria-label="Close"></button>
|
||||
|
||||
<button
|
||||
v-if="previousImage"
|
||||
class="previous-image-btn"
|
||||
aria-label="Previous image"
|
||||
@click.stop="previous"
|
||||
></button>
|
||||
<button
|
||||
v-if="nextImage"
|
||||
class="next-image-btn"
|
||||
aria-label="Next image"
|
||||
@click.stop="next"
|
||||
></button>
|
||||
|
||||
<a class="open-btn" :href="link.link" target="_blank" rel="noopener"></a>
|
||||
|
||||
<img
|
||||
ref="image"
|
||||
:src="link.thumb"
|
||||
alt=""
|
||||
:style="computeImageStyles"
|
||||
@load="onImageLoad"
|
||||
@mousedown="onImageMouseDown"
|
||||
@touchstart.passive="onImageTouchStart"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Mousetrap from "mousetrap";
|
||||
import {computed, defineComponent, ref, watch} from "vue";
|
||||
import eventbus from "../js/eventbus";
|
||||
import {ClientChan, ClientLinkPreview} from "../js/types";
|
||||
import {SharedMsg} from "../../shared/types/msg";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ImageViewer",
|
||||
setup() {
|
||||
const viewer = ref<HTMLDivElement>();
|
||||
const image = ref<HTMLImageElement>();
|
||||
|
||||
const link = ref<ClientLinkPreview | null>(null);
|
||||
const previousImage = ref<ClientLinkPreview | null>();
|
||||
const nextImage = ref<ClientLinkPreview | null>();
|
||||
const channel = ref<ClientChan | null>();
|
||||
|
||||
const position = ref<{
|
||||
x: number;
|
||||
y: number;
|
||||
}>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
|
||||
const transform = ref<{
|
||||
scale: number;
|
||||
x: number;
|
||||
y: number;
|
||||
}>({
|
||||
scale: 1,
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
|
||||
const computeImageStyles = computed(() => {
|
||||
// Sub pixels may cause the image to blur in certain browsers
|
||||
// round it down to prevent that
|
||||
const transformX = Math.floor(transform.value.x);
|
||||
const transformY = Math.floor(transform.value.y);
|
||||
|
||||
return {
|
||||
left: `${position.value.x}px`,
|
||||
top: `${position.value.y}px`,
|
||||
transform: `translate3d(${transformX}px, ${transformY}px, 0) scale3d(${transform.value.scale}, ${transform.value.scale}, 1)`,
|
||||
};
|
||||
});
|
||||
|
||||
const closeViewer = () => {
|
||||
if (link.value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
channel.value = null;
|
||||
previousImage.value = null;
|
||||
nextImage.value = null;
|
||||
link.value = null;
|
||||
};
|
||||
|
||||
const setPrevNextImages = () => {
|
||||
if (!channel.value || !link.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const links = channel.value.messages
|
||||
.map((msg: SharedMsg) => msg.previews)
|
||||
.flat()
|
||||
.filter((preview) => preview && preview.thumb);
|
||||
|
||||
const currentIndex = links.indexOf(link.value);
|
||||
|
||||
previousImage.value = links[currentIndex - 1] || null;
|
||||
nextImage.value = links[currentIndex + 1] || null;
|
||||
};
|
||||
|
||||
const previous = () => {
|
||||
if (previousImage.value) {
|
||||
link.value = previousImage.value;
|
||||
}
|
||||
};
|
||||
|
||||
const next = () => {
|
||||
if (nextImage.value) {
|
||||
link.value = nextImage.value;
|
||||
}
|
||||
};
|
||||
|
||||
const prepareImage = () => {
|
||||
const viewerEl = viewer.value;
|
||||
const imageEl = image.value;
|
||||
|
||||
if (!viewerEl || !imageEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const width = viewerEl.offsetWidth;
|
||||
const height = viewerEl.offsetHeight;
|
||||
const scale = Math.min(1, width / imageEl.width, height / imageEl.height);
|
||||
|
||||
position.value.x = Math.floor(-image.value!.naturalWidth / 2);
|
||||
position.value.y = Math.floor(-image.value!.naturalHeight / 2);
|
||||
transform.value.scale = Math.max(scale, 0.1);
|
||||
transform.value.x = width / 2;
|
||||
transform.value.y = height / 2;
|
||||
};
|
||||
|
||||
const onImageLoad = () => {
|
||||
prepareImage();
|
||||
};
|
||||
|
||||
const calculateZoomShift = (newScale: number, x: number, y: number, oldScale: number) => {
|
||||
if (!image.value || !viewer.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageWidth = image.value.width;
|
||||
const centerX = viewer.value.offsetWidth / 2;
|
||||
const centerY = viewer.value.offsetHeight / 2;
|
||||
|
||||
return {
|
||||
x:
|
||||
centerX -
|
||||
((centerX - (y - (imageWidth * x) / 2)) / x) * newScale +
|
||||
(imageWidth * newScale) / 2,
|
||||
y:
|
||||
centerY -
|
||||
((centerY - (oldScale - (imageWidth * x) / 2)) / x) * newScale +
|
||||
(imageWidth * newScale) / 2,
|
||||
};
|
||||
};
|
||||
|
||||
const correctPosition = () => {
|
||||
const imageEl = image.value;
|
||||
const viewerEl = viewer.value;
|
||||
|
||||
if (!imageEl || !viewerEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const widthScaled = imageEl.width * transform.value.scale;
|
||||
const heightScaled = imageEl.height * transform.value.scale;
|
||||
const containerWidth = viewerEl.offsetWidth;
|
||||
const containerHeight = viewerEl.offsetHeight;
|
||||
|
||||
if (widthScaled < containerWidth) {
|
||||
transform.value.x = containerWidth / 2;
|
||||
} else if (transform.value.x - widthScaled / 2 > 0) {
|
||||
transform.value.x = widthScaled / 2;
|
||||
} else if (transform.value.x + widthScaled / 2 < containerWidth) {
|
||||
transform.value.x = containerWidth - widthScaled / 2;
|
||||
}
|
||||
|
||||
if (heightScaled < containerHeight) {
|
||||
transform.value.y = containerHeight / 2;
|
||||
} else if (transform.value.y - heightScaled / 2 > 0) {
|
||||
transform.value.y = heightScaled / 2;
|
||||
} else if (transform.value.y + heightScaled / 2 < containerHeight) {
|
||||
transform.value.y = containerHeight - heightScaled / 2;
|
||||
}
|
||||
};
|
||||
|
||||
// Reduce multiple touch points into a single x/y/scale
|
||||
const reduceTouches = (touches: TouchList) => {
|
||||
let totalX = 0;
|
||||
let totalY = 0;
|
||||
let totalScale = 0;
|
||||
|
||||
for (let i = 0; i < touches.length; i++) {
|
||||
const x = touches[i].clientX;
|
||||
const y = touches[i].clientY;
|
||||
|
||||
totalX += x;
|
||||
totalY += y;
|
||||
|
||||
for (let i2 = 0; i2 < touches.length; i2++) {
|
||||
if (i !== i2) {
|
||||
const x2 = touches[i2].clientX;
|
||||
const y2 = touches[i2].clientY;
|
||||
totalScale += Math.sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (totalScale === 0) {
|
||||
totalScale = 1;
|
||||
}
|
||||
|
||||
return {
|
||||
x: totalX / touches.length,
|
||||
y: totalY / touches.length,
|
||||
scale: totalScale / touches.length,
|
||||
};
|
||||
};
|
||||
|
||||
const onTouchStart = (e: TouchEvent) => {
|
||||
// prevent sidebar touchstart event, we don't want to interact with sidebar while in image viewer
|
||||
e.stopImmediatePropagation();
|
||||
};
|
||||
|
||||
// Touch image manipulation:
|
||||
// 1. Move around by dragging it with one finger
|
||||
// 2. Change image scale by using two fingers
|
||||
const onImageTouchStart = (e: TouchEvent) => {
|
||||
const img = image.value;
|
||||
let touch = reduceTouches(e.touches);
|
||||
let currentTouches = e.touches;
|
||||
let touchEndFingers = 0;
|
||||
|
||||
const currentTransform = {
|
||||
x: touch.x,
|
||||
y: touch.y,
|
||||
scale: touch.scale,
|
||||
};
|
||||
|
||||
const startTransform = {
|
||||
x: transform.value.x,
|
||||
y: transform.value.y,
|
||||
scale: transform.value.scale,
|
||||
};
|
||||
|
||||
const touchMove = (moveEvent) => {
|
||||
touch = reduceTouches(moveEvent.touches);
|
||||
|
||||
if (currentTouches.length !== moveEvent.touches.length) {
|
||||
currentTransform.x = touch.x;
|
||||
currentTransform.y = touch.y;
|
||||
currentTransform.scale = touch.scale;
|
||||
startTransform.x = transform.value.x;
|
||||
startTransform.y = transform.value.y;
|
||||
startTransform.scale = transform.value.scale;
|
||||
}
|
||||
|
||||
const deltaX = touch.x - currentTransform.x;
|
||||
const deltaY = touch.y - currentTransform.y;
|
||||
const deltaScale = touch.scale / currentTransform.scale;
|
||||
currentTouches = moveEvent.touches;
|
||||
touchEndFingers = 0;
|
||||
|
||||
const newScale = Math.min(3, Math.max(0.1, startTransform.scale * deltaScale));
|
||||
|
||||
const fixedPosition = calculateZoomShift(
|
||||
newScale,
|
||||
startTransform.scale,
|
||||
startTransform.x,
|
||||
startTransform.y
|
||||
);
|
||||
|
||||
if (!fixedPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
transform.value.x = fixedPosition.x + deltaX;
|
||||
transform.value.y = fixedPosition.y + deltaY;
|
||||
transform.value.scale = newScale;
|
||||
correctPosition();
|
||||
};
|
||||
|
||||
const touchEnd = (endEvent: TouchEvent) => {
|
||||
const changedTouches = endEvent.changedTouches.length;
|
||||
|
||||
if (currentTouches.length > changedTouches + touchEndFingers) {
|
||||
touchEndFingers += changedTouches;
|
||||
return;
|
||||
}
|
||||
|
||||
// todo: this is swipe to close, but it's not working very well due to unfinished delta calculation
|
||||
/* if (
|
||||
transform.value.scale <= 1 &&
|
||||
endEvent.changedTouches[0].clientY - startTransform.y <= -70
|
||||
) {
|
||||
return this.closeViewer();
|
||||
}*/
|
||||
|
||||
correctPosition();
|
||||
|
||||
img?.removeEventListener("touchmove", touchMove);
|
||||
img?.removeEventListener("touchend", touchEnd);
|
||||
};
|
||||
|
||||
img?.addEventListener("touchmove", touchMove, {passive: true});
|
||||
img?.addEventListener("touchend", touchEnd, {passive: true});
|
||||
};
|
||||
|
||||
// Image mouse manipulation:
|
||||
// 1. Mouse wheel scrolling will zoom in and out
|
||||
// 2. If image is zoomed in, simply dragging it will move it around
|
||||
const onImageMouseDown = (e: MouseEvent) => {
|
||||
// todo: ignore if in touch event currently?
|
||||
|
||||
// only left mouse
|
||||
// TODO: e.buttons?
|
||||
if (e.which !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
const viewerEl = viewer.value;
|
||||
const imageEl = image.value;
|
||||
|
||||
if (!viewerEl || !imageEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startX = e.clientX;
|
||||
const startY = e.clientY;
|
||||
const startTransformX = transform.value.x;
|
||||
const startTransformY = transform.value.y;
|
||||
const widthScaled = imageEl.width * transform.value.scale;
|
||||
const heightScaled = imageEl.height * transform.value.scale;
|
||||
const containerWidth = viewerEl.offsetWidth;
|
||||
const containerHeight = viewerEl.offsetHeight;
|
||||
const centerX = transform.value.x - widthScaled / 2;
|
||||
const centerY = transform.value.y - heightScaled / 2;
|
||||
let movedDistance = 0;
|
||||
|
||||
const mouseMove = (moveEvent: MouseEvent) => {
|
||||
moveEvent.stopPropagation();
|
||||
moveEvent.preventDefault();
|
||||
|
||||
const newX = moveEvent.clientX - startX;
|
||||
const newY = moveEvent.clientY - startY;
|
||||
|
||||
movedDistance = Math.max(movedDistance, Math.abs(newX), Math.abs(newY));
|
||||
|
||||
if (centerX < 0 || widthScaled + centerX > containerWidth) {
|
||||
transform.value.x = startTransformX + newX;
|
||||
}
|
||||
|
||||
if (centerY < 0 || heightScaled + centerY > containerHeight) {
|
||||
transform.value.y = startTransformY + newY;
|
||||
}
|
||||
|
||||
correctPosition();
|
||||
};
|
||||
|
||||
const mouseUp = (upEvent: MouseEvent) => {
|
||||
correctPosition();
|
||||
|
||||
if (movedDistance < 2 && upEvent.button === 0) {
|
||||
closeViewer();
|
||||
}
|
||||
|
||||
image.value?.removeEventListener("mousemove", mouseMove);
|
||||
image.value?.removeEventListener("mouseup", mouseUp);
|
||||
};
|
||||
|
||||
image.value?.addEventListener("mousemove", mouseMove);
|
||||
image.value?.addEventListener("mouseup", mouseUp);
|
||||
};
|
||||
|
||||
// If image is zoomed in, holding ctrl while scrolling will move the image up and down
|
||||
const onMouseWheel = (e: WheelEvent) => {
|
||||
// if image viewer is closing (css animation), you can still trigger mousewheel
|
||||
// TODO: Figure out a better fix for this
|
||||
if (link.value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault(); // TODO: Can this be passive?
|
||||
|
||||
if (e.ctrlKey) {
|
||||
transform.value.y += e.deltaY;
|
||||
} else {
|
||||
const delta = e.deltaY > 0 ? 0.1 : -0.1;
|
||||
const newScale = Math.min(3, Math.max(0.1, transform.value.scale + delta));
|
||||
const fixedPosition = calculateZoomShift(
|
||||
newScale,
|
||||
transform.value.scale,
|
||||
transform.value.x,
|
||||
transform.value.y
|
||||
);
|
||||
|
||||
if (!fixedPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
transform.value.scale = newScale;
|
||||
transform.value.x = fixedPosition.x;
|
||||
transform.value.y = fixedPosition.y;
|
||||
}
|
||||
|
||||
correctPosition();
|
||||
};
|
||||
|
||||
const onClick = (e: Event) => {
|
||||
// If click triggers on the image, ignore it
|
||||
if (e.target === image.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
closeViewer();
|
||||
};
|
||||
|
||||
watch(link, (newLink, oldLink) => {
|
||||
// TODO: history.pushState
|
||||
if (newLink === null) {
|
||||
eventbus.off("escapekey", closeViewer);
|
||||
eventbus.off("resize", correctPosition);
|
||||
Mousetrap.unbind("left");
|
||||
Mousetrap.unbind("right");
|
||||
return;
|
||||
}
|
||||
|
||||
setPrevNextImages();
|
||||
|
||||
if (!oldLink) {
|
||||
eventbus.on("escapekey", closeViewer);
|
||||
eventbus.on("resize", correctPosition);
|
||||
Mousetrap.bind("left", previous);
|
||||
Mousetrap.bind("right", next);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
link,
|
||||
channel,
|
||||
image,
|
||||
transform,
|
||||
closeViewer,
|
||||
next,
|
||||
previous,
|
||||
onImageLoad,
|
||||
onImageMouseDown,
|
||||
onMouseWheel,
|
||||
onClick,
|
||||
onTouchStart,
|
||||
previousImage,
|
||||
nextImage,
|
||||
onImageTouchStart,
|
||||
computeImageStyles,
|
||||
viewer,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
35
client/components/InlineChannel.vue
Normal file
35
client/components/InlineChannel.vue
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<span
|
||||
class="inline-channel"
|
||||
dir="auto"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click.prevent="openContextMenu"
|
||||
@contextmenu.prevent="openContextMenu"
|
||||
><slot></slot
|
||||
></span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from "vue";
|
||||
import eventbus from "../js/eventbus";
|
||||
|
||||
export default defineComponent({
|
||||
name: "InlineChannel",
|
||||
props: {
|
||||
channel: String,
|
||||
},
|
||||
setup(props) {
|
||||
const openContextMenu = (event) => {
|
||||
eventbus.emit("contextmenu:inline-channel", {
|
||||
event: event,
|
||||
channel: props.channel,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
openContextMenu,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
93
client/components/JoinChannel.vue
Normal file
93
client/components/JoinChannel.vue
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<template>
|
||||
<form
|
||||
:id="'join-channel-' + channel.id"
|
||||
class="join-form"
|
||||
method="post"
|
||||
action=""
|
||||
autocomplete="off"
|
||||
@keydown.esc.prevent="$emit('toggle-join-channel')"
|
||||
@submit.prevent="onSubmit"
|
||||
>
|
||||
<input
|
||||
v-model="inputChannel"
|
||||
v-focus
|
||||
type="text"
|
||||
class="input"
|
||||
name="channel"
|
||||
placeholder="Channel"
|
||||
pattern="[^\s]+"
|
||||
maxlength="200"
|
||||
title="The channel name may not contain spaces"
|
||||
required
|
||||
/>
|
||||
<input
|
||||
v-model="inputPassword"
|
||||
type="password"
|
||||
class="input"
|
||||
name="key"
|
||||
placeholder="Password (optional)"
|
||||
pattern="[^\s]+"
|
||||
maxlength="200"
|
||||
title="The channel password may not contain spaces"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<button type="submit" class="btn btn-small">Join</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType, ref} from "vue";
|
||||
import {switchToChannel} from "../js/router";
|
||||
import socket from "../js/socket";
|
||||
import {useStore} from "../js/store";
|
||||
import {ClientNetwork, ClientChan} from "../js/types";
|
||||
|
||||
export default defineComponent({
|
||||
name: "JoinChannel",
|
||||
directives: {
|
||||
focus: {
|
||||
mounted: (el: HTMLFormElement) => el.focus(),
|
||||
},
|
||||
},
|
||||
props: {
|
||||
network: {type: Object as PropType<ClientNetwork>, required: true},
|
||||
channel: {type: Object as PropType<ClientChan>, required: true},
|
||||
},
|
||||
emits: ["toggle-join-channel"],
|
||||
setup(props, {emit}) {
|
||||
const store = useStore();
|
||||
const inputChannel = ref("");
|
||||
const inputPassword = ref("");
|
||||
|
||||
const onSubmit = () => {
|
||||
const existingChannel = store.getters.findChannelOnCurrentNetwork(inputChannel.value);
|
||||
|
||||
if (existingChannel) {
|
||||
switchToChannel(existingChannel);
|
||||
} else {
|
||||
const chanTypes = props.network.serverOptions.CHANTYPES;
|
||||
let channel = inputChannel.value;
|
||||
|
||||
if (chanTypes && chanTypes.length > 0 && !chanTypes.includes(channel[0])) {
|
||||
channel = chanTypes[0] + channel;
|
||||
}
|
||||
|
||||
socket.emit("input", {
|
||||
text: `/join ${channel} ${inputPassword.value}`,
|
||||
target: props.channel.id,
|
||||
});
|
||||
}
|
||||
|
||||
inputChannel.value = "";
|
||||
inputPassword.value = "";
|
||||
emit("toggle-join-channel");
|
||||
};
|
||||
|
||||
return {
|
||||
inputChannel,
|
||||
inputPassword,
|
||||
onSubmit,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
329
client/components/LinkPreview.vue
Normal file
329
client/components/LinkPreview.vue
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="link.shown"
|
||||
v-show="link.sourceLoaded || link.type === 'link'"
|
||||
ref="container"
|
||||
class="preview"
|
||||
dir="ltr"
|
||||
>
|
||||
<div
|
||||
ref="content"
|
||||
:class="['toggle-content', 'toggle-type-' + link.type, {opened: isContentShown}]"
|
||||
>
|
||||
<template v-if="link.type === 'link'">
|
||||
<a
|
||||
v-if="link.thumb"
|
||||
v-show="link.sourceLoaded"
|
||||
:href="link.link"
|
||||
class="toggle-thumbnail"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
@click="onThumbnailClick"
|
||||
>
|
||||
<img
|
||||
:src="link.thumb"
|
||||
decoding="async"
|
||||
alt=""
|
||||
class="thumb"
|
||||
@error="onThumbnailError"
|
||||
@abort="onThumbnailError"
|
||||
@load="onPreviewReady"
|
||||
/>
|
||||
</a>
|
||||
<div class="toggle-text" dir="auto">
|
||||
<div class="head">
|
||||
<div class="overflowable">
|
||||
<a
|
||||
:href="link.link"
|
||||
:title="link.head"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>{{ link.head }}</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="showMoreButton"
|
||||
:aria-expanded="isContentShown"
|
||||
:aria-label="moreButtonLabel"
|
||||
dir="auto"
|
||||
class="more"
|
||||
@click="onMoreClick"
|
||||
>
|
||||
<span class="more-caret" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="body overflowable">
|
||||
<a :href="link.link" :title="link.body" target="_blank" rel="noopener">{{
|
||||
link.body
|
||||
}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="link.type === 'image'">
|
||||
<a
|
||||
:href="link.link"
|
||||
class="toggle-thumbnail"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
@click="onThumbnailClick"
|
||||
>
|
||||
<img
|
||||
v-show="link.sourceLoaded"
|
||||
:src="link.thumb"
|
||||
decoding="async"
|
||||
alt=""
|
||||
@load="onPreviewReady"
|
||||
/>
|
||||
</a>
|
||||
</template>
|
||||
<template v-else-if="link.type === 'video'">
|
||||
<video
|
||||
v-show="link.sourceLoaded"
|
||||
preload="metadata"
|
||||
controls
|
||||
@canplay="onPreviewReady"
|
||||
>
|
||||
<source :src="link.media" :type="link.mediaType" />
|
||||
</video>
|
||||
</template>
|
||||
<template v-else-if="link.type === 'audio'">
|
||||
<audio
|
||||
v-show="link.sourceLoaded"
|
||||
controls
|
||||
preload="metadata"
|
||||
@canplay="onPreviewReady"
|
||||
>
|
||||
<source :src="link.media" :type="link.mediaType" />
|
||||
</audio>
|
||||
</template>
|
||||
<template v-else-if="link.type === 'error'">
|
||||
<em v-if="link.error === 'image-too-big'">
|
||||
This image is larger than {{ imageMaxSize }} and cannot be previewed.
|
||||
<a :href="link.link" target="_blank" rel="noopener">Click here</a>
|
||||
to open it in a new window.
|
||||
</em>
|
||||
<template v-else-if="link.error === 'message'">
|
||||
<div>
|
||||
<em>
|
||||
A preview could not be loaded.
|
||||
<a :href="link.link" target="_blank" rel="noopener">Click here</a>
|
||||
to open it in a new window.
|
||||
</em>
|
||||
<br />
|
||||
<pre class="prefetch-error">{{ link.message }}</pre>
|
||||
</div>
|
||||
|
||||
<button
|
||||
:aria-expanded="isContentShown"
|
||||
:aria-label="moreButtonLabel"
|
||||
class="more"
|
||||
@click="onMoreClick"
|
||||
>
|
||||
<span class="more-caret" />
|
||||
</button>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
inject,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
PropType,
|
||||
ref,
|
||||
watch,
|
||||
} from "vue";
|
||||
import {onBeforeRouteUpdate} from "vue-router";
|
||||
import eventbus from "../js/eventbus";
|
||||
import friendlysize from "../js/helpers/friendlysize";
|
||||
import {useStore} from "../js/store";
|
||||
import type {ClientChan, ClientLinkPreview} from "../js/types";
|
||||
import {imageViewerKey} from "./App.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "LinkPreview",
|
||||
props: {
|
||||
link: {
|
||||
type: Object as PropType<ClientLinkPreview>,
|
||||
required: true,
|
||||
},
|
||||
keepScrollPosition: {
|
||||
type: Function as PropType<() => void>,
|
||||
required: true,
|
||||
},
|
||||
channel: {type: Object as PropType<ClientChan>, required: true},
|
||||
},
|
||||
setup(props) {
|
||||
const store = useStore();
|
||||
|
||||
const showMoreButton = ref(false);
|
||||
const isContentShown = ref(false);
|
||||
const imageViewer = inject(imageViewerKey);
|
||||
|
||||
onBeforeRouteUpdate((to, from, next) => {
|
||||
// cancel the navigation if the user is trying to close the image viewer
|
||||
if (imageViewer?.value?.link) {
|
||||
imageViewer.value.closeViewer();
|
||||
return next(false);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
const content = ref<HTMLDivElement | null>(null);
|
||||
const container = ref<HTMLDivElement | null>(null);
|
||||
|
||||
const moreButtonLabel = computed(() => {
|
||||
return isContentShown.value ? "Less" : "More";
|
||||
});
|
||||
|
||||
const imageMaxSize = computed(() => {
|
||||
if (!props.link.maxSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
return friendlysize(props.link.maxSize);
|
||||
});
|
||||
|
||||
const handleResize = () => {
|
||||
nextTick(() => {
|
||||
if (!content.value || !container.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
showMoreButton.value = content.value.offsetWidth >= container.value.offsetWidth;
|
||||
}).catch((e) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error in LinkPreview.handleResize", e);
|
||||
});
|
||||
};
|
||||
|
||||
const onPreviewReady = () => {
|
||||
props.link.sourceLoaded = true;
|
||||
|
||||
props.keepScrollPosition();
|
||||
|
||||
if (props.link.type === "link") {
|
||||
handleResize();
|
||||
}
|
||||
};
|
||||
|
||||
const onPreviewUpdate = () => {
|
||||
// Don't display previews while they are loading on the server
|
||||
if (props.link.type === "loading") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Error does not have any media to render
|
||||
if (props.link.type === "error") {
|
||||
onPreviewReady();
|
||||
}
|
||||
|
||||
// If link doesn't have a thumbnail, render it
|
||||
if (props.link.type === "link") {
|
||||
handleResize();
|
||||
props.keepScrollPosition();
|
||||
}
|
||||
};
|
||||
|
||||
const onThumbnailError = () => {
|
||||
// If thumbnail fails to load, hide it and show the preview without it
|
||||
props.link.thumb = "";
|
||||
onPreviewReady();
|
||||
};
|
||||
|
||||
const onThumbnailClick = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!imageViewer?.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
imageViewer.value.channel = props.channel;
|
||||
imageViewer.value.link = props.link;
|
||||
};
|
||||
|
||||
const onMoreClick = () => {
|
||||
isContentShown.value = !isContentShown.value;
|
||||
props.keepScrollPosition();
|
||||
};
|
||||
|
||||
const updateShownState = () => {
|
||||
// User has manually toggled the preview, do not apply default
|
||||
if (props.link.shown !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let defaultState = false;
|
||||
|
||||
switch (props.link.type) {
|
||||
case "error":
|
||||
// Collapse all errors by default unless its a message about image being too big
|
||||
if (props.link.error === "image-too-big") {
|
||||
defaultState = store.state.settings.media;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "link":
|
||||
defaultState = store.state.settings.links;
|
||||
break;
|
||||
|
||||
default:
|
||||
defaultState = store.state.settings.media;
|
||||
}
|
||||
|
||||
props.link.shown = defaultState;
|
||||
};
|
||||
|
||||
updateShownState();
|
||||
|
||||
watch(
|
||||
() => props.link.type,
|
||||
() => {
|
||||
updateShownState();
|
||||
onPreviewUpdate();
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
eventbus.on("resize", handleResize);
|
||||
|
||||
onPreviewUpdate();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
eventbus.off("resize", handleResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// Let this preview go through load/canplay events again,
|
||||
// Otherwise the browser can cause a resize on video elements
|
||||
props.link.sourceLoaded = false;
|
||||
});
|
||||
|
||||
return {
|
||||
moreButtonLabel,
|
||||
imageMaxSize,
|
||||
onThumbnailClick,
|
||||
onThumbnailError,
|
||||
onMoreClick,
|
||||
onPreviewReady,
|
||||
onPreviewUpdate,
|
||||
showMoreButton,
|
||||
isContentShown,
|
||||
content,
|
||||
container,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
22
client/components/LinkPreviewFileSize.vue
Normal file
22
client/components/LinkPreviewFileSize.vue
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<template>
|
||||
<span class="preview-size">({{ previewSize }})</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from "vue";
|
||||
import friendlysize from "../js/helpers/friendlysize";
|
||||
|
||||
export default defineComponent({
|
||||
name: "LinkPreviewFileSize",
|
||||
props: {
|
||||
size: {type: Number, required: true},
|
||||
},
|
||||
setup(props) {
|
||||
const previewSize = friendlysize(props.size);
|
||||
|
||||
return {
|
||||
previewSize,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
37
client/components/LinkPreviewToggle.vue
Normal file
37
client/components/LinkPreviewToggle.vue
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<button
|
||||
v-if="link.type !== 'loading'"
|
||||
:class="['toggle-button', 'toggle-preview', {opened: link.shown}]"
|
||||
:aria-label="ariaLabel"
|
||||
@click="onClick"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, PropType} from "vue";
|
||||
import {ClientMessage, ClientLinkPreview} from "../js/types";
|
||||
|
||||
export default defineComponent({
|
||||
name: "LinkPreviewToggle",
|
||||
props: {
|
||||
link: {type: Object as PropType<ClientLinkPreview>, required: true},
|
||||
message: {type: Object as PropType<ClientMessage>, required: true},
|
||||
},
|
||||
emits: ["toggle-link-preview"],
|
||||
setup(props, {emit}) {
|
||||
const ariaLabel = computed(() => {
|
||||
return props.link.shown ? "Collapse preview" : "Expand preview";
|
||||
});
|
||||
|
||||
const onClick = () => {
|
||||
props.link.shown = !props.link.shown;
|
||||
emit("toggle-link-preview", props.link, props.message);
|
||||
};
|
||||
|
||||
return {
|
||||
ariaLabel,
|
||||
onClick,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
247
client/components/Mentions.vue
Normal file
247
client/components/Mentions.vue
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
id="mentions-popup-container"
|
||||
@click="containerClick"
|
||||
@contextmenu="containerClick"
|
||||
>
|
||||
<div class="mentions-popup">
|
||||
<div class="mentions-popup-title">
|
||||
Recent mentions
|
||||
<button
|
||||
v-if="resolvedMessages.length"
|
||||
class="btn dismiss-all-mentions"
|
||||
@click="dismissAllMentions()"
|
||||
>
|
||||
Dismiss all
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="resolvedMessages.length === 0">
|
||||
<p v-if="isLoading">Loading…</p>
|
||||
<p v-else>You have no recent mentions.</p>
|
||||
</template>
|
||||
<template v-for="message in resolvedMessages" v-else :key="message.msgId">
|
||||
<div :class="['msg', message.type]">
|
||||
<div class="mentions-info">
|
||||
<div>
|
||||
<span class="from">
|
||||
<Username :user="(message.from as any)" />
|
||||
<template v-if="message.channel">
|
||||
in {{ message.channel.channel.name }} on
|
||||
{{ message.channel.network.name }}
|
||||
</template>
|
||||
<template v-else> in unknown channel </template> </span
|
||||
>{{ ` ` }}
|
||||
<span :title="message.localetime" class="time">
|
||||
{{ messageTime(message.time.toString()) }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
class="close-tooltip tooltipped tooltipped-w"
|
||||
aria-label="Dismiss this mention"
|
||||
>
|
||||
<button
|
||||
class="msg-dismiss"
|
||||
aria-label="Dismiss this mention"
|
||||
@click="dismissMention(message)"
|
||||
></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content" dir="auto">
|
||||
<ParsedMessage :message="(message as any)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#mentions-popup-container {
|
||||
z-index: 8;
|
||||
}
|
||||
|
||||
.mentions-popup {
|
||||
background-color: var(--window-bg-color);
|
||||
position: absolute;
|
||||
width: 400px;
|
||||
right: 80px;
|
||||
top: 55px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
z-index: 2;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.mentions-popup > .mentions-popup-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.mentions-popup .mentions-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.mentions-popup .msg {
|
||||
margin-bottom: 15px;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.mentions-popup .msg:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.mentions-popup .msg .content {
|
||||
background-color: var(--highlight-bg-color);
|
||||
border-radius: 5px;
|
||||
padding: 6px;
|
||||
margin-top: 2px;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word; /* Webkit-specific */
|
||||
}
|
||||
|
||||
.mentions-popup .msg-dismiss::before {
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
display: inline-block;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
content: "×";
|
||||
}
|
||||
|
||||
.mentions-popup .msg-dismiss:hover {
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
.mentions-popup .dismiss-all-mentions {
|
||||
margin: 0;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
@media (min-height: 500px) {
|
||||
.mentions-popup {
|
||||
max-height: 60vh;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mentions-popup {
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
width: 100%;
|
||||
max-height: none;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
top: 45px; /* header height */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import Username from "./Username.vue";
|
||||
import ParsedMessage from "./ParsedMessage.vue";
|
||||
import socket from "../js/socket";
|
||||
import eventbus from "../js/eventbus";
|
||||
import localetime from "../js/helpers/localetime";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import {computed, watch, defineComponent, ref, onMounted, onUnmounted} from "vue";
|
||||
import {useStore} from "../js/store";
|
||||
import {ClientMention} from "../js/types";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export default defineComponent({
|
||||
name: "Mentions",
|
||||
components: {
|
||||
Username,
|
||||
ParsedMessage,
|
||||
},
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const isOpen = ref(false);
|
||||
const isLoading = ref(false);
|
||||
const resolvedMessages = computed(() => {
|
||||
const messages = store.state.mentions.slice().reverse();
|
||||
|
||||
for (const message of messages) {
|
||||
message.localetime = localetime(message.time);
|
||||
message.channel = store.getters.findChannel(message.chanId);
|
||||
}
|
||||
|
||||
return messages.filter((message) => !message.channel?.channel.muted);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => store.state.mentions,
|
||||
() => {
|
||||
isLoading.value = false;
|
||||
}
|
||||
);
|
||||
|
||||
const messageTime = (time: string) => {
|
||||
return dayjs(time).fromNow();
|
||||
};
|
||||
|
||||
const dismissMention = (message: ClientMention) => {
|
||||
store.state.mentions.splice(
|
||||
store.state.mentions.findIndex((m) => m.msgId === message.msgId),
|
||||
1
|
||||
);
|
||||
|
||||
socket.emit("mentions:dismiss", message.msgId);
|
||||
};
|
||||
|
||||
const dismissAllMentions = () => {
|
||||
store.state.mentions = [];
|
||||
socket.emit("mentions:dismiss_all");
|
||||
};
|
||||
|
||||
const containerClick = (event: Event) => {
|
||||
if (event.currentTarget === event.target) {
|
||||
isOpen.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const togglePopup = () => {
|
||||
isOpen.value = !isOpen.value;
|
||||
|
||||
if (isOpen.value) {
|
||||
isLoading.value = true;
|
||||
socket.emit("mentions:get");
|
||||
}
|
||||
};
|
||||
|
||||
const closePopup = () => {
|
||||
isOpen.value = false;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
eventbus.on("mentions:toggle", togglePopup);
|
||||
eventbus.on("escapekey", closePopup);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
eventbus.off("mentions:toggle", togglePopup);
|
||||
eventbus.off("escapekey", closePopup);
|
||||
});
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
isLoading,
|
||||
resolvedMessages,
|
||||
messageTime,
|
||||
dismissMention,
|
||||
dismissAllMentions,
|
||||
containerClick,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
173
client/components/Message.vue
Normal file
173
client/components/Message.vue
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
<template>
|
||||
<div
|
||||
:id="'msg-' + message.id"
|
||||
:class="[
|
||||
'msg',
|
||||
{
|
||||
self: message.self,
|
||||
highlight: message.highlight || focused,
|
||||
'previous-source': isPreviousSource,
|
||||
},
|
||||
]"
|
||||
:data-type="message.type"
|
||||
:data-command="message.command"
|
||||
:data-from="message.from && message.from.nick"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
:aria-label="messageTimeLocale"
|
||||
class="time tooltipped tooltipped-e"
|
||||
>{{ `${messageTime} ` }}
|
||||
</span>
|
||||
<template v-if="message.type === 'unhandled'">
|
||||
<span class="from">[{{ message.command }}]</span>
|
||||
<span class="content">
|
||||
<span v-for="(param, id) in message.params" :key="id">{{
|
||||
` ${param} `
|
||||
}}</span>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="isAction()">
|
||||
<span class="from"><span class="only-copy" aria-hidden="true">*** </span></span>
|
||||
<component :is="messageComponent" :network="network" :message="message" />
|
||||
</template>
|
||||
<template v-else-if="message.type === 'action'">
|
||||
<span class="from"><span class="only-copy">* </span></span>
|
||||
<span class="content" dir="auto">
|
||||
<Username
|
||||
:user="message.from"
|
||||
:network="network"
|
||||
:channel="channel"
|
||||
dir="auto"
|
||||
/> <ParsedMessage :message="message" />
|
||||
<LinkPreview
|
||||
v-for="preview in message.previews"
|
||||
:key="preview.link"
|
||||
:keep-scroll-position="keepScrollPosition"
|
||||
:link="preview"
|
||||
:channel="channel"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="message.type === 'message'" class="from">
|
||||
<template v-if="message.from && message.from.nick">
|
||||
<span class="only-copy" aria-hidden="true"><</span>
|
||||
<Username :user="message.from" :network="network" :channel="channel" />
|
||||
<span class="only-copy" aria-hidden="true">> </span>
|
||||
</template>
|
||||
</span>
|
||||
<span v-else-if="message.type === 'plugin'" class="from">
|
||||
<template v-if="message.from && message.from.nick">
|
||||
<span class="only-copy" aria-hidden="true">[</span>
|
||||
{{ message.from.nick }}
|
||||
<span class="only-copy" aria-hidden="true">] </span>
|
||||
</template>
|
||||
</span>
|
||||
<span v-else class="from">
|
||||
<template v-if="message.from && message.from.nick">
|
||||
<span class="only-copy" aria-hidden="true">-</span>
|
||||
<Username :user="message.from" :network="network" :channel="channel" />
|
||||
<span class="only-copy" aria-hidden="true">- </span>
|
||||
</template>
|
||||
</span>
|
||||
<span class="content" dir="auto">
|
||||
<span
|
||||
v-if="message.showInActive"
|
||||
aria-label="This message was shown in your active channel"
|
||||
class="msg-shown-in-active tooltipped tooltipped-e"
|
||||
><span></span
|
||||
></span>
|
||||
<span
|
||||
v-if="message.statusmsgGroup"
|
||||
:aria-label="`This message was only shown to users with ${message.statusmsgGroup} mode`"
|
||||
class="msg-statusmsg tooltipped tooltipped-e"
|
||||
><span>{{ message.statusmsgGroup }}</span></span
|
||||
>
|
||||
<ParsedMessage :network="network" :message="message" />
|
||||
<LinkPreview
|
||||
v-for="preview in message.previews"
|
||||
:key="preview.link"
|
||||
:keep-scroll-position="keepScrollPosition"
|
||||
:link="preview"
|
||||
:channel="channel"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, PropType} from "vue";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import constants from "../js/constants";
|
||||
import localetime from "../js/helpers/localetime";
|
||||
import Username from "./Username.vue";
|
||||
import LinkPreview from "./LinkPreview.vue";
|
||||
import ParsedMessage from "./ParsedMessage.vue";
|
||||
import MessageTypes from "./MessageTypes";
|
||||
|
||||
import type {ClientChan, ClientMessage, ClientNetwork} from "../js/types";
|
||||
import {useStore} from "../js/store";
|
||||
|
||||
MessageTypes.ParsedMessage = ParsedMessage;
|
||||
MessageTypes.LinkPreview = LinkPreview;
|
||||
MessageTypes.Username = Username;
|
||||
|
||||
export default defineComponent({
|
||||
name: "Message",
|
||||
components: MessageTypes,
|
||||
props: {
|
||||
message: {type: Object as PropType<ClientMessage>, required: true},
|
||||
channel: {type: Object as PropType<ClientChan>, required: false},
|
||||
network: {type: Object as PropType<ClientNetwork>, required: true},
|
||||
keepScrollPosition: Function as PropType<() => void>,
|
||||
isPreviousSource: Boolean,
|
||||
focused: Boolean,
|
||||
},
|
||||
setup(props) {
|
||||
const store = useStore();
|
||||
|
||||
const timeFormat = computed(() => {
|
||||
let format: keyof typeof constants.timeFormats;
|
||||
|
||||
if (store.state.settings.use12hClock) {
|
||||
format = store.state.settings.showSeconds ? "msg12hWithSeconds" : "msg12h";
|
||||
} else {
|
||||
format = store.state.settings.showSeconds ? "msgWithSeconds" : "msgDefault";
|
||||
}
|
||||
|
||||
return constants.timeFormats[format];
|
||||
});
|
||||
|
||||
const messageTime = computed(() => {
|
||||
return dayjs(props.message.time).format(timeFormat.value);
|
||||
});
|
||||
|
||||
const messageTimeLocale = computed(() => {
|
||||
return localetime(props.message.time);
|
||||
});
|
||||
|
||||
const messageComponent = computed(() => {
|
||||
return "message-" + (props.message.type || "invalid"); // TODO: force existence of type in sharedmsg
|
||||
});
|
||||
|
||||
const isAction = () => {
|
||||
if (!props.message.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return typeof MessageTypes["message-" + props.message.type] !== "undefined";
|
||||
};
|
||||
|
||||
return {
|
||||
timeFormat,
|
||||
messageTime,
|
||||
messageTimeLocale,
|
||||
messageComponent,
|
||||
isAction,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
165
client/components/MessageCondensed.vue
Normal file
165
client/components/MessageCondensed.vue
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
<template>
|
||||
<div :class="['msg', {closed: isCollapsed}]" data-type="condensed">
|
||||
<div class="condensed-summary">
|
||||
<span class="time" />
|
||||
<span class="from" />
|
||||
<span class="content" @click="onCollapseClick"
|
||||
>{{ condensedText
|
||||
}}<button class="toggle-button" aria-label="Toggle status messages"
|
||||
/></span>
|
||||
</div>
|
||||
<Message
|
||||
v-for="message in messages"
|
||||
:key="message.id"
|
||||
:network="network"
|
||||
:message="message"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, PropType, ref} from "vue";
|
||||
import {condensedTypes} from "../../shared/irc";
|
||||
import {MessageType} from "../../shared/types/msg";
|
||||
import {ClientMessage, ClientNetwork} from "../js/types";
|
||||
import Message from "./Message.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageCondensed",
|
||||
components: {
|
||||
Message,
|
||||
},
|
||||
props: {
|
||||
network: {type: Object as PropType<ClientNetwork>, required: true},
|
||||
messages: {
|
||||
type: Array as PropType<ClientMessage[]>,
|
||||
required: true,
|
||||
},
|
||||
keepScrollPosition: {
|
||||
type: Function as PropType<() => void>,
|
||||
required: true,
|
||||
},
|
||||
focused: Boolean,
|
||||
},
|
||||
setup(props) {
|
||||
const isCollapsed = ref(true);
|
||||
|
||||
const onCollapseClick = () => {
|
||||
isCollapsed.value = !isCollapsed.value;
|
||||
props.keepScrollPosition();
|
||||
};
|
||||
|
||||
const condensedText = computed(() => {
|
||||
const obj: Record<string, number> = {};
|
||||
|
||||
condensedTypes.forEach((type) => {
|
||||
obj[type] = 0;
|
||||
});
|
||||
|
||||
for (const message of props.messages) {
|
||||
// special case since one MODE message can change multiple modes
|
||||
if (message.type === MessageType.MODE) {
|
||||
// syntax: +vv-t maybe-some targets
|
||||
// we want the number of mode changes in the message, so count the
|
||||
// number of chars other than + and - before the first space
|
||||
const text = message.text ? message.text : "";
|
||||
const modeChangesCount = text
|
||||
.split(" ")[0]
|
||||
.split("")
|
||||
.filter((char) => char !== "+" && char !== "-").length;
|
||||
obj[message.type] += modeChangesCount;
|
||||
} else {
|
||||
if (!message.type) {
|
||||
/* eslint-disable no-console */
|
||||
console.log(`empty message type, this should not happen: ${message.id}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
obj[message.type]++;
|
||||
}
|
||||
}
|
||||
|
||||
// Count quits as parts in condensed messages to reduce information density
|
||||
obj.part += obj.quit;
|
||||
|
||||
const strings: string[] = [];
|
||||
condensedTypes.forEach((type) => {
|
||||
if (obj[type]) {
|
||||
switch (type) {
|
||||
case "chghost":
|
||||
strings.push(
|
||||
String(obj[type]) +
|
||||
(obj[type] > 1
|
||||
? " users have changed hostname"
|
||||
: " user has changed hostname")
|
||||
);
|
||||
break;
|
||||
case "join":
|
||||
strings.push(
|
||||
String(obj[type]) +
|
||||
(obj[type] > 1 ? " users have joined" : " user has joined")
|
||||
);
|
||||
break;
|
||||
case "part":
|
||||
strings.push(
|
||||
String(obj[type]) +
|
||||
(obj[type] > 1 ? " users have left" : " user has left")
|
||||
);
|
||||
break;
|
||||
case "nick":
|
||||
strings.push(
|
||||
String(obj[type]) +
|
||||
(obj[type] > 1
|
||||
? " users have changed nick"
|
||||
: " user has changed nick")
|
||||
);
|
||||
break;
|
||||
case "kick":
|
||||
strings.push(
|
||||
String(obj[type]) +
|
||||
(obj[type] > 1 ? " users were kicked" : " user was kicked")
|
||||
);
|
||||
break;
|
||||
case "mode":
|
||||
strings.push(
|
||||
String(obj[type]) +
|
||||
(obj[type] > 1 ? " modes were set" : " mode was set")
|
||||
);
|
||||
break;
|
||||
case "away":
|
||||
strings.push(
|
||||
"marked away " +
|
||||
(obj[type] > 1 ? String(obj[type]) + " times" : "once")
|
||||
);
|
||||
break;
|
||||
case "back":
|
||||
strings.push(
|
||||
"marked back " +
|
||||
(obj[type] > 1 ? String(obj[type]) + " times" : "once")
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (strings.length) {
|
||||
let text = strings.pop();
|
||||
|
||||
if (strings.length) {
|
||||
text = strings.join(", ") + ", and " + text!;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
return "";
|
||||
});
|
||||
|
||||
return {
|
||||
isCollapsed,
|
||||
condensedText,
|
||||
onCollapseClick,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
440
client/components/MessageList.vue
Normal file
440
client/components/MessageList.vue
Normal file
|
|
@ -0,0 +1,440 @@
|
|||
<template>
|
||||
<div ref="chat" class="chat" tabindex="-1">
|
||||
<div v-show="channel.moreHistoryAvailable" class="show-more">
|
||||
<button
|
||||
ref="loadMoreButton"
|
||||
:disabled="channel.historyLoading || !store.state.isConnected"
|
||||
class="btn"
|
||||
@click="onShowMoreClick"
|
||||
>
|
||||
<span v-if="channel.historyLoading">Loading…</span>
|
||||
<span v-else>Show older messages</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="messages"
|
||||
role="log"
|
||||
aria-live="polite"
|
||||
aria-relevant="additions"
|
||||
@copy="onCopy"
|
||||
>
|
||||
<template v-for="(message, id) in condensedMessages">
|
||||
<DateMarker
|
||||
v-if="shouldDisplayDateMarker(message, id)"
|
||||
:key="message.id + '-date'"
|
||||
:message="message as any"
|
||||
:focused="message.id === focused"
|
||||
/>
|
||||
<div
|
||||
v-if="shouldDisplayUnreadMarker(Number(message.id))"
|
||||
:key="message.id + '-unread'"
|
||||
class="unread-marker"
|
||||
>
|
||||
<span class="unread-marker-text" />
|
||||
</div>
|
||||
|
||||
<MessageCondensed
|
||||
v-if="message.type === 'condensed'"
|
||||
:key="message.messages[0].id"
|
||||
:network="network"
|
||||
:keep-scroll-position="keepScrollPosition"
|
||||
:messages="message.messages"
|
||||
:focused="message.id === focused"
|
||||
/>
|
||||
<Message
|
||||
v-else
|
||||
:key="message.id"
|
||||
:channel="channel"
|
||||
:network="network"
|
||||
:message="message"
|
||||
:keep-scroll-position="keepScrollPosition"
|
||||
:is-previous-source="isPreviousSource(message, id)"
|
||||
:focused="message.id === focused"
|
||||
@toggle-link-preview="onLinkPreviewToggle"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {condensedTypes} from "../../shared/irc";
|
||||
import {ChanType} from "../../shared/types/chan";
|
||||
import {MessageType, SharedMsg} from "../../shared/types/msg";
|
||||
import eventbus from "../js/eventbus";
|
||||
import clipboard from "../js/clipboard";
|
||||
import socket from "../js/socket";
|
||||
import Message from "./Message.vue";
|
||||
import MessageCondensed from "./MessageCondensed.vue";
|
||||
import DateMarker from "./DateMarker.vue";
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onBeforeUpdate,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
PropType,
|
||||
ref,
|
||||
watch,
|
||||
} from "vue";
|
||||
import {useStore} from "../js/store";
|
||||
import {ClientChan, ClientMessage, ClientNetwork, ClientLinkPreview} from "../js/types";
|
||||
|
||||
type CondensedMessageContainer = {
|
||||
type: "condensed";
|
||||
time: Date;
|
||||
messages: ClientMessage[];
|
||||
id?: number;
|
||||
};
|
||||
|
||||
// TODO; move into component
|
||||
let unreadMarkerShown = false;
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageList",
|
||||
components: {
|
||||
Message,
|
||||
MessageCondensed,
|
||||
DateMarker,
|
||||
},
|
||||
props: {
|
||||
network: {type: Object as PropType<ClientNetwork>, required: true},
|
||||
channel: {type: Object as PropType<ClientChan>, required: true},
|
||||
focused: Number,
|
||||
},
|
||||
setup(props) {
|
||||
const store = useStore();
|
||||
|
||||
const chat = ref<HTMLDivElement | null>(null);
|
||||
const loadMoreButton = ref<HTMLButtonElement | null>(null);
|
||||
const historyObserver = ref<IntersectionObserver | null>(null);
|
||||
const skipNextScrollEvent = ref(false);
|
||||
|
||||
const isWaitingForNextTick = ref(false);
|
||||
|
||||
const jumpToBottom = () => {
|
||||
skipNextScrollEvent.value = true;
|
||||
props.channel.scrolledToBottom = true;
|
||||
|
||||
const el = chat.value;
|
||||
|
||||
if (el) {
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
};
|
||||
|
||||
const onShowMoreClick = () => {
|
||||
if (!store.state.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
let lastMessage = -1;
|
||||
|
||||
// Find the id of first message that isn't showInActive
|
||||
// If showInActive is set, this message is actually in another channel
|
||||
for (const message of props.channel.messages) {
|
||||
if (!message.showInActive) {
|
||||
lastMessage = message.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
props.channel.historyLoading = true;
|
||||
|
||||
socket.emit("more", {
|
||||
target: props.channel.id,
|
||||
lastId: lastMessage,
|
||||
condensed: store.state.settings.statusMessages !== "shown",
|
||||
});
|
||||
};
|
||||
|
||||
const onLoadButtonObserved = (entries: IntersectionObserverEntry[]) => {
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.isIntersecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
onShowMoreClick();
|
||||
});
|
||||
};
|
||||
|
||||
nextTick(() => {
|
||||
if (!chat.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.IntersectionObserver) {
|
||||
historyObserver.value = new window.IntersectionObserver(onLoadButtonObserved, {
|
||||
root: chat.value,
|
||||
});
|
||||
}
|
||||
|
||||
jumpToBottom();
|
||||
}).catch((e) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error in new IntersectionObserver", e);
|
||||
});
|
||||
|
||||
const condensedMessages = computed(() => {
|
||||
if (props.channel.type !== ChanType.CHANNEL && props.channel.type !== ChanType.QUERY) {
|
||||
return props.channel.messages;
|
||||
}
|
||||
|
||||
// If actions are hidden, just return a message list with them excluded
|
||||
if (store.state.settings.statusMessages === "hidden") {
|
||||
return props.channel.messages.filter(
|
||||
(message) => !condensedTypes.has(message.type || "")
|
||||
);
|
||||
}
|
||||
|
||||
// If actions are not condensed, just return raw message list
|
||||
if (store.state.settings.statusMessages !== "condensed") {
|
||||
return props.channel.messages;
|
||||
}
|
||||
|
||||
let lastCondensedContainer: CondensedMessageContainer | null = null;
|
||||
|
||||
const condensed: (ClientMessage | CondensedMessageContainer)[] = [];
|
||||
|
||||
for (const message of props.channel.messages) {
|
||||
// If this message is not condensable, or its an action affecting our user,
|
||||
// then just append the message to container and be done with it
|
||||
if (message.self || message.highlight || !condensedTypes.has(message.type || "")) {
|
||||
lastCondensedContainer = null;
|
||||
|
||||
condensed.push(message);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!lastCondensedContainer) {
|
||||
lastCondensedContainer = {
|
||||
time: message.time,
|
||||
type: "condensed",
|
||||
messages: [],
|
||||
};
|
||||
|
||||
condensed.push(lastCondensedContainer);
|
||||
}
|
||||
|
||||
lastCondensedContainer!.messages.push(message);
|
||||
|
||||
// Set id of the condensed container to last message id,
|
||||
// which is required for the unread marker to work correctly
|
||||
lastCondensedContainer!.id = message.id;
|
||||
|
||||
// If this message is the unread boundary, create a split condensed container
|
||||
if (message.id === props.channel.firstUnread) {
|
||||
lastCondensedContainer = null;
|
||||
}
|
||||
}
|
||||
|
||||
return condensed.map((message) => {
|
||||
// Skip condensing single messages, it doesn't save any
|
||||
// space but makes useful information harder to see
|
||||
if (message.type === "condensed" && message.messages.length === 1) {
|
||||
return message.messages[0];
|
||||
}
|
||||
|
||||
return message;
|
||||
});
|
||||
});
|
||||
|
||||
const shouldDisplayDateMarker = (
|
||||
message: SharedMsg | CondensedMessageContainer,
|
||||
id: number
|
||||
) => {
|
||||
const previousMessage = condensedMessages.value[id - 1];
|
||||
|
||||
if (!previousMessage) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const oldDate = new Date(previousMessage.time);
|
||||
const newDate = new Date(message.time);
|
||||
|
||||
return (
|
||||
oldDate.getDate() !== newDate.getDate() ||
|
||||
oldDate.getMonth() !== newDate.getMonth() ||
|
||||
oldDate.getFullYear() !== newDate.getFullYear()
|
||||
);
|
||||
};
|
||||
|
||||
const shouldDisplayUnreadMarker = (id: number) => {
|
||||
if (!unreadMarkerShown && id > props.channel.firstUnread) {
|
||||
unreadMarkerShown = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const isPreviousSource = (currentMessage: ClientMessage, id: number) => {
|
||||
const previousMessage = condensedMessages.value[id - 1];
|
||||
return (
|
||||
previousMessage &&
|
||||
currentMessage.type === MessageType.MESSAGE &&
|
||||
previousMessage.type === MessageType.MESSAGE &&
|
||||
currentMessage.from &&
|
||||
previousMessage.from &&
|
||||
currentMessage.from.nick === previousMessage.from.nick
|
||||
);
|
||||
};
|
||||
|
||||
const onCopy = () => {
|
||||
if (chat.value) {
|
||||
clipboard(chat.value);
|
||||
}
|
||||
};
|
||||
|
||||
const keepScrollPosition = async () => {
|
||||
// If we are already waiting for the next tick to force scroll position,
|
||||
// we have no reason to perform more checks and set it again in the next tick
|
||||
if (isWaitingForNextTick.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const el = chat.value;
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!props.channel.scrolledToBottom) {
|
||||
if (props.channel.historyLoading) {
|
||||
const heightOld = el.scrollHeight - el.scrollTop;
|
||||
|
||||
isWaitingForNextTick.value = true;
|
||||
|
||||
await nextTick();
|
||||
|
||||
isWaitingForNextTick.value = false;
|
||||
skipNextScrollEvent.value = true;
|
||||
|
||||
el.scrollTop = el.scrollHeight - heightOld;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
isWaitingForNextTick.value = true;
|
||||
await nextTick();
|
||||
isWaitingForNextTick.value = false;
|
||||
|
||||
jumpToBottom();
|
||||
};
|
||||
|
||||
const onLinkPreviewToggle = async (preview: ClientLinkPreview, message: ClientMessage) => {
|
||||
await keepScrollPosition();
|
||||
|
||||
// Tell the server we're toggling so it remembers at page reload
|
||||
socket.emit("msg:preview:toggle", {
|
||||
target: props.channel.id,
|
||||
msgId: message.id,
|
||||
link: preview.link,
|
||||
shown: preview.shown,
|
||||
});
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
// Setting scrollTop also triggers scroll event
|
||||
// We don't want to perform calculations for that
|
||||
if (skipNextScrollEvent.value) {
|
||||
skipNextScrollEvent.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const el = chat.value;
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
props.channel.scrolledToBottom = el.scrollHeight - el.scrollTop - el.offsetHeight <= 30;
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
// Keep message list scrolled to bottom on resize
|
||||
if (props.channel.scrolledToBottom) {
|
||||
jumpToBottom();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
chat.value?.addEventListener("scroll", handleScroll, {passive: true});
|
||||
|
||||
eventbus.on("resize", handleResize);
|
||||
|
||||
void nextTick(() => {
|
||||
if (historyObserver.value && loadMoreButton.value) {
|
||||
historyObserver.value.observe(loadMoreButton.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.channel.id,
|
||||
() => {
|
||||
props.channel.scrolledToBottom = true;
|
||||
|
||||
// Re-add the intersection observer to trigger the check again on channel switch
|
||||
// Otherwise if last channel had the button visible, switching to a new channel won't trigger the history
|
||||
if (historyObserver.value && loadMoreButton.value) {
|
||||
historyObserver.value.unobserve(loadMoreButton.value);
|
||||
historyObserver.value.observe(loadMoreButton.value);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.channel.messages,
|
||||
async () => {
|
||||
await keepScrollPosition();
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.channel.pendingMessage,
|
||||
async () => {
|
||||
// Keep the scroll stuck when input gets resized while typing
|
||||
await keepScrollPosition();
|
||||
}
|
||||
);
|
||||
|
||||
onBeforeUpdate(() => {
|
||||
unreadMarkerShown = false;
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
eventbus.off("resize", handleResize);
|
||||
chat.value?.removeEventListener("scroll", handleScroll);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (historyObserver.value) {
|
||||
historyObserver.value.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
chat,
|
||||
store,
|
||||
onShowMoreClick,
|
||||
loadMoreButton,
|
||||
onCopy,
|
||||
condensedMessages,
|
||||
shouldDisplayDateMarker,
|
||||
shouldDisplayUnreadMarker,
|
||||
keepScrollPosition,
|
||||
isPreviousSource,
|
||||
jumpToBottom,
|
||||
onLinkPreviewToggle,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
175
client/components/MessageSearchForm.vue
Normal file
175
client/components/MessageSearchForm.vue
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
<template>
|
||||
<form :class="['message-search', {opened: searchOpened}]" @submit.prevent="searchMessages">
|
||||
<div class="input-wrapper">
|
||||
<input
|
||||
ref="searchInputField"
|
||||
v-model="searchInput"
|
||||
type="search"
|
||||
name="search"
|
||||
class="input"
|
||||
placeholder="Search messages…"
|
||||
@blur="closeSearch"
|
||||
@keyup.esc="closeSearch"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
v-if="!onSearchPage"
|
||||
class="search"
|
||||
type="button"
|
||||
aria-label="Search messages in this channel"
|
||||
@mousedown.prevent="toggleSearch"
|
||||
/>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
form.message-search {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
form.message-search .input-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
form.message-search input {
|
||||
width: 100%;
|
||||
height: auto !important;
|
||||
margin: 7px 0;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
background-color: #fafafa;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
form.message-search input::placeholder {
|
||||
color: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
form.message-search input {
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
form.message-search input:focus {
|
||||
min-width: 220px;
|
||||
}
|
||||
}
|
||||
|
||||
form.message-search .input-wrapper {
|
||||
position: absolute;
|
||||
top: 45px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--window-bg-color);
|
||||
}
|
||||
|
||||
form.message-search .input-wrapper input {
|
||||
margin: 7px;
|
||||
}
|
||||
|
||||
form.message-search.opened .input-wrapper {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
#chat form.message-search button {
|
||||
display: flex;
|
||||
color: #607992;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, onMounted, PropType, ref, watch} from "vue";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import eventbus from "../js/eventbus";
|
||||
import {ClientNetwork, ClientChan} from "../js/types";
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageSearchForm",
|
||||
props: {
|
||||
network: {type: Object as PropType<ClientNetwork>, required: true},
|
||||
channel: {type: Object as PropType<ClientChan>, required: true},
|
||||
},
|
||||
setup(props) {
|
||||
const searchOpened = ref(false);
|
||||
const searchInput = ref("");
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const searchInputField = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const onSearchPage = computed(() => {
|
||||
return route.name === "SearchResults";
|
||||
});
|
||||
|
||||
watch(route, (newValue) => {
|
||||
if (newValue.query.q) {
|
||||
searchInput.value = String(newValue.query.q);
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
searchInput.value = String(route.query.q || "");
|
||||
searchOpened.value = onSearchPage.value;
|
||||
|
||||
if (searchInputField.value && !searchInput.value && searchOpened.value) {
|
||||
searchInputField.value.focus();
|
||||
}
|
||||
});
|
||||
|
||||
const closeSearch = () => {
|
||||
if (!onSearchPage.value) {
|
||||
searchInput.value = "";
|
||||
searchOpened.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSearch = () => {
|
||||
if (searchOpened.value) {
|
||||
searchInputField.value?.blur();
|
||||
return;
|
||||
}
|
||||
|
||||
searchOpened.value = true;
|
||||
searchInputField.value?.focus();
|
||||
};
|
||||
|
||||
const searchMessages = (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!searchInput.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
router
|
||||
.push({
|
||||
name: "SearchResults",
|
||||
params: {
|
||||
id: props.channel.id,
|
||||
},
|
||||
query: {
|
||||
q: searchInput.value,
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.name === "NavigationDuplicated") {
|
||||
// Search for the same query again
|
||||
eventbus.emit("re-search");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
searchOpened,
|
||||
searchInput,
|
||||
searchInputField,
|
||||
closeSearch,
|
||||
toggleSearch,
|
||||
searchMessages,
|
||||
onSearchPage,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
35
client/components/MessageTypes/away.vue
Normal file
35
client/components/MessageTypes/away.vue
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<ParsedMessage v-if="message.self" :network="network" :message="message" />
|
||||
<template v-else>
|
||||
<Username :user="message.from" />
|
||||
is away
|
||||
<i class="away-message">(<ParsedMessage :network="network" :message="message" />)</i>
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import type {ClientNetwork, ClientMessage} from "../../js/types";
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
import Username from "../Username.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageTypeAway",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
Username,
|
||||
},
|
||||
props: {
|
||||
network: {
|
||||
type: Object as PropType<ClientNetwork>,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: Object as PropType<ClientMessage>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
34
client/components/MessageTypes/back.vue
Normal file
34
client/components/MessageTypes/back.vue
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<ParsedMessage v-if="message.self" :network="network" :message="message" />
|
||||
<template v-else>
|
||||
<Username :user="message.from" />
|
||||
is back
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import {ClientNetwork, ClientMessage} from "../../js/types";
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
import Username from "../Username.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageTypeBack",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
Username,
|
||||
},
|
||||
props: {
|
||||
network: {
|
||||
type: Object as PropType<ClientNetwork>,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: Object as PropType<ClientMessage>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
38
client/components/MessageTypes/chghost.vue
Normal file
38
client/components/MessageTypes/chghost.vue
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<Username :user="message.from" />
|
||||
has changed
|
||||
<span v-if="message.new_ident"
|
||||
>username to <b>{{ message.new_ident }}</b></span
|
||||
>
|
||||
<span v-if="message.new_host"
|
||||
>hostname to
|
||||
<i class="hostmask"><ParsedMessage :network="network" :text="message.new_host" /></i
|
||||
></span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import {ClientNetwork, ClientMessage} from "../../js/types";
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
import Username from "../Username.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageTypeChangeHost",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
Username,
|
||||
},
|
||||
props: {
|
||||
network: {
|
||||
type: Object as PropType<ClientNetwork>,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: Object as PropType<ClientMessage>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
31
client/components/MessageTypes/ctcp.vue
Normal file
31
client/components/MessageTypes/ctcp.vue
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<Username :user="message.from" />
|
||||
{{ ` ` }}<span class="ctcp-message"><ParsedMessage :text="message.ctcpMessage" /></span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import {ClientNetwork, ClientMessage} from "../../js/types";
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
import Username from "../Username.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageTypeCTCP",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
Username,
|
||||
},
|
||||
props: {
|
||||
network: {
|
||||
type: Object as PropType<ClientNetwork>,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: Object as PropType<ClientMessage>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
32
client/components/MessageTypes/ctcp_request.vue
Normal file
32
client/components/MessageTypes/ctcp_request.vue
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<Username :user="message.from" />
|
||||
sent a <abbr title="Client-to-client protocol">CTCP</abbr> request:
|
||||
<span class="ctcp-message"><ParsedMessage :text="message.ctcpMessage" /></span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import {ClientNetwork, ClientMessage} from "../../js/types";
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
import Username from "../Username.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageTypeRequestCTCP",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
Username,
|
||||
},
|
||||
props: {
|
||||
network: {
|
||||
type: Object as PropType<ClientNetwork>,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: Object as PropType<ClientMessage>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
77
client/components/MessageTypes/error.vue
Normal file
77
client/components/MessageTypes/error.vue
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<ParsedMessage :network="network" :message="message" :text="errorMessage" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
import {computed, defineComponent, PropType} from "vue";
|
||||
import {ClientNetwork, ClientMessage} from "../../js/types";
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageTypeError",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
},
|
||||
props: {
|
||||
network: {
|
||||
type: Object as PropType<ClientNetwork>,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: Object as PropType<ClientMessage>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const errorMessage = computed(() => {
|
||||
// TODO: enforce chan and nick fields so that we can get rid of that
|
||||
const chan = props.message.channel || "!UNKNOWN_CHAN";
|
||||
const nick = props.message.nick || "!UNKNOWN_NICK";
|
||||
|
||||
switch (props.message.error) {
|
||||
case "bad_channel_key":
|
||||
return `Cannot join ${chan} - Bad channel key.`;
|
||||
case "banned_from_channel":
|
||||
return `Cannot join ${chan} - You have been banned from the channel.`;
|
||||
case "cannot_send_to_channel":
|
||||
return `Cannot send to channel ${chan}`;
|
||||
case "channel_is_full":
|
||||
return `Cannot join ${chan} - Channel is full.`;
|
||||
case "chanop_privs_needed":
|
||||
return "Cannot perform action: You're not a channel operator.";
|
||||
case "invite_only_channel":
|
||||
return `Cannot join ${chan} - Channel is invite only.`;
|
||||
case "no_such_nick":
|
||||
return `User ${nick} hasn't logged in or does not exist.`;
|
||||
case "not_on_channel":
|
||||
return "Cannot perform action: You're not on the channel.";
|
||||
case "password_mismatch":
|
||||
return "Password mismatch.";
|
||||
case "too_many_channels":
|
||||
return `Cannot join ${chan} - You've already reached the maximum number of channels allowed.`;
|
||||
case "unknown_command":
|
||||
// TODO: not having message.command should never happen, so force existence
|
||||
return `Unknown command: ${props.message.command || "!UNDEFINED_COMMAND_BUG"}`;
|
||||
case "user_not_in_channel":
|
||||
return `User ${nick} is not on the channel.`;
|
||||
case "user_on_channel":
|
||||
return `User ${nick} is already on the channel.`;
|
||||
default:
|
||||
if (props.message.reason) {
|
||||
return `${props.message.reason} (${
|
||||
props.message.error || "!UNDEFINED_ERR"
|
||||
})`;
|
||||
}
|
||||
|
||||
return props.message.error;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
errorMessage,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
11
client/components/MessageTypes/index.ts
Normal file
11
client/components/MessageTypes/index.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// This creates a version of `require()` in the context of the current
|
||||
// directory, so we iterate over its content, which is a map statically built by
|
||||
// Webpack.
|
||||
// Second argument says it's recursive, third makes sure we only load templates.
|
||||
const requireViews = require.context(".", false, /\.vue$/);
|
||||
|
||||
export default requireViews.keys().reduce((acc: Record<string, any>, path) => {
|
||||
acc["message-" + path.substring(2, path.length - 4)] = requireViews(path).default;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
34
client/components/MessageTypes/invite.vue
Normal file
34
client/components/MessageTypes/invite.vue
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<Username :user="message.from" />
|
||||
invited
|
||||
<span v-if="message.invitedYou">you</span>
|
||||
<Username v-else :user="message.target" />
|
||||
to <ParsedMessage :network="network" :text="message.channel" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import {ClientNetwork, ClientMessage} from "../../js/types";
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
import Username from "../Username.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageTypeInvite",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
Username,
|
||||
},
|
||||
props: {
|
||||
network: {
|
||||
type: Object as PropType<ClientNetwork>,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: Object as PropType<ClientMessage>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
38
client/components/MessageTypes/join.vue
Normal file
38
client/components/MessageTypes/join.vue
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<Username :user="message.from" />
|
||||
<i class="hostmask"> (<ParsedMessage :network="network" :text="message.hostmask" />)</i>
|
||||
<template v-if="message.account">
|
||||
<i class="account"> [{{ message.account }}]</i>
|
||||
</template>
|
||||
<template v-if="message.gecos">
|
||||
<i class="realname"> ({{ message.gecos }})</i>
|
||||
</template>
|
||||
has joined the channel
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import {ClientNetwork, ClientMessage} from "../../js/types";
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
import Username from "../Username.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageTypeJoin",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
Username,
|
||||
},
|
||||
props: {
|
||||
network: {
|
||||
type: Object as PropType<ClientNetwork>,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: Object as PropType<ClientMessage>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
35
client/components/MessageTypes/kick.vue
Normal file
35
client/components/MessageTypes/kick.vue
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<Username :user="message.from" />
|
||||
has kicked
|
||||
<Username :user="message.target" />
|
||||
<i v-if="message.text" class="part-reason"
|
||||
> (<ParsedMessage :network="network" :message="message" />)</i
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import {ClientNetwork, ClientMessage} from "../../js/types";
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
import Username from "../Username.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageTypeKick",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
Username,
|
||||
},
|
||||
props: {
|
||||
network: {
|
||||
type: Object as PropType<ClientNetwork>,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: Object as PropType<ClientMessage>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
32
client/components/MessageTypes/mode.vue
Normal file
32
client/components/MessageTypes/mode.vue
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<Username :user="message.from" />
|
||||
sets mode
|
||||
<ParsedMessage :message="message" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import {ClientNetwork, ClientMessage} from "../../js/types";
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
import Username from "../Username.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageTypeMode",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
Username,
|
||||
},
|
||||
props: {
|
||||
network: {
|
||||
type: Object as PropType<ClientNetwork>,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: Object as PropType<ClientMessage>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
24
client/components/MessageTypes/mode_channel.vue
Normal file
24
client/components/MessageTypes/mode_channel.vue
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
Channel mode is <b>{{ message.text }}</b>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import {ClientNetwork, ClientMessage} from "../../js/types";
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageChannelMode",
|
||||
props: {
|
||||
network: {
|
||||
type: Object as PropType<ClientNetwork>,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: Object as PropType<ClientMessage>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
24
client/components/MessageTypes/mode_user.vue
Normal file
24
client/components/MessageTypes/mode_user.vue
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
Your user mode is <b>{{ message.raw_modes }}</b>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import {ClientNetwork, ClientMessage} from "../../js/types";
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageChannelMode",
|
||||
props: {
|
||||
network: {
|
||||
type: Object as PropType<ClientNetwork>,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: Object as PropType<ClientMessage>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
49
client/components/MessageTypes/monospace_block.vue
Normal file
49
client/components/MessageTypes/monospace_block.vue
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<span class="text"><ParsedMessage :network="network" :text="cleanText" /></span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, PropType} from "vue";
|
||||
import {ClientNetwork, ClientMessage} from "../../js/types";
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageTypeMonospaceBlock",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
},
|
||||
props: {
|
||||
network: {
|
||||
type: Object as PropType<ClientNetwork>,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: Object as PropType<ClientMessage>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const cleanText = computed(() => {
|
||||
let lines = props.message.text.split("\n");
|
||||
|
||||
// If all non-empty lines of the MOTD start with a hyphen (which is common
|
||||
// across MOTDs), remove all the leading hyphens.
|
||||
if (lines.every((line) => line === "" || line[0] === "-")) {
|
||||
lines = lines.map((line) => line.substring(2));
|
||||
}
|
||||
|
||||
// Remove empty lines around the MOTD (but not within it)
|
||||
return lines
|
||||
.map((line) => line.replace(/\s*$/, ""))
|
||||
.join("\n")
|
||||
.replace(/^[\r\n]+|[\r\n]+$/g, "");
|
||||
});
|
||||
|
||||
return {
|
||||
cleanText,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
30
client/components/MessageTypes/nick.vue
Normal file
30
client/components/MessageTypes/nick.vue
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<Username :user="message.from" />
|
||||
is now known as
|
||||
<Username :user="{nick: message.new_nick, mode: message.from.mode}" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import {ClientNetwork, ClientMessage} from "../../js/types";
|
||||
import Username from "../Username.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageTypeNick",
|
||||
components: {
|
||||
Username,
|
||||
},
|
||||
props: {
|
||||
network: {
|
||||
type: Object as PropType<ClientNetwork>,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: Object as PropType<ClientMessage>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
35
client/components/MessageTypes/part.vue
Normal file
35
client/components/MessageTypes/part.vue
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<Username :user="message.from" />
|
||||
<i class="hostmask"> (<ParsedMessage :network="network" :text="message.hostmask" />)</i> has
|
||||
left the channel
|
||||
<i v-if="message.text" class="part-reason"
|
||||
>(<ParsedMessage :network="network" :message="message" />)</i
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import {ClientNetwork, ClientMessage} from "../../js/types";
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
import Username from "../Username.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageTypePart",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
Username,
|
||||
},
|
||||
props: {
|
||||
network: {
|
||||
type: Object as PropType<ClientNetwork>,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: Object as PropType<ClientMessage>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
35
client/components/MessageTypes/quit.vue
Normal file
35
client/components/MessageTypes/quit.vue
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<Username :user="message.from" />
|
||||
<i class="hostmask"> (<ParsedMessage :network="network" :text="message.hostmask" />)</i> has
|
||||
quit
|
||||
<i v-if="message.text" class="quit-reason"
|
||||
>(<ParsedMessage :network="network" :message="message" />)</i
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import type {ClientMessage, ClientNetwork} from "../../js/types";
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
import Username from "../Username.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageTypeQuit",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
Username,
|
||||
},
|
||||
props: {
|
||||
network: {
|
||||
type: Object as PropType<ClientNetwork>,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: Object as PropType<ClientMessage>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
22
client/components/MessageTypes/raw.vue
Normal file
22
client/components/MessageTypes/raw.vue
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<template>
|
||||
<span class="content">{{ message.text }}</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import {ClientNetwork, ClientMessage} from "../../js/types";
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageTypeRaw",
|
||||
props: {
|
||||
network: {
|
||||
type: Object as PropType<ClientNetwork>,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: Object as PropType<ClientMessage>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
36
client/components/MessageTypes/topic.vue
Normal file
36
client/components/MessageTypes/topic.vue
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<template v-if="message.from && message.from.nick"
|
||||
><Username :user="message.from" /> has changed the topic to:
|
||||
</template>
|
||||
<template v-else>The topic is: </template>
|
||||
<span v-if="message.text" class="new-topic"
|
||||
><ParsedMessage :network="network" :message="message"
|
||||
/></span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import type {ClientMessage, ClientNetwork} from "../../js/types";
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
import Username from "../Username.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageTypeTopic",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
Username,
|
||||
},
|
||||
props: {
|
||||
network: {
|
||||
type: Object as PropType<ClientNetwork>,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: Object as PropType<ClientMessage>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
38
client/components/MessageTypes/topic_set_by.vue
Normal file
38
client/components/MessageTypes/topic_set_by.vue
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
Topic set by
|
||||
<Username :user="message.from" />
|
||||
on {{ messageTimeLocale }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import localetime from "../../js/helpers/localetime";
|
||||
import {computed, defineComponent, PropType} from "vue";
|
||||
import {ClientNetwork, ClientMessage} from "../../js/types";
|
||||
import Username from "../Username.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageTypeTopicSetBy",
|
||||
components: {
|
||||
Username,
|
||||
},
|
||||
props: {
|
||||
network: {
|
||||
type: Object as PropType<ClientNetwork>,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: Object as PropType<ClientMessage>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const messageTimeLocale = computed(() => localetime(props.message.when));
|
||||
|
||||
return {
|
||||
messageTimeLocale,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
150
client/components/MessageTypes/whois.vue
Normal file
150
client/components/MessageTypes/whois.vue
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
<template>
|
||||
<span class="content">
|
||||
<p>
|
||||
<Username :user="{nick: message.whois.nick}" />
|
||||
<span v-if="message.whois.whowas"> is offline, last information:</span>
|
||||
</p>
|
||||
|
||||
<dl class="whois">
|
||||
<template v-if="message.whois.account">
|
||||
<dt>Logged in as:</dt>
|
||||
<dd>{{ message.whois.account }}</dd>
|
||||
</template>
|
||||
|
||||
<dt>Host mask:</dt>
|
||||
<dd class="hostmask">
|
||||
<ParsedMessage
|
||||
:network="network"
|
||||
:text="message.whois.ident + '@' + message.whois.hostname"
|
||||
/>
|
||||
</dd>
|
||||
|
||||
<template v-if="message.whois.actual_hostname">
|
||||
<dt>Actual host:</dt>
|
||||
<dd class="hostmask">
|
||||
<a
|
||||
:href="'https://ipinfo.io/' + message.whois.actual_ip"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>{{ message.whois.actual_ip }}</a
|
||||
>
|
||||
<i v-if="message.whois.actual_hostname != message.whois.actual_ip">
|
||||
({{ message.whois.actual_hostname }})</i
|
||||
>
|
||||
</dd>
|
||||
</template>
|
||||
|
||||
<template v-if="message.whois.actual_username">
|
||||
<dt>Actual username:</dt>
|
||||
<dd>{{ message.whois.actual_username }}</dd>
|
||||
</template>
|
||||
|
||||
<template v-if="message.whois.real_name">
|
||||
<dt>Real name:</dt>
|
||||
<dd><ParsedMessage :network="network" :text="message.whois.real_name" /></dd>
|
||||
</template>
|
||||
|
||||
<template v-if="message.whois.registered_nick">
|
||||
<dt>Registered nick:</dt>
|
||||
<dd>{{ message.whois.registered_nick }}</dd>
|
||||
</template>
|
||||
|
||||
<template v-if="message.whois.channels">
|
||||
<dt>Channels:</dt>
|
||||
<dd><ParsedMessage :network="network" :text="message.whois.channels" /></dd>
|
||||
</template>
|
||||
|
||||
<template v-if="message.whois.modes">
|
||||
<dt>Modes:</dt>
|
||||
<dd>{{ message.whois.modes }}</dd>
|
||||
</template>
|
||||
|
||||
<template v-if="message.whois.special">
|
||||
<template v-for="special in message.whois.special" :key="special">
|
||||
<dt>Special:</dt>
|
||||
<dd>{{ special }}</dd>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-if="message.whois.operator">
|
||||
<dt>Operator:</dt>
|
||||
<dd>{{ message.whois.operator }}</dd>
|
||||
</template>
|
||||
|
||||
<template v-if="message.whois.helpop">
|
||||
<dt>Available for help:</dt>
|
||||
<dd>Yes</dd>
|
||||
</template>
|
||||
|
||||
<template v-if="message.whois.bot">
|
||||
<dt>Is a bot:</dt>
|
||||
<dd>Yes</dd>
|
||||
</template>
|
||||
|
||||
<template v-if="message.whois.away">
|
||||
<dt>Away:</dt>
|
||||
<dd><ParsedMessage :network="network" :text="message.whois.away" /></dd>
|
||||
</template>
|
||||
|
||||
<template v-if="message.whois.secure">
|
||||
<dt>Secure connection:</dt>
|
||||
<dd>Yes</dd>
|
||||
</template>
|
||||
|
||||
<template v-if="message.whois.certfps">
|
||||
<template v-for="certfp in message.whois.certfps" :key="certfp">
|
||||
<dt>Certificate:</dt>
|
||||
<dd>{{ certfp }}</dd>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-if="message.whois.server">
|
||||
<dt>Connected to:</dt>
|
||||
<dd>
|
||||
{{ message.whois.server }} <i>({{ message.whois.server_info }})</i>
|
||||
</dd>
|
||||
</template>
|
||||
|
||||
<template v-if="message.whois.logonTime">
|
||||
<dt>Connected at:</dt>
|
||||
<dd>{{ localetime(message.whois.logonTime) }}</dd>
|
||||
</template>
|
||||
|
||||
<template v-if="message.whois.idle">
|
||||
<dt>Idle since:</dt>
|
||||
<dd>{{ localetime(message.whois.idleTime) }}</dd>
|
||||
</template>
|
||||
</dl>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import localetime from "../../js/helpers/localetime";
|
||||
import {ClientNetwork, ClientMessage} from "../../js/types";
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
import Username from "../Username.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "MessageTypeWhois",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
Username,
|
||||
},
|
||||
props: {
|
||||
network: {
|
||||
type: Object as PropType<ClientNetwork>,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: Object as PropType<ClientMessage>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
localetime: (date: Date) => localetime(date),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
572
client/components/NetworkForm.vue
Normal file
572
client/components/NetworkForm.vue
Normal file
|
|
@ -0,0 +1,572 @@
|
|||
<template>
|
||||
<div id="connect" class="window" role="tabpanel" aria-label="Connect">
|
||||
<div class="header">
|
||||
<SidebarToggle />
|
||||
</div>
|
||||
<form class="container" method="post" action="" @submit.prevent="onSubmit">
|
||||
<h1 class="title">
|
||||
<template v-if="defaults.uuid">
|
||||
<input v-model="defaults.uuid" type="hidden" name="uuid" />
|
||||
Edit {{ defaults.name }}
|
||||
</template>
|
||||
<template v-else>
|
||||
Connect
|
||||
<template
|
||||
v-if="config?.lockNetwork && store?.state.serverConfiguration?.public"
|
||||
>
|
||||
to {{ defaults.name }}
|
||||
</template>
|
||||
</template>
|
||||
</h1>
|
||||
<template v-if="!config?.lockNetwork">
|
||||
<h2>Network settings</h2>
|
||||
<div class="connect-row">
|
||||
<label for="connect:name">Name</label>
|
||||
<input
|
||||
id="connect:name"
|
||||
v-model.trim="defaults.name"
|
||||
class="input"
|
||||
name="name"
|
||||
maxlength="100"
|
||||
/>
|
||||
</div>
|
||||
<div class="connect-row">
|
||||
<label for="connect:host">Server</label>
|
||||
<div class="input-wrap">
|
||||
<input
|
||||
id="connect:host"
|
||||
v-model.trim="defaults.host"
|
||||
class="input"
|
||||
name="host"
|
||||
aria-label="Server address"
|
||||
maxlength="255"
|
||||
required
|
||||
/>
|
||||
<span id="connect:portseparator">:</span>
|
||||
<input
|
||||
id="connect:port"
|
||||
v-model="defaults.port"
|
||||
class="input"
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
name="port"
|
||||
aria-label="Server port"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="connect-row">
|
||||
<label for="connect:password">Password</label>
|
||||
<RevealPassword
|
||||
v-slot:default="slotProps"
|
||||
class="input-wrap password-container"
|
||||
>
|
||||
<input
|
||||
id="connect:password"
|
||||
v-model="defaults.password"
|
||||
class="input"
|
||||
:type="slotProps.isVisible ? 'text' : 'password'"
|
||||
placeholder="Server password (optional)"
|
||||
name="password"
|
||||
maxlength="300"
|
||||
/>
|
||||
</RevealPassword>
|
||||
</div>
|
||||
<div class="connect-row">
|
||||
<label></label>
|
||||
<div class="input-wrap">
|
||||
<label class="tls">
|
||||
<input
|
||||
v-model="defaults.tls"
|
||||
type="checkbox"
|
||||
name="tls"
|
||||
:disabled="defaults.hasSTSPolicy"
|
||||
/>
|
||||
Use secure connection (TLS)
|
||||
<span
|
||||
v-if="defaults.hasSTSPolicy"
|
||||
class="tooltipped tooltipped-n tooltipped-no-delay"
|
||||
aria-label="This network has a strict transport security policy, you will be unable to disable TLS"
|
||||
>🔒 STS</span
|
||||
>
|
||||
</label>
|
||||
<label class="tls">
|
||||
<input
|
||||
v-model="defaults.rejectUnauthorized"
|
||||
type="checkbox"
|
||||
name="rejectUnauthorized"
|
||||
/>
|
||||
Only allow trusted certificates
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Proxy Settings</h2>
|
||||
<div class="connect-row">
|
||||
<label></label>
|
||||
<div class="input-wrap">
|
||||
<label for="connect:proxyEnabled">
|
||||
<input
|
||||
id="connect:proxyEnabled"
|
||||
v-model="defaults.proxyEnabled"
|
||||
type="checkbox"
|
||||
name="proxyEnabled"
|
||||
/>
|
||||
Enable Proxy
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="defaults.proxyEnabled">
|
||||
<div class="connect-row">
|
||||
<label for="connect:proxyHost">SOCKS Address</label>
|
||||
<div class="input-wrap">
|
||||
<input
|
||||
id="connect:proxyHost"
|
||||
v-model.trim="defaults.proxyHost"
|
||||
class="input"
|
||||
name="proxyHost"
|
||||
aria-label="Proxy host"
|
||||
maxlength="255"
|
||||
/>
|
||||
<span id="connect:proxyPortSeparator">:</span>
|
||||
<input
|
||||
id="connect:proxyPort"
|
||||
v-model="defaults.proxyPort"
|
||||
class="input"
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
name="proxyPort"
|
||||
aria-label="SOCKS port"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="connect-row">
|
||||
<label for="connect:proxyUsername">Proxy username</label>
|
||||
<input
|
||||
id="connect:proxyUsername"
|
||||
ref="proxyUsernameInput"
|
||||
v-model.trim="defaults.proxyUsername"
|
||||
class="input username"
|
||||
name="proxyUsername"
|
||||
maxlength="100"
|
||||
placeholder="Proxy username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="connect-row">
|
||||
<label for="connect:proxyPassword">Proxy password</label>
|
||||
<RevealPassword
|
||||
v-slot:default="slotProps"
|
||||
class="input-wrap password-container"
|
||||
>
|
||||
<input
|
||||
id="connect:proxyPassword"
|
||||
ref="proxyPassword"
|
||||
v-model="defaults.proxyPassword"
|
||||
class="input"
|
||||
:type="slotProps.isVisible ? 'text' : 'password'"
|
||||
placeholder="Proxy password"
|
||||
name="proxyPassword"
|
||||
maxlength="300"
|
||||
/>
|
||||
</RevealPassword>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="config.lockNetwork && !store.state.serverConfiguration?.public">
|
||||
<h2>Network settings</h2>
|
||||
<div class="connect-row">
|
||||
<label for="connect:name">Name</label>
|
||||
<input
|
||||
id="connect:name"
|
||||
v-model.trim="defaults.name"
|
||||
class="input"
|
||||
name="name"
|
||||
maxlength="100"
|
||||
/>
|
||||
</div>
|
||||
<div class="connect-row">
|
||||
<label for="connect:password">Password</label>
|
||||
<RevealPassword
|
||||
v-slot:default="slotProps"
|
||||
class="input-wrap password-container"
|
||||
>
|
||||
<input
|
||||
id="connect:password"
|
||||
v-model="defaults.password"
|
||||
class="input"
|
||||
:type="slotProps.isVisible ? 'text' : 'password'"
|
||||
placeholder="Server password (optional)"
|
||||
name="password"
|
||||
maxlength="300"
|
||||
/>
|
||||
</RevealPassword>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<h2>User preferences</h2>
|
||||
<div class="connect-row">
|
||||
<label for="connect:nick">Nick</label>
|
||||
<input
|
||||
id="connect:nick"
|
||||
v-model="defaults.nick"
|
||||
class="input nick"
|
||||
name="nick"
|
||||
pattern="[^\s:!@]+"
|
||||
maxlength="100"
|
||||
required
|
||||
@input="onNickChanged"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="!config?.useHexIp">
|
||||
<div class="connect-row">
|
||||
<label for="connect:username">Username</label>
|
||||
<input
|
||||
id="connect:username"
|
||||
ref="usernameInput"
|
||||
v-model.trim="defaults.username"
|
||||
class="input username"
|
||||
name="username"
|
||||
maxlength="100"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="connect-row">
|
||||
<label for="connect:realname">Real name</label>
|
||||
<input
|
||||
id="connect:realname"
|
||||
v-model.trim="defaults.realname"
|
||||
class="input"
|
||||
name="realname"
|
||||
maxlength="300"
|
||||
/>
|
||||
</div>
|
||||
<div class="connect-row">
|
||||
<label for="connect:leaveMessage">Leave message</label>
|
||||
<input
|
||||
id="connect:leaveMessage"
|
||||
v-model.trim="defaults.leaveMessage"
|
||||
autocomplete="off"
|
||||
class="input"
|
||||
name="leaveMessage"
|
||||
placeholder="The Lounge - https://thelounge.chat"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="defaults.uuid && !store.state.serverConfiguration?.public">
|
||||
<div class="connect-row">
|
||||
<label for="connect:commands">
|
||||
Commands
|
||||
<span
|
||||
class="tooltipped tooltipped-ne tooltipped-no-delay"
|
||||
aria-label="One /command per line.
|
||||
Each command will be executed in
|
||||
the server tab on new connection"
|
||||
>
|
||||
<button class="extra-help" />
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="connect:commands"
|
||||
ref="commandsInput"
|
||||
autocomplete="off"
|
||||
:value="defaults.commands ? defaults.commands.join('\n') : ''"
|
||||
class="input"
|
||||
name="commands"
|
||||
@input="resizeCommandsInput"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="!defaults.uuid">
|
||||
<div class="connect-row">
|
||||
<label for="connect:channels">Channels</label>
|
||||
<input
|
||||
id="connect:channels"
|
||||
v-model.trim="defaults.join"
|
||||
class="input"
|
||||
name="join"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="store.state.serverConfiguration?.public">
|
||||
<template v-if="config?.lockNetwork">
|
||||
<div class="connect-row">
|
||||
<label></label>
|
||||
<div class="input-wrap">
|
||||
<label class="tls">
|
||||
<input v-model="displayPasswordField" type="checkbox" />
|
||||
I have a password
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="displayPasswordField" class="connect-row">
|
||||
<label for="connect:password">Password</label>
|
||||
<RevealPassword
|
||||
v-slot:default="slotProps"
|
||||
class="input-wrap password-container"
|
||||
>
|
||||
<input
|
||||
id="connect:password"
|
||||
ref="publicPassword"
|
||||
v-model="defaults.password"
|
||||
class="input"
|
||||
:type="slotProps.isVisible ? 'text' : 'password'"
|
||||
placeholder="Server password (optional)"
|
||||
name="password"
|
||||
maxlength="300"
|
||||
/>
|
||||
</RevealPassword>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h2 id="label-auth">Authentication</h2>
|
||||
<div class="connect-row connect-auth" role="group" aria-labelledby="label-auth">
|
||||
<label class="opt">
|
||||
<input
|
||||
:checked="!defaults.sasl"
|
||||
type="radio"
|
||||
name="sasl"
|
||||
value=""
|
||||
@change="setSaslAuth('')"
|
||||
/>
|
||||
No authentication
|
||||
</label>
|
||||
<label class="opt">
|
||||
<input
|
||||
:checked="defaults.sasl === 'plain'"
|
||||
type="radio"
|
||||
name="sasl"
|
||||
value="plain"
|
||||
@change="setSaslAuth('plain')"
|
||||
/>
|
||||
Username + password (SASL PLAIN)
|
||||
</label>
|
||||
<label
|
||||
v-if="!store.state.serverConfiguration?.public && defaults.tls"
|
||||
class="opt"
|
||||
>
|
||||
<input
|
||||
:checked="defaults.sasl === 'external'"
|
||||
type="radio"
|
||||
name="sasl"
|
||||
value="external"
|
||||
@change="setSaslAuth('external')"
|
||||
/>
|
||||
Client certificate (SASL EXTERNAL)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<template v-if="defaults.sasl === 'plain'">
|
||||
<div class="connect-row">
|
||||
<label for="connect:username">Account</label>
|
||||
<input
|
||||
id="connect:saslAccount"
|
||||
v-model.trim="defaults.saslAccount"
|
||||
class="input"
|
||||
name="saslAccount"
|
||||
maxlength="100"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="connect-row">
|
||||
<label for="connect:password">Password</label>
|
||||
<RevealPassword
|
||||
v-slot:default="slotProps"
|
||||
class="input-wrap password-container"
|
||||
>
|
||||
<input
|
||||
id="connect:saslPassword"
|
||||
v-model="defaults.saslPassword"
|
||||
class="input"
|
||||
:type="slotProps.isVisible ? 'text' : 'password'"
|
||||
name="saslPassword"
|
||||
maxlength="300"
|
||||
required
|
||||
/>
|
||||
</RevealPassword>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="defaults.sasl === 'external'" class="connect-sasl-external">
|
||||
<p>The Lounge automatically generates and manages the client certificate.</p>
|
||||
<p>
|
||||
On the IRC server, you will need to tell the services to attach the
|
||||
certificate fingerprint (certfp) to your account, for example:
|
||||
</p>
|
||||
<pre><code>/msg NickServ CERT ADD</code></pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<button type="submit" class="btn" :disabled="disabled ? true : false">
|
||||
<template v-if="defaults.uuid">Save network</template>
|
||||
<template v-else>Connect</template>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#connect .connect-auth {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#connect .connect-auth .opt {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#connect .connect-auth input {
|
||||
margin: 3px 10px 0 0;
|
||||
}
|
||||
|
||||
#connect .connect-sasl-external {
|
||||
padding: 10px;
|
||||
border-radius: 2px;
|
||||
background-color: #d9edf7;
|
||||
color: #31708f;
|
||||
}
|
||||
|
||||
#connect .connect-sasl-external pre {
|
||||
margin: 0;
|
||||
user-select: text;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import RevealPassword from "./RevealPassword.vue";
|
||||
import SidebarToggle from "./SidebarToggle.vue";
|
||||
import {defineComponent, nextTick, PropType, ref, watch} from "vue";
|
||||
import {useStore} from "../js/store";
|
||||
import {ClientNetwork} from "../js/types";
|
||||
|
||||
export type NetworkFormDefaults = Partial<ClientNetwork> & {
|
||||
join?: string;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: "NetworkForm",
|
||||
components: {
|
||||
RevealPassword,
|
||||
SidebarToggle,
|
||||
},
|
||||
props: {
|
||||
handleSubmit: {
|
||||
type: Function as PropType<(network: ClientNetwork) => void>,
|
||||
required: true,
|
||||
},
|
||||
defaults: {
|
||||
type: Object as PropType<NetworkFormDefaults>,
|
||||
required: true,
|
||||
},
|
||||
disabled: Boolean,
|
||||
},
|
||||
setup(props) {
|
||||
const store = useStore();
|
||||
const config = ref(store.state.serverConfiguration);
|
||||
const previousUsername = ref(props.defaults?.username);
|
||||
const displayPasswordField = ref(false);
|
||||
|
||||
const publicPassword = ref<HTMLInputElement | null>(null);
|
||||
|
||||
watch(displayPasswordField, (newValue) => {
|
||||
if (newValue) {
|
||||
void nextTick(() => {
|
||||
publicPassword.value?.focus();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const commandsInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const resizeCommandsInput = () => {
|
||||
if (!commandsInput.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset height first so it can down size
|
||||
commandsInput.value.style.height = "";
|
||||
|
||||
// 2 pixels to account for the border
|
||||
commandsInput.value.style.height = `${Math.ceil(
|
||||
commandsInput.value.scrollHeight + 2
|
||||
)}px`;
|
||||
};
|
||||
|
||||
watch(
|
||||
// eslint-disable-next-line
|
||||
() => props.defaults?.commands,
|
||||
() => {
|
||||
void nextTick(() => {
|
||||
resizeCommandsInput();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
// eslint-disable-next-line
|
||||
() => props.defaults?.tls,
|
||||
(isSecureChecked) => {
|
||||
const ports = [6667, 6697];
|
||||
const newPort = isSecureChecked ? 0 : 1;
|
||||
|
||||
// If you disable TLS and current port is 6697,
|
||||
// set it to 6667, and vice versa
|
||||
if (props.defaults?.port === ports[newPort]) {
|
||||
props.defaults.port = ports[1 - newPort];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const setSaslAuth = (type: string) => {
|
||||
if (props.defaults) {
|
||||
props.defaults.sasl = type;
|
||||
}
|
||||
};
|
||||
|
||||
const usernameInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
const onNickChanged = (event: Event) => {
|
||||
if (!usernameInput.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const usernameRef = usernameInput.value;
|
||||
|
||||
if (!usernameRef.value || usernameRef.value === previousUsername.value) {
|
||||
usernameRef.value = (event.target as HTMLInputElement)?.value;
|
||||
}
|
||||
|
||||
previousUsername.value = (event.target as HTMLInputElement)?.value;
|
||||
};
|
||||
|
||||
const onSubmit = (event: Event) => {
|
||||
const formData = new FormData(event.target as HTMLFormElement);
|
||||
const data: Partial<ClientNetwork> = {};
|
||||
|
||||
formData.forEach((value, key) => {
|
||||
data[key] = value;
|
||||
});
|
||||
|
||||
props.handleSubmit(data as ClientNetwork);
|
||||
};
|
||||
|
||||
return {
|
||||
store,
|
||||
config,
|
||||
displayPasswordField,
|
||||
publicPassword,
|
||||
commandsInput,
|
||||
resizeCommandsInput,
|
||||
setSaslAuth,
|
||||
usernameInput,
|
||||
onNickChanged,
|
||||
onSubmit,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
576
client/components/NetworkList.vue
Normal file
576
client/components/NetworkList.vue
Normal file
|
|
@ -0,0 +1,576 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="store.state.networks.length === 0"
|
||||
class="empty"
|
||||
role="navigation"
|
||||
aria-label="Network and Channel list"
|
||||
>
|
||||
You are not connected to any networks yet.
|
||||
</div>
|
||||
<div v-else ref="networklist" role="navigation" aria-label="Network and Channel list">
|
||||
<div class="jump-to-input">
|
||||
<input
|
||||
ref="searchInput"
|
||||
:value="searchText"
|
||||
placeholder="Jump to..."
|
||||
type="search"
|
||||
class="search input mousetrap"
|
||||
aria-label="Search among the channel list"
|
||||
tabindex="-1"
|
||||
@input="setSearchText"
|
||||
@keydown.up="navigateResults($event, -1)"
|
||||
@keydown.down="navigateResults($event, 1)"
|
||||
@keydown.page-up="navigateResults($event, -10)"
|
||||
@keydown.page-down="navigateResults($event, 10)"
|
||||
@keydown.enter="selectResult"
|
||||
@keydown.escape="deactivateSearch"
|
||||
@focus="activateSearch"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="searchText" class="jump-to-results">
|
||||
<div v-if="results.length">
|
||||
<div
|
||||
v-for="item in results"
|
||||
:key="item.channel.id"
|
||||
@mouseenter="setActiveSearchItem(item.channel)"
|
||||
@click.prevent="selectResult"
|
||||
>
|
||||
<Channel
|
||||
v-if="item.channel.type !== 'lobby'"
|
||||
:channel="item.channel"
|
||||
:network="item.network"
|
||||
:active="item.channel === activeSearchItem"
|
||||
:is-filtering="true"
|
||||
/>
|
||||
<NetworkLobby
|
||||
v-else
|
||||
:channel="item.channel"
|
||||
:network="item.network"
|
||||
:active="item.channel === activeSearchItem"
|
||||
:is-filtering="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-results">No results found.</div>
|
||||
</div>
|
||||
<Draggable
|
||||
v-else
|
||||
:list="store.state.networks"
|
||||
:delay="LONG_TOUCH_DURATION"
|
||||
:delay-on-touch-only="true"
|
||||
:touch-start-threshold="10"
|
||||
handle=".channel-list-item[data-type='lobby']"
|
||||
draggable=".network"
|
||||
ghost-class="ui-sortable-ghost"
|
||||
drag-class="ui-sortable-dragging"
|
||||
group="networks"
|
||||
class="networks"
|
||||
item-key="uuid"
|
||||
@change="onNetworkSort"
|
||||
@choose="onDraggableChoose"
|
||||
@unchoose="onDraggableUnchoose"
|
||||
>
|
||||
<template v-slot:item="{element: network}">
|
||||
<div
|
||||
:id="'network-' + network.uuid"
|
||||
:key="network.uuid"
|
||||
:class="{
|
||||
collapsed: network.isCollapsed,
|
||||
'not-connected': !network.status.connected,
|
||||
'not-secure': !network.status.secure,
|
||||
}"
|
||||
class="network"
|
||||
role="region"
|
||||
aria-live="polite"
|
||||
@touchstart="onDraggableTouchStart"
|
||||
@touchmove="onDraggableTouchMove"
|
||||
@touchend="onDraggableTouchEnd"
|
||||
@touchcancel="onDraggableTouchEnd"
|
||||
>
|
||||
<NetworkLobby
|
||||
:network="network"
|
||||
:is-join-channel-shown="network.isJoinChannelShown"
|
||||
:active="
|
||||
store.state.activeChannel &&
|
||||
network.channels[0] === store.state.activeChannel.channel
|
||||
"
|
||||
@toggle-join-channel="
|
||||
network.isJoinChannelShown = !network.isJoinChannelShown
|
||||
"
|
||||
/>
|
||||
<JoinChannel
|
||||
v-if="network.isJoinChannelShown"
|
||||
:network="network"
|
||||
:channel="network.channels[0]"
|
||||
@toggle-join-channel="
|
||||
network.isJoinChannelShown = !network.isJoinChannelShown
|
||||
"
|
||||
/>
|
||||
|
||||
<Draggable
|
||||
draggable=".channel-list-item"
|
||||
ghost-class="ui-sortable-ghost"
|
||||
drag-class="ui-sortable-dragging"
|
||||
:group="network.uuid"
|
||||
:list="network.channels"
|
||||
:delay="LONG_TOUCH_DURATION"
|
||||
:delay-on-touch-only="true"
|
||||
:touch-start-threshold="10"
|
||||
class="channels"
|
||||
item-key="name"
|
||||
@change="onChannelSort"
|
||||
@choose="onDraggableChoose"
|
||||
@unchoose="onDraggableUnchoose"
|
||||
>
|
||||
<template v-slot:item="{element: channel, index}">
|
||||
<Channel
|
||||
v-if="index > 0"
|
||||
:key="channel.id"
|
||||
:data-item="channel.id"
|
||||
:channel="channel"
|
||||
:network="network"
|
||||
:active="
|
||||
store.state.activeChannel &&
|
||||
channel === store.state.activeChannel.channel
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</Draggable>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.jump-to-input {
|
||||
margin: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.jump-to-input .input {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
color: #fff;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
padding-right: 35px;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.jump-to-input .input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.jump-to-input::before {
|
||||
content: "\f002"; /* http://fontawesome.io/icon/search/ */
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
line-height: 35px !important;
|
||||
}
|
||||
|
||||
.jump-to-results {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.jump-to-results .no-results {
|
||||
margin: 14px 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.jump-to-results .channel-list-item.active {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.jump-to-results .channel-list-item .add-channel,
|
||||
.jump-to-results .channel-list-item .close-tooltip {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.jump-to-results .channel-list-item[data-type="lobby"] {
|
||||
padding: 8px 14px;
|
||||
}
|
||||
|
||||
.jump-to-results .channel-list-item[data-type="lobby"]::before {
|
||||
content: "\f233";
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, watch, defineComponent, nextTick, onBeforeUnmount, onMounted, ref} from "vue";
|
||||
|
||||
import Mousetrap from "mousetrap";
|
||||
import Draggable from "./Draggable.vue";
|
||||
import {filter as fuzzyFilter} from "fuzzy";
|
||||
import NetworkLobby from "./NetworkLobby.vue";
|
||||
import Channel from "./Channel.vue";
|
||||
import JoinChannel from "./JoinChannel.vue";
|
||||
|
||||
import socket from "../js/socket";
|
||||
import collapseNetworkHelper from "../js/helpers/collapseNetwork";
|
||||
import isIgnoredKeybind from "../js/helpers/isIgnoredKeybind";
|
||||
import distance from "../js/helpers/distance";
|
||||
import eventbus from "../js/eventbus";
|
||||
import {ClientChan, NetChan} from "../js/types";
|
||||
import {useStore} from "../js/store";
|
||||
import {switchToChannel} from "../js/router";
|
||||
import Sortable from "sortablejs";
|
||||
|
||||
export default defineComponent({
|
||||
name: "NetworkList",
|
||||
components: {
|
||||
JoinChannel,
|
||||
NetworkLobby,
|
||||
Channel,
|
||||
Draggable,
|
||||
},
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const searchText = ref("");
|
||||
const activeSearchItem = ref<ClientChan | null>();
|
||||
// Number of milliseconds a touch has to last to be considered long
|
||||
const LONG_TOUCH_DURATION = 500;
|
||||
|
||||
const startDrag = ref<[number, number] | null>();
|
||||
const searchInput = ref<HTMLInputElement | null>(null);
|
||||
const networklist = ref<HTMLDivElement | null>(null);
|
||||
|
||||
const sidebarWasClosed = ref(false);
|
||||
|
||||
const moveItemInArray = <T>(array: T[], from: number, to: number) => {
|
||||
const item = array.splice(from, 1)[0];
|
||||
array.splice(to, 0, item);
|
||||
};
|
||||
|
||||
const items = computed(() => {
|
||||
const newItems: NetChan[] = [];
|
||||
|
||||
for (const network of store.state.networks) {
|
||||
for (const channel of network.channels) {
|
||||
if (
|
||||
store.state.activeChannel &&
|
||||
channel === store.state.activeChannel.channel
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
newItems.push({network, channel});
|
||||
}
|
||||
}
|
||||
|
||||
return newItems;
|
||||
});
|
||||
|
||||
const results = computed(() => {
|
||||
const newResults = fuzzyFilter(searchText.value, items.value, {
|
||||
extract: (item) => item.channel.name,
|
||||
}).map((item) => item.original);
|
||||
|
||||
return newResults;
|
||||
});
|
||||
|
||||
const collapseNetwork = (event: Mousetrap.ExtendedKeyboardEvent) => {
|
||||
if (isIgnoredKeybind(event)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (store.state.activeChannel) {
|
||||
collapseNetworkHelper(store.state.activeChannel.network, true);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const expandNetwork = (event: Mousetrap.ExtendedKeyboardEvent) => {
|
||||
if (isIgnoredKeybind(event)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (store.state.activeChannel) {
|
||||
collapseNetworkHelper(store.state.activeChannel.network, false);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const onNetworkSort = (e: Sortable.SortableEvent) => {
|
||||
const {oldIndex, newIndex} = e;
|
||||
|
||||
if (oldIndex === undefined || newIndex === undefined || oldIndex === newIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
moveItemInArray(store.state.networks, oldIndex, newIndex);
|
||||
|
||||
socket.emit("sort:networks", {
|
||||
order: store.state.networks.map((n) => n.uuid),
|
||||
});
|
||||
};
|
||||
|
||||
const onChannelSort = (e: Sortable.SortableEvent) => {
|
||||
let {oldIndex, newIndex} = e;
|
||||
|
||||
if (oldIndex === undefined || newIndex === undefined || oldIndex === newIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Indexes are offset by one due to the lobby
|
||||
oldIndex += 1;
|
||||
newIndex += 1;
|
||||
|
||||
const unparsedId = e.item.getAttribute("data-item");
|
||||
|
||||
if (!unparsedId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = parseInt(unparsedId);
|
||||
const netChan = store.getters.findChannel(id);
|
||||
|
||||
if (!netChan) {
|
||||
return;
|
||||
}
|
||||
|
||||
moveItemInArray(netChan.network.channels, oldIndex, newIndex);
|
||||
|
||||
socket.emit("sort:channels", {
|
||||
network: netChan.network.uuid,
|
||||
order: netChan.network.channels.map((c) => c.id),
|
||||
});
|
||||
};
|
||||
|
||||
const isTouchEvent = (event: any): boolean => {
|
||||
// This is the same way Sortable.js detects a touch event. See
|
||||
// SortableJS/Sortable@daaefeda:/src/Sortable.js#L465
|
||||
|
||||
return !!(
|
||||
(event.touches && event.touches[0]) ||
|
||||
(event.pointerType && event.pointerType === "touch")
|
||||
);
|
||||
};
|
||||
|
||||
const onDraggableChoose = (event: any) => {
|
||||
const original = event.originalEvent;
|
||||
|
||||
if (isTouchEvent(original)) {
|
||||
// onDrag is only triggered when the user actually moves the
|
||||
// dragged object but onChoose is triggered as soon as the
|
||||
// item is eligible for dragging. This gives us an opportunity
|
||||
// to tell the user they've held the touch long enough.
|
||||
event.item.classList.add("ui-sortable-dragging-touch-cue");
|
||||
|
||||
if (original instanceof TouchEvent && original.touches.length > 0) {
|
||||
startDrag.value = [original.touches[0].clientX, original.touches[0].clientY];
|
||||
} else if (original instanceof PointerEvent) {
|
||||
startDrag.value = [original.clientX, original.clientY];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onDraggableUnchoose = (event: any) => {
|
||||
event.item.classList.remove("ui-sortable-dragging-touch-cue");
|
||||
startDrag.value = null;
|
||||
};
|
||||
|
||||
const onDraggableTouchStart = (event: TouchEvent) => {
|
||||
if (event.touches.length === 1) {
|
||||
// This prevents an iOS long touch default behavior: selecting
|
||||
// the nearest selectable text.
|
||||
document.body.classList.add("force-no-select");
|
||||
}
|
||||
};
|
||||
|
||||
const onDraggableTouchMove = (event: TouchEvent) => {
|
||||
if (startDrag.value && event.touches.length > 0) {
|
||||
const touch = event.touches[0];
|
||||
const currentPosition = [touch.clientX, touch.clientY];
|
||||
|
||||
if (distance(startDrag.value, currentPosition as [number, number]) > 10) {
|
||||
// Context menu is shown on Android after long touch.
|
||||
// Dismiss it now that we're sure the user is dragging.
|
||||
eventbus.emit("contextmenu:cancel");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onDraggableTouchEnd = (event: TouchEvent) => {
|
||||
if (event.touches.length === 0) {
|
||||
document.body.classList.remove("force-no-select");
|
||||
}
|
||||
};
|
||||
|
||||
const activateSearch = () => {
|
||||
if (searchInput.value === document.activeElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
sidebarWasClosed.value = store.state.sidebarOpen ? false : true;
|
||||
store.commit("sidebarOpen", true);
|
||||
|
||||
void nextTick(() => {
|
||||
searchInput.value?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
const deactivateSearch = () => {
|
||||
activeSearchItem.value = null;
|
||||
searchText.value = "";
|
||||
searchInput.value?.blur();
|
||||
|
||||
if (sidebarWasClosed.value) {
|
||||
store.commit("sidebarOpen", false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSearch = (event: Mousetrap.ExtendedKeyboardEvent) => {
|
||||
if (isIgnoredKeybind(event)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (searchInput.value === document.activeElement) {
|
||||
deactivateSearch();
|
||||
return false;
|
||||
}
|
||||
|
||||
activateSearch();
|
||||
return false;
|
||||
};
|
||||
|
||||
const setSearchText = (e: Event) => {
|
||||
searchText.value = (e.target as HTMLInputElement).value;
|
||||
};
|
||||
|
||||
const setActiveSearchItem = (channel?: ClientChan) => {
|
||||
if (!results.value.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!channel) {
|
||||
channel = results.value[0].channel;
|
||||
}
|
||||
|
||||
activeSearchItem.value = channel;
|
||||
};
|
||||
|
||||
const scrollToActive = () => {
|
||||
// Scroll the list if needed after the active class is applied
|
||||
void nextTick(() => {
|
||||
const el = networklist.value?.querySelector(".channel-list-item.active");
|
||||
|
||||
if (el) {
|
||||
el.scrollIntoView({block: "nearest", inline: "nearest"});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const selectResult = () => {
|
||||
if (!searchText.value || !results.value.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeSearchItem.value) {
|
||||
switchToChannel(activeSearchItem.value);
|
||||
deactivateSearch();
|
||||
scrollToActive();
|
||||
}
|
||||
};
|
||||
|
||||
const navigateResults = (event: Event, direction: number) => {
|
||||
// Prevent propagation to stop global keybind handler from capturing pagedown/pageup
|
||||
// and redirecting it to the message list container for scrolling
|
||||
event.stopImmediatePropagation();
|
||||
event.preventDefault();
|
||||
|
||||
if (!searchText.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channels = results.value.map((r) => r.channel);
|
||||
|
||||
// Bail out if there's no channels to select
|
||||
if (!channels.length) {
|
||||
activeSearchItem.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
let currentIndex = activeSearchItem.value
|
||||
? channels.indexOf(activeSearchItem.value)
|
||||
: -1;
|
||||
|
||||
// If there's no active channel select the first or last one depending on direction
|
||||
if (!activeSearchItem.value || currentIndex === -1) {
|
||||
activeSearchItem.value = direction ? channels[0] : channels[channels.length - 1];
|
||||
scrollToActive();
|
||||
return;
|
||||
}
|
||||
|
||||
currentIndex += direction;
|
||||
|
||||
// Wrap around the list if necessary. Normaly each loop iterates once at most,
|
||||
// but might iterate more often if pgup or pgdown are used in a very short list
|
||||
while (currentIndex < 0) {
|
||||
currentIndex += channels.length;
|
||||
}
|
||||
|
||||
while (currentIndex > channels.length - 1) {
|
||||
currentIndex -= channels.length;
|
||||
}
|
||||
|
||||
activeSearchItem.value = channels[currentIndex];
|
||||
scrollToActive();
|
||||
};
|
||||
|
||||
watch(searchText, () => {
|
||||
setActiveSearchItem();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
Mousetrap.bind("alt+shift+right", expandNetwork);
|
||||
Mousetrap.bind("alt+shift+left", collapseNetwork);
|
||||
Mousetrap.bind("alt+j", toggleSearch);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
Mousetrap.unbind("alt+shift+right");
|
||||
Mousetrap.unbind("alt+shift+left");
|
||||
Mousetrap.unbind("alt+j");
|
||||
});
|
||||
|
||||
const networkContainerRef = ref<HTMLDivElement>();
|
||||
const channelRefs = ref<{[key: string]: HTMLDivElement}>({});
|
||||
|
||||
return {
|
||||
store,
|
||||
networklist,
|
||||
searchInput,
|
||||
searchText,
|
||||
results,
|
||||
activeSearchItem,
|
||||
LONG_TOUCH_DURATION,
|
||||
|
||||
activateSearch,
|
||||
deactivateSearch,
|
||||
toggleSearch,
|
||||
setSearchText,
|
||||
setActiveSearchItem,
|
||||
scrollToActive,
|
||||
selectResult,
|
||||
navigateResults,
|
||||
onChannelSort,
|
||||
onNetworkSort,
|
||||
onDraggableTouchStart,
|
||||
onDraggableTouchMove,
|
||||
onDraggableTouchEnd,
|
||||
onDraggableChoose,
|
||||
onDraggableUnchoose,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
101
client/components/NetworkLobby.vue
Normal file
101
client/components/NetworkLobby.vue
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<template>
|
||||
<ChannelWrapper v-bind="$props" :channel="channel">
|
||||
<button
|
||||
v-if="network.channels.length > 1"
|
||||
:aria-controls="'network-' + network.uuid"
|
||||
:aria-label="getExpandLabel(network)"
|
||||
:aria-expanded="!network.isCollapsed"
|
||||
class="collapse-network"
|
||||
@click.stop="onCollapseClick"
|
||||
>
|
||||
<span class="collapse-network-icon" />
|
||||
</button>
|
||||
<span v-else class="collapse-network" />
|
||||
<div class="lobby-wrap">
|
||||
<span :title="channel.name" class="name">{{ channel.name }}</span>
|
||||
<span
|
||||
v-if="network.status.connected && !network.status.secure"
|
||||
class="not-secure-tooltip tooltipped tooltipped-w"
|
||||
aria-label="Insecure connection"
|
||||
>
|
||||
<span class="not-secure-icon" />
|
||||
</span>
|
||||
<span
|
||||
v-if="!network.status.connected"
|
||||
class="not-connected-tooltip tooltipped tooltipped-w"
|
||||
aria-label="Disconnected"
|
||||
>
|
||||
<span class="not-connected-icon" />
|
||||
</span>
|
||||
<span v-if="channel.unread" :class="{highlight: channel.highlight}" class="badge">{{
|
||||
unreadCount
|
||||
}}</span>
|
||||
</div>
|
||||
<span
|
||||
:aria-label="joinChannelLabel"
|
||||
class="add-channel-tooltip tooltipped tooltipped-w tooltipped-no-touch"
|
||||
>
|
||||
<button
|
||||
:class="['add-channel', {opened: isJoinChannelShown}]"
|
||||
:aria-controls="'join-channel-' + channel.id"
|
||||
:aria-label="joinChannelLabel"
|
||||
@click.stop="$emit('toggle-join-channel')"
|
||||
/>
|
||||
</span>
|
||||
</ChannelWrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, PropType} from "vue";
|
||||
import collapseNetwork from "../js/helpers/collapseNetwork";
|
||||
import roundBadgeNumber from "../js/helpers/roundBadgeNumber";
|
||||
import ChannelWrapper from "./ChannelWrapper.vue";
|
||||
|
||||
import type {ClientChan, ClientNetwork} from "../js/types";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Channel",
|
||||
components: {
|
||||
ChannelWrapper,
|
||||
},
|
||||
props: {
|
||||
network: {
|
||||
type: Object as PropType<ClientNetwork>,
|
||||
required: true,
|
||||
},
|
||||
isJoinChannelShown: Boolean,
|
||||
active: Boolean,
|
||||
isFiltering: Boolean,
|
||||
},
|
||||
emits: ["toggle-join-channel"],
|
||||
setup(props) {
|
||||
const channel = computed(() => {
|
||||
return props.network.channels[0];
|
||||
});
|
||||
|
||||
const joinChannelLabel = computed(() => {
|
||||
return props.isJoinChannelShown ? "Cancel" : "Join a channel…";
|
||||
});
|
||||
|
||||
const unreadCount = computed(() => {
|
||||
return roundBadgeNumber(channel.value.unread);
|
||||
});
|
||||
|
||||
const onCollapseClick = () => {
|
||||
collapseNetwork(props.network, !props.network.isCollapsed);
|
||||
};
|
||||
|
||||
const getExpandLabel = (network: ClientNetwork) => {
|
||||
return network.isCollapsed ? "Expand" : "Collapse";
|
||||
};
|
||||
|
||||
return {
|
||||
channel,
|
||||
joinChannelLabel,
|
||||
unreadCount,
|
||||
onCollapseClick,
|
||||
getExpandLabel,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
22
client/components/ParsedMessage.vue
Normal file
22
client/components/ParsedMessage.vue
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<script lang="ts">
|
||||
import {defineComponent, PropType, h} from "vue";
|
||||
import parse from "../js/helpers/parse";
|
||||
import type {ClientMessage, ClientNetwork} from "../js/types";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ParsedMessage",
|
||||
functional: true,
|
||||
props: {
|
||||
text: String,
|
||||
message: {type: Object as PropType<ClientMessage | string>, required: false},
|
||||
network: {type: Object as PropType<ClientNetwork>, required: false},
|
||||
},
|
||||
render(context) {
|
||||
return parse(
|
||||
typeof context.text !== "undefined" ? context.text : context.message.text,
|
||||
context.message,
|
||||
context.network
|
||||
);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
37
client/components/RevealPassword.vue
Normal file
37
client/components/RevealPassword.vue
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<div>
|
||||
<slot :is-visible="isVisible" />
|
||||
<span
|
||||
ref="revealButton"
|
||||
type="button"
|
||||
:class="[
|
||||
'reveal-password tooltipped tooltipped-n tooltipped-no-delay',
|
||||
{'reveal-password-visible': isVisible},
|
||||
]"
|
||||
:aria-label="isVisible ? 'Hide password' : 'Show password'"
|
||||
@click="onClick"
|
||||
>
|
||||
<span :aria-label="isVisible ? 'Hide password' : 'Show password'" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, ref} from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "RevealPassword",
|
||||
setup() {
|
||||
const isVisible = ref(false);
|
||||
|
||||
const onClick = () => {
|
||||
isVisible.value = !isVisible.value;
|
||||
};
|
||||
|
||||
return {
|
||||
isVisible,
|
||||
onClick,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
66
client/components/RoutedChat.vue
Normal file
66
client/components/RoutedChat.vue
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<template>
|
||||
<Chat
|
||||
v-if="activeChannel"
|
||||
:network="activeChannel.network"
|
||||
:channel="activeChannel.channel"
|
||||
:focused="parseInt(String(route.query.focused), 10)"
|
||||
@channel-changed="channelChanged"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {watch, computed, defineComponent, onMounted} from "vue";
|
||||
import {useRoute} from "vue-router";
|
||||
import {useStore} from "../js/store";
|
||||
import {ClientChan} from "../js/types";
|
||||
|
||||
// Temporary component for routing channels and lobbies
|
||||
import Chat from "./Chat.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "RoutedChat",
|
||||
components: {
|
||||
Chat,
|
||||
},
|
||||
setup() {
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
|
||||
const activeChannel = computed(() => {
|
||||
const chanId = parseInt(String(route.params.id || ""), 10);
|
||||
const channel = store.getters.findChannel(chanId);
|
||||
return channel;
|
||||
});
|
||||
|
||||
const setActiveChannel = () => {
|
||||
if (activeChannel.value) {
|
||||
store.commit("activeChannel", activeChannel.value);
|
||||
}
|
||||
};
|
||||
|
||||
watch(activeChannel, () => {
|
||||
setActiveChannel();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
setActiveChannel();
|
||||
});
|
||||
|
||||
const channelChanged = (channel: ClientChan) => {
|
||||
const chanId = channel.id;
|
||||
const chanInStore = store.getters.findChannel(chanId);
|
||||
|
||||
if (chanInStore?.channel) {
|
||||
chanInStore.channel.unread = 0;
|
||||
chanInStore.channel.highlight = 0;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
route,
|
||||
activeChannel,
|
||||
channelChanged,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
83
client/components/Session.vue
Normal file
83
client/components/Session.vue
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<template>
|
||||
<div class="session-item">
|
||||
<div class="session-item-info">
|
||||
<strong>{{ session.agent }}</strong>
|
||||
|
||||
<a :href="'https://ipinfo.io/' + session.ip" target="_blank" rel="noopener">{{
|
||||
session.ip
|
||||
}}</a>
|
||||
|
||||
<p v-if="session.active > 1" class="session-usage">
|
||||
Active in {{ session.active }} browsers
|
||||
</p>
|
||||
<p v-else-if="!session.current && !session.active" class="session-usage">
|
||||
Last used on <time>{{ lastUse }}</time>
|
||||
</p>
|
||||
</div>
|
||||
<div class="session-item-btn">
|
||||
<button class="btn" @click.prevent="signOut">
|
||||
<template v-if="session.current">Sign out</template>
|
||||
<template v-else>Revoke</template>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.session-list .session-item {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.session-list .session-item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.session-list .session-item-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.session-list .session-usage {
|
||||
font-style: italic;
|
||||
color: var(--body-color-muted);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, PropType} from "vue";
|
||||
import localetime from "../js/helpers/localetime";
|
||||
import Auth from "../js/auth";
|
||||
import socket from "../js/socket";
|
||||
import {ClientSession} from "../js/store";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Session",
|
||||
props: {
|
||||
session: {
|
||||
type: Object as PropType<ClientSession>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const lastUse = computed(() => {
|
||||
return localetime(props.session.lastUse);
|
||||
});
|
||||
|
||||
const signOut = () => {
|
||||
if (!props.session.current) {
|
||||
socket.emit("sign-out", props.session.token);
|
||||
} else {
|
||||
socket.emit("sign-out");
|
||||
Auth.signout();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
lastUse,
|
||||
signOut,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
197
client/components/Settings/Account.vue
Normal file
197
client/components/Settings/Account.vue
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="
|
||||
!store.state.serverConfiguration?.public &&
|
||||
!store.state.serverConfiguration?.ldapEnabled
|
||||
"
|
||||
id="change-password"
|
||||
role="group"
|
||||
aria-labelledby="label-change-password"
|
||||
>
|
||||
<h2 id="label-change-password">Change password</h2>
|
||||
<div class="password-container">
|
||||
<label for="current-password" class="sr-only"> Enter current password </label>
|
||||
<RevealPassword v-slot:default="slotProps">
|
||||
<input
|
||||
id="current-password"
|
||||
v-model="old_password"
|
||||
autocomplete="current-password"
|
||||
:type="slotProps.isVisible ? 'text' : 'password'"
|
||||
name="old_password"
|
||||
class="input"
|
||||
placeholder="Enter current password"
|
||||
/>
|
||||
</RevealPassword>
|
||||
</div>
|
||||
<div class="password-container">
|
||||
<label for="new-password" class="sr-only"> Enter desired new password </label>
|
||||
<RevealPassword v-slot:default="slotProps">
|
||||
<input
|
||||
id="new-password"
|
||||
v-model="new_password"
|
||||
:type="slotProps.isVisible ? 'text' : 'password'"
|
||||
name="new_password"
|
||||
autocomplete="new-password"
|
||||
class="input"
|
||||
placeholder="Enter desired new password"
|
||||
/>
|
||||
</RevealPassword>
|
||||
</div>
|
||||
<div class="password-container">
|
||||
<label for="new-password-verify" class="sr-only"> Repeat new password </label>
|
||||
<RevealPassword v-slot:default="slotProps">
|
||||
<input
|
||||
id="new-password-verify"
|
||||
v-model="verify_password"
|
||||
:type="slotProps.isVisible ? 'text' : 'password'"
|
||||
name="verify_password"
|
||||
autocomplete="new-password"
|
||||
class="input"
|
||||
placeholder="Repeat new password"
|
||||
/>
|
||||
</RevealPassword>
|
||||
</div>
|
||||
<div
|
||||
v-if="passwordChangeStatus && passwordChangeStatus.success"
|
||||
class="feedback success"
|
||||
>
|
||||
Successfully updated your password
|
||||
</div>
|
||||
<div
|
||||
v-else-if="passwordChangeStatus && passwordChangeStatus.error"
|
||||
class="feedback error"
|
||||
>
|
||||
{{ passwordErrors[passwordChangeStatus.error] }}
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="btn" @click.prevent="changePassword">
|
||||
Change password
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!store.state.serverConfiguration?.public" class="session-list" role="group">
|
||||
<h2>Sessions</h2>
|
||||
|
||||
<h3>Current session</h3>
|
||||
<Session v-if="currentSession" :session="currentSession" />
|
||||
|
||||
<template v-if="activeSessions.length > 0">
|
||||
<h3>Active sessions</h3>
|
||||
<Session
|
||||
v-for="session in activeSessions"
|
||||
:key="session.token"
|
||||
:session="session"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<h3>Other sessions</h3>
|
||||
<p v-if="store.state.sessions.length === 0">Loading…</p>
|
||||
<p v-else-if="otherSessions.length === 0">
|
||||
<em>You are not currently logged in to any other device.</em>
|
||||
</p>
|
||||
<Session
|
||||
v-for="session in otherSessions"
|
||||
v-else
|
||||
:key="session.token"
|
||||
:session="session"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import socket from "../../js/socket";
|
||||
import RevealPassword from "../RevealPassword.vue";
|
||||
import Session from "../Session.vue";
|
||||
import {computed, defineComponent, onMounted, PropType, ref} from "vue";
|
||||
import {useStore} from "../../js/store";
|
||||
|
||||
export default defineComponent({
|
||||
name: "UserSettings",
|
||||
components: {
|
||||
RevealPassword,
|
||||
Session,
|
||||
},
|
||||
setup() {
|
||||
const store = useStore();
|
||||
|
||||
const passwordErrors = {
|
||||
missing_fields: "Please fill in all fields",
|
||||
password_mismatch: "Both new password fields must match",
|
||||
password_incorrect: "The current password field does not match your account password",
|
||||
update_failed: "Failed to update your password",
|
||||
};
|
||||
|
||||
const passwordChangeStatus = ref<{
|
||||
success: boolean;
|
||||
error: keyof typeof passwordErrors;
|
||||
}>();
|
||||
|
||||
const old_password = ref("");
|
||||
const new_password = ref("");
|
||||
const verify_password = ref("");
|
||||
|
||||
const currentSession = computed(() => {
|
||||
return store.state.sessions.find((item) => item.current);
|
||||
});
|
||||
|
||||
const activeSessions = computed(() => {
|
||||
return store.state.sessions.filter((item) => !item.current && item.active > 0);
|
||||
});
|
||||
|
||||
const otherSessions = computed(() => {
|
||||
return store.state.sessions.filter((item) => !item.current && !item.active);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
socket.emit("sessions:get");
|
||||
});
|
||||
|
||||
const changePassword = () => {
|
||||
const data = {
|
||||
old_password: old_password.value,
|
||||
new_password: new_password.value,
|
||||
verify_password: verify_password.value,
|
||||
};
|
||||
|
||||
if (!data.old_password || !data.new_password || !data.verify_password) {
|
||||
passwordChangeStatus.value = {
|
||||
success: false,
|
||||
error: "missing_fields",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.new_password !== data.verify_password) {
|
||||
passwordChangeStatus.value = {
|
||||
success: false,
|
||||
error: "password_mismatch",
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
socket.once("change-password", (response) => {
|
||||
// TODO type
|
||||
passwordChangeStatus.value = response as any;
|
||||
});
|
||||
|
||||
socket.emit("change-password", data);
|
||||
};
|
||||
|
||||
return {
|
||||
store,
|
||||
passwordChangeStatus,
|
||||
passwordErrors,
|
||||
currentSession,
|
||||
activeSessions,
|
||||
otherSessions,
|
||||
changePassword,
|
||||
old_password,
|
||||
new_password,
|
||||
verify_password,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
179
client/components/Settings/Appearance.vue
Normal file
179
client/components/Settings/Appearance.vue
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
<template>
|
||||
<div>
|
||||
<h2>Messages</h2>
|
||||
<div>
|
||||
<label class="opt">
|
||||
<input :checked="store.state.settings.motd" type="checkbox" name="motd" />
|
||||
Show <abbr title="Message Of The Day">MOTD</abbr>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="opt">
|
||||
<input
|
||||
:checked="store.state.settings.showSeconds"
|
||||
type="checkbox"
|
||||
name="showSeconds"
|
||||
/>
|
||||
Include seconds in timestamp
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="opt">
|
||||
<input
|
||||
:checked="store.state.settings.use12hClock"
|
||||
type="checkbox"
|
||||
name="use12hClock"
|
||||
/>
|
||||
Use 12-hour timestamps
|
||||
</label>
|
||||
</div>
|
||||
<template v-if="store.state.serverConfiguration?.prefetch">
|
||||
<h2>Link previews</h2>
|
||||
<div>
|
||||
<label class="opt">
|
||||
<input :checked="store.state.settings.media" type="checkbox" name="media" />
|
||||
Auto-expand media
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="opt">
|
||||
<input :checked="store.state.settings.links" type="checkbox" name="links" />
|
||||
Auto-expand websites
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<h2 id="label-status-messages">
|
||||
Status messages
|
||||
<span
|
||||
class="tooltipped tooltipped-n tooltipped-no-delay"
|
||||
aria-label="Joins, parts, quits, kicks, nick changes, and mode changes"
|
||||
>
|
||||
<button class="extra-help" />
|
||||
</span>
|
||||
</h2>
|
||||
<div role="group" aria-labelledby="label-status-messages">
|
||||
<label class="opt">
|
||||
<input
|
||||
:checked="store.state.settings.statusMessages === 'shown'"
|
||||
type="radio"
|
||||
name="statusMessages"
|
||||
value="shown"
|
||||
/>
|
||||
Show all status messages individually
|
||||
</label>
|
||||
<label class="opt">
|
||||
<input
|
||||
:checked="store.state.settings.statusMessages === 'condensed'"
|
||||
type="radio"
|
||||
name="statusMessages"
|
||||
value="condensed"
|
||||
/>
|
||||
Condense status messages together
|
||||
</label>
|
||||
<label class="opt">
|
||||
<input
|
||||
:checked="store.state.settings.statusMessages === 'hidden'"
|
||||
type="radio"
|
||||
name="statusMessages"
|
||||
value="hidden"
|
||||
/>
|
||||
Hide all status messages
|
||||
</label>
|
||||
</div>
|
||||
<h2>Visual Aids</h2>
|
||||
<div>
|
||||
<label class="opt">
|
||||
<input
|
||||
:checked="store.state.settings.coloredNicks"
|
||||
type="checkbox"
|
||||
name="coloredNicks"
|
||||
/>
|
||||
Enable colored nicknames
|
||||
</label>
|
||||
<label class="opt">
|
||||
<input
|
||||
:checked="store.state.settings.autocomplete"
|
||||
type="checkbox"
|
||||
name="autocomplete"
|
||||
/>
|
||||
Enable autocomplete
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="opt">
|
||||
<label for="nickPostfix" class="opt">
|
||||
Nick autocomplete postfix
|
||||
<span
|
||||
class="tooltipped tooltipped-n tooltipped-no-delay"
|
||||
aria-label="Nick autocomplete postfix (for example a comma)"
|
||||
>
|
||||
<button class="extra-help" />
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="nickPostfix"
|
||||
:value="store.state.settings.nickPostfix"
|
||||
type="text"
|
||||
name="nickPostfix"
|
||||
class="input"
|
||||
placeholder="Nick autocomplete postfix (e.g. ', ')"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h2>Theme</h2>
|
||||
<div>
|
||||
<label for="theme-select" class="sr-only">Theme</label>
|
||||
<select
|
||||
id="theme-select"
|
||||
:value="store.state.settings.theme"
|
||||
name="theme"
|
||||
class="input"
|
||||
>
|
||||
<option
|
||||
v-for="theme in store.state.serverConfiguration?.themes"
|
||||
:key="theme.name"
|
||||
:value="theme.name"
|
||||
>
|
||||
{{ theme.displayName }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Custom Stylesheet</h2>
|
||||
<label for="user-specified-css-input" class="sr-only">
|
||||
Custom stylesheet. You can override any style with CSS here.
|
||||
</label>
|
||||
<textarea
|
||||
id="user-specified-css-input"
|
||||
:value="store.state.settings.userStyles"
|
||||
class="input"
|
||||
name="userStyles"
|
||||
placeholder="/* You can override any style with CSS here */"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
textarea#user-specified-css-input {
|
||||
height: 100px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from "vue";
|
||||
import {useStore} from "../../js/store";
|
||||
|
||||
export default defineComponent({
|
||||
name: "AppearanceSettings",
|
||||
setup() {
|
||||
const store = useStore();
|
||||
|
||||
return {
|
||||
store,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
175
client/components/Settings/General.vue
Normal file
175
client/components/Settings/General.vue
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="canRegisterProtocol || hasInstallPromptEvent">
|
||||
<h2>Native app</h2>
|
||||
<button
|
||||
v-if="hasInstallPromptEvent"
|
||||
type="button"
|
||||
class="btn"
|
||||
@click.prevent="nativeInstallPrompt"
|
||||
>
|
||||
Add The Lounge to Home screen
|
||||
</button>
|
||||
<button
|
||||
v-if="canRegisterProtocol"
|
||||
type="button"
|
||||
class="btn"
|
||||
@click.prevent="registerProtocol"
|
||||
>
|
||||
Open irc:// URLs with The Lounge
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="store.state.serverConfiguration?.fileUpload">
|
||||
<h2>File uploads</h2>
|
||||
<div>
|
||||
<label class="opt">
|
||||
<input
|
||||
:checked="store.state.settings.uploadCanvas"
|
||||
type="checkbox"
|
||||
name="uploadCanvas"
|
||||
/>
|
||||
Attempt to remove metadata from images before uploading
|
||||
<span
|
||||
class="tooltipped tooltipped-n tooltipped-no-delay"
|
||||
aria-label="This option renders the image into a canvas element to remove metadata from the image.
|
||||
This may break orientation if your browser does not support that."
|
||||
>
|
||||
<button class="extra-help" />
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!store.state.serverConfiguration?.public">
|
||||
<h2>Settings synchronisation</h2>
|
||||
<label class="opt">
|
||||
<input
|
||||
:checked="store.state.settings.syncSettings"
|
||||
type="checkbox"
|
||||
name="syncSettings"
|
||||
/>
|
||||
Synchronize settings with other clients
|
||||
</label>
|
||||
<template v-if="!store.state.settings.syncSettings">
|
||||
<div v-if="store.state.serverHasSettings" class="settings-sync-panel">
|
||||
<p>
|
||||
<strong>Warning:</strong> Checking this box will override the settings of
|
||||
this client with those stored on the server.
|
||||
</p>
|
||||
<p>
|
||||
Use the button below to enable synchronization, and override any settings
|
||||
already synced to the server.
|
||||
</p>
|
||||
<button type="button" class="btn btn-small" @click="onForceSyncClick">
|
||||
Sync settings and enable
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="settings-sync-panel">
|
||||
<p>
|
||||
<strong>Warning:</strong> No settings have been synced before. Enabling this
|
||||
will sync all settings of this client as the base for other clients.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="!store.state.serverConfiguration?.public">
|
||||
<h2>Automatic away message</h2>
|
||||
|
||||
<label class="opt">
|
||||
<label for="awayMessage" class="sr-only">Automatic away message</label>
|
||||
<input
|
||||
id="awayMessage"
|
||||
:value="store.state.settings.awayMessage"
|
||||
type="text"
|
||||
name="awayMessage"
|
||||
class="input"
|
||||
placeholder="Away message if The Lounge is not open"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style></style>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, onMounted, ref} from "vue";
|
||||
import {useStore} from "../../js/store";
|
||||
import {BeforeInstallPromptEvent} from "../../js/types";
|
||||
|
||||
let installPromptEvent: BeforeInstallPromptEvent | null = null;
|
||||
|
||||
window.addEventListener("beforeinstallprompt", (e) => {
|
||||
e.preventDefault();
|
||||
installPromptEvent = e as BeforeInstallPromptEvent;
|
||||
});
|
||||
|
||||
export default defineComponent({
|
||||
name: "GeneralSettings",
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const canRegisterProtocol = ref(false);
|
||||
|
||||
const hasInstallPromptEvent = computed(() => {
|
||||
// TODO: This doesn't hide the button after clicking
|
||||
return installPromptEvent !== null;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// Enable protocol handler registration if supported,
|
||||
// and the network configuration is not locked
|
||||
canRegisterProtocol.value =
|
||||
!!window.navigator.registerProtocolHandler &&
|
||||
!store.state.serverConfiguration?.lockNetwork;
|
||||
});
|
||||
|
||||
const nativeInstallPrompt = () => {
|
||||
if (!installPromptEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
installPromptEvent.prompt().catch((e) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
});
|
||||
|
||||
installPromptEvent = null;
|
||||
};
|
||||
|
||||
const onForceSyncClick = () => {
|
||||
store.dispatch("settings/syncAll", true).catch((e) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
});
|
||||
|
||||
store
|
||||
.dispatch("settings/update", {
|
||||
name: "syncSettings",
|
||||
value: true,
|
||||
sync: true,
|
||||
})
|
||||
.catch((e) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
|
||||
const registerProtocol = () => {
|
||||
const uri = document.location.origin + document.location.pathname + "?uri=%s";
|
||||
// @ts-expect-error
|
||||
// the third argument is deprecated but recommended for compatibility: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/registerProtocolHandler
|
||||
window.navigator.registerProtocolHandler("irc", uri, "The Lounge");
|
||||
// @ts-expect-error
|
||||
window.navigator.registerProtocolHandler("ircs", uri, "The Lounge");
|
||||
};
|
||||
|
||||
return {
|
||||
store,
|
||||
canRegisterProtocol,
|
||||
hasInstallPromptEvent,
|
||||
nativeInstallPrompt,
|
||||
onForceSyncClick,
|
||||
registerProtocol,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
103
client/components/Settings/Navigation.vue
Normal file
103
client/components/Settings/Navigation.vue
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<template>
|
||||
<!-- 220px is the width of the sidebar, and we add 100px to allow for the text -->
|
||||
<aside class="settings-menu">
|
||||
<h2>Settings</h2>
|
||||
<ul role="navigation" aria-label="Settings tabs">
|
||||
<SettingTabItem name="General" class-name="general" to="" />
|
||||
<SettingTabItem name="Appearance" class-name="appearance" to="appearance" />
|
||||
<SettingTabItem name="Notifications" class-name="notifications" to="notifications" />
|
||||
<SettingTabItem name="Account" class-name="account" to="account" />
|
||||
</ul>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.settings-menu {
|
||||
position: fixed;
|
||||
/* top: Header + (padding bottom of h2 - border) */
|
||||
top: calc(45px + 5px);
|
||||
/* Mid page minus width of container and 30 pixels for padding */
|
||||
margin-left: calc(50% - 480px - 30px);
|
||||
}
|
||||
|
||||
/** The calculation is mobile + 2/3 of container width. Fairly arbitrary. */
|
||||
@media screen and (max-width: calc(768px + 320px)) {
|
||||
.settings-menu {
|
||||
position: static;
|
||||
width: min(480px, 100%);
|
||||
align-self: center;
|
||||
margin: 0 auto;
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-menu ul {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.settings-menu li {
|
||||
font-size: 18px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.settings-menu button {
|
||||
color: var(--body-color-muted);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.settings-menu li:not(:last-of-type) button {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.settings-menu button::before {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: inline-block;
|
||||
content: "";
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.settings-menu .appearance::before {
|
||||
content: "\f108"; /* http://fontawesome.io/icon/desktop/ */
|
||||
}
|
||||
|
||||
.settings-menu .account::before {
|
||||
content: "\f007"; /* http://fontawesome.io/icon/user/ */
|
||||
}
|
||||
|
||||
.settings-menu .messages::before {
|
||||
content: "\f0e0"; /* http://fontawesome.io/icon/envelope/ */
|
||||
}
|
||||
|
||||
.settings-menu .notifications::before {
|
||||
content: "\f0f3"; /* http://fontawesome.io/icon/bell/ */
|
||||
}
|
||||
|
||||
.settings-menu .general::before {
|
||||
content: "\f013"; /* http://fontawesome.io/icon/cog/ */
|
||||
}
|
||||
|
||||
.settings-menu button:hover,
|
||||
.settings-menu button.active {
|
||||
color: var(--body-color);
|
||||
}
|
||||
|
||||
.settings-menu button.active {
|
||||
cursor: default;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import SettingTabItem from "./SettingTabItem.vue";
|
||||
import {defineComponent} from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "SettingsTabs",
|
||||
components: {
|
||||
SettingTabItem,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
188
client/components/Settings/Notifications.vue
Normal file
188
client/components/Settings/Notifications.vue
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
<template>
|
||||
<div>
|
||||
<template v-if="!store.state.serverConfiguration?.public">
|
||||
<h2>Push Notifications</h2>
|
||||
<div>
|
||||
<button
|
||||
id="pushNotifications"
|
||||
type="button"
|
||||
class="btn"
|
||||
:disabled="
|
||||
store.state.pushNotificationState !== 'supported' &&
|
||||
store.state.pushNotificationState !== 'subscribed'
|
||||
"
|
||||
@click="onPushButtonClick"
|
||||
>
|
||||
<template v-if="store.state.pushNotificationState === 'subscribed'">
|
||||
Unsubscribe from push notifications
|
||||
</template>
|
||||
<template v-else-if="store.state.pushNotificationState === 'loading'">
|
||||
Loading…
|
||||
</template>
|
||||
<template v-else> Subscribe to push notifications </template>
|
||||
</button>
|
||||
<div v-if="store.state.pushNotificationState === 'nohttps'" class="error">
|
||||
<strong>Warning</strong>: Push notifications are only supported over HTTPS
|
||||
connections.
|
||||
</div>
|
||||
<div v-if="store.state.pushNotificationState === 'unsupported'" class="error">
|
||||
<strong>Warning</strong>:
|
||||
<span>Push notifications are not supported by your browser.</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<h2>Browser Notifications</h2>
|
||||
<div>
|
||||
<label class="opt">
|
||||
<input
|
||||
id="desktopNotifications"
|
||||
:checked="store.state.settings.desktopNotifications"
|
||||
:disabled="store.state.desktopNotificationState === 'nohttps'"
|
||||
type="checkbox"
|
||||
name="desktopNotifications"
|
||||
/>
|
||||
Enable browser notifications<br />
|
||||
<div v-if="store.state.desktopNotificationState === 'unsupported'" class="error">
|
||||
<strong>Warning</strong>: Notifications are not supported by your browser.
|
||||
</div>
|
||||
<div
|
||||
v-if="store.state.desktopNotificationState === 'nohttps'"
|
||||
id="warnBlockedDesktopNotifications"
|
||||
class="error"
|
||||
>
|
||||
<strong>Warning</strong>: Notifications are only supported over HTTPS
|
||||
connections.
|
||||
</div>
|
||||
<div
|
||||
v-if="store.state.desktopNotificationState === 'blocked'"
|
||||
id="warnBlockedDesktopNotifications"
|
||||
class="error"
|
||||
>
|
||||
<strong>Warning</strong>: Notifications are blocked by your browser.
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="opt">
|
||||
<input
|
||||
:checked="store.state.settings.notification"
|
||||
type="checkbox"
|
||||
name="notification"
|
||||
/>
|
||||
Enable notification sound
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div class="opt">
|
||||
<button id="play" @click.prevent="playNotification">Play sound</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="opt">
|
||||
<input
|
||||
:checked="store.state.settings.notifyAllMessages"
|
||||
type="checkbox"
|
||||
name="notifyAllMessages"
|
||||
/>
|
||||
Enable notification for all messages
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="!store.state.serverConfiguration?.public">
|
||||
<label class="opt">
|
||||
<label for="highlights" class="opt">
|
||||
Custom highlights
|
||||
<span
|
||||
class="tooltipped tooltipped-n tooltipped-no-delay"
|
||||
aria-label="If a message contains any of these comma-separated
|
||||
expressions, it will trigger a highlight."
|
||||
>
|
||||
<button class="extra-help" />
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="highlights"
|
||||
:value="store.state.settings.highlights"
|
||||
type="text"
|
||||
name="highlights"
|
||||
class="input"
|
||||
autocomplete="off"
|
||||
placeholder="Comma-separated, e.g.: word, some more words, anotherword"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="!store.state.serverConfiguration?.public">
|
||||
<label class="opt">
|
||||
<label for="highlightExceptions" class="opt">
|
||||
Highlight exceptions
|
||||
<span
|
||||
class="tooltipped tooltipped-n tooltipped-no-delay"
|
||||
aria-label="If a message contains any of these comma-separated
|
||||
expressions, it will not trigger a highlight even if it contains
|
||||
your nickname or expressions defined in custom highlights."
|
||||
>
|
||||
<button class="extra-help" />
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="highlightExceptions"
|
||||
:value="store.state.settings.highlightExceptions"
|
||||
type="text"
|
||||
name="highlightExceptions"
|
||||
class="input"
|
||||
autocomplete="off"
|
||||
placeholder="Comma-separated, e.g.: word, some more words, anotherword"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent} from "vue";
|
||||
import {useStore} from "../../js/store";
|
||||
import webpush from "../../js/webpush";
|
||||
|
||||
export default defineComponent({
|
||||
name: "NotificationSettings",
|
||||
setup() {
|
||||
const store = useStore();
|
||||
|
||||
const isIOS = computed(
|
||||
() =>
|
||||
[
|
||||
"iPad Simulator",
|
||||
"iPhone Simulator",
|
||||
"iPod Simulator",
|
||||
"iPad",
|
||||
"iPhone",
|
||||
"iPod",
|
||||
].includes(navigator.platform) ||
|
||||
// iPad on iOS 13 detection
|
||||
(navigator.userAgent.includes("Mac") && "ontouchend" in document)
|
||||
);
|
||||
|
||||
const playNotification = () => {
|
||||
const pop = new Audio();
|
||||
pop.src = "audio/pop.wav";
|
||||
|
||||
// eslint-disable-next-line
|
||||
pop.play();
|
||||
};
|
||||
|
||||
const onPushButtonClick = () => {
|
||||
webpush.togglePushSubscription();
|
||||
};
|
||||
|
||||
return {
|
||||
isIOS,
|
||||
store,
|
||||
playNotification,
|
||||
onPushButtonClick,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
43
client/components/Settings/SettingTabItem.vue
Normal file
43
client/components/Settings/SettingTabItem.vue
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<li :aria-label="name" role="tab" :aria-selected="route.name === name" aria-controls="settings">
|
||||
<router-link v-slot:default="{navigate, isExactActive}" :to="'/settings/' + to" custom>
|
||||
<button
|
||||
:class="['icon', className, {active: isExactActive}]"
|
||||
@click="navigate"
|
||||
@keypress.enter="navigate"
|
||||
>
|
||||
{{ name }}
|
||||
</button>
|
||||
</router-link>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from "vue";
|
||||
import {useRoute} from "vue-router";
|
||||
|
||||
export default defineComponent({
|
||||
name: "SettingTabListItem",
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
className: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
to: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const route = useRoute();
|
||||
|
||||
return {
|
||||
route,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
269
client/components/Sidebar.vue
Normal file
269
client/components/Sidebar.vue
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
<template>
|
||||
<aside id="sidebar" ref="sidebar">
|
||||
<div class="scrollable-area">
|
||||
<div class="logo-container">
|
||||
<img
|
||||
:src="`img/logo-${isPublic() ? 'horizontal-' : ''}transparent-bg.svg`"
|
||||
class="logo"
|
||||
alt="The Lounge"
|
||||
role="presentation"
|
||||
/>
|
||||
<img
|
||||
:src="`img/logo-${isPublic() ? 'horizontal-' : ''}transparent-bg-inverted.svg`"
|
||||
class="logo-inverted"
|
||||
alt="The Lounge"
|
||||
role="presentation"
|
||||
/>
|
||||
<span
|
||||
v-if="isDevelopment"
|
||||
title="The Lounge has been built in development mode"
|
||||
:style="{
|
||||
backgroundColor: '#ff9e18',
|
||||
color: '#000',
|
||||
padding: '2px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
}"
|
||||
>DEVELOPER</span
|
||||
>
|
||||
</div>
|
||||
<NetworkList />
|
||||
</div>
|
||||
<footer id="footer">
|
||||
<span
|
||||
class="tooltipped tooltipped-n tooltipped-no-touch"
|
||||
aria-label="Connect to network"
|
||||
><router-link
|
||||
v-slot:default="{navigate, isActive}"
|
||||
to="/connect"
|
||||
role="tab"
|
||||
aria-controls="connect"
|
||||
>
|
||||
<button
|
||||
:class="['icon', 'connect', {active: isActive}]"
|
||||
:aria-selected="isActive"
|
||||
@click="navigate"
|
||||
@keypress.enter="navigate"
|
||||
/> </router-link
|
||||
></span>
|
||||
<span class="tooltipped tooltipped-n tooltipped-no-touch" aria-label="Settings"
|
||||
><router-link
|
||||
v-slot:default="{navigate, isActive}"
|
||||
to="/settings"
|
||||
role="tab"
|
||||
aria-controls="settings"
|
||||
>
|
||||
<button
|
||||
:class="['icon', 'settings', {active: isActive}]"
|
||||
:aria-selected="isActive"
|
||||
@click="navigate"
|
||||
@keypress.enter="navigate"
|
||||
></button> </router-link
|
||||
></span>
|
||||
<span
|
||||
class="tooltipped tooltipped-n tooltipped-no-touch"
|
||||
:aria-label="
|
||||
store.state.serverConfiguration?.isUpdateAvailable
|
||||
? 'Help\n(update available)'
|
||||
: 'Help'
|
||||
"
|
||||
><router-link
|
||||
v-slot:default="{navigate, isActive}"
|
||||
to="/help"
|
||||
role="tab"
|
||||
aria-controls="help"
|
||||
>
|
||||
<button
|
||||
:aria-selected="route.name === 'Help'"
|
||||
:class="[
|
||||
'icon',
|
||||
'help',
|
||||
{notified: store.state.serverConfiguration?.isUpdateAvailable},
|
||||
{active: isActive},
|
||||
]"
|
||||
@click="navigate"
|
||||
@keypress.enter="navigate"
|
||||
></button> </router-link
|
||||
></span>
|
||||
</footer>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, nextTick, onMounted, onUnmounted, PropType, ref} from "vue";
|
||||
import {useRoute} from "vue-router";
|
||||
import {useStore} from "../js/store";
|
||||
import NetworkList from "./NetworkList.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Sidebar",
|
||||
components: {
|
||||
NetworkList,
|
||||
},
|
||||
props: {
|
||||
overlay: {type: Object as PropType<HTMLElement | null>, required: true},
|
||||
},
|
||||
setup(props) {
|
||||
const isDevelopment = process.env.NODE_ENV !== "production";
|
||||
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
|
||||
const touchStartPos = ref<Touch | null>();
|
||||
const touchCurPos = ref<Touch | null>();
|
||||
const touchStartTime = ref<number>(0);
|
||||
const menuWidth = ref<number>(0);
|
||||
const menuIsMoving = ref<boolean>(false);
|
||||
const menuIsAbsolute = ref<boolean>(false);
|
||||
|
||||
const sidebar = ref<HTMLElement | null>(null);
|
||||
|
||||
const toggle = (state: boolean) => {
|
||||
store.commit("sidebarOpen", state);
|
||||
};
|
||||
|
||||
const onTouchMove = (e: TouchEvent) => {
|
||||
const touch = (touchCurPos.value = e.touches.item(0));
|
||||
|
||||
if (
|
||||
!touch ||
|
||||
!touchStartPos.value ||
|
||||
!touchStartPos.value.screenX ||
|
||||
!touchStartPos.value.screenY
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let distX = touch.screenX - touchStartPos.value.screenX;
|
||||
const distY = touch.screenY - touchStartPos.value.screenY;
|
||||
|
||||
if (!menuIsMoving.value) {
|
||||
// tan(45°) is 1. Gestures in 0°-45° (< 1) are considered horizontal, so
|
||||
// menu must be open; gestures in 45°-90° (>1) are considered vertical, so
|
||||
// chat windows must be scrolled.
|
||||
if (Math.abs(distY / distX) >= 1) {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
onTouchEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
const devicePixelRatio = window.devicePixelRatio || 2;
|
||||
|
||||
if (Math.abs(distX) > devicePixelRatio) {
|
||||
store.commit("sidebarDragging", true);
|
||||
menuIsMoving.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Do not animate the menu on desktop view
|
||||
if (!menuIsAbsolute.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (store.state.sidebarOpen) {
|
||||
distX += menuWidth.value;
|
||||
}
|
||||
|
||||
if (distX > menuWidth.value) {
|
||||
distX = menuWidth.value;
|
||||
} else if (distX < 0) {
|
||||
distX = 0;
|
||||
}
|
||||
|
||||
if (sidebar.value) {
|
||||
sidebar.value.style.transform = "translate3d(" + distX.toString() + "px, 0, 0)";
|
||||
}
|
||||
|
||||
if (props.overlay) {
|
||||
props.overlay.style.opacity = `${distX / menuWidth.value}`;
|
||||
}
|
||||
};
|
||||
|
||||
const onTouchEnd = () => {
|
||||
if (!touchStartPos.value?.screenX || !touchCurPos.value?.screenX) {
|
||||
return;
|
||||
}
|
||||
|
||||
const diff = touchCurPos.value.screenX - touchStartPos.value.screenX;
|
||||
const absDiff = Math.abs(diff);
|
||||
|
||||
if (
|
||||
absDiff > menuWidth.value / 2 ||
|
||||
(Date.now() - touchStartTime.value < 180 && absDiff > 50)
|
||||
) {
|
||||
toggle(diff > 0);
|
||||
}
|
||||
|
||||
document.body.removeEventListener("touchmove", onTouchMove);
|
||||
document.body.removeEventListener("touchend", onTouchEnd);
|
||||
|
||||
store.commit("sidebarDragging", false);
|
||||
|
||||
touchStartPos.value = null;
|
||||
touchCurPos.value = null;
|
||||
touchStartTime.value = 0;
|
||||
menuIsMoving.value = false;
|
||||
|
||||
void nextTick(() => {
|
||||
if (sidebar.value) {
|
||||
sidebar.value.style.transform = "";
|
||||
}
|
||||
|
||||
if (props.overlay) {
|
||||
props.overlay.style.opacity = "";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onTouchStart = (e: TouchEvent) => {
|
||||
if (!sidebar.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
touchStartPos.value = touchCurPos.value = e.touches.item(0);
|
||||
|
||||
if (e.touches.length !== 1) {
|
||||
onTouchEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
const styles = window.getComputedStyle(sidebar.value);
|
||||
|
||||
menuWidth.value = parseFloat(styles.width);
|
||||
menuIsAbsolute.value = styles.position === "absolute";
|
||||
|
||||
if (
|
||||
!store.state.sidebarOpen ||
|
||||
(touchStartPos.value?.screenX && touchStartPos.value.screenX > menuWidth.value)
|
||||
) {
|
||||
touchStartTime.value = Date.now();
|
||||
|
||||
document.body.addEventListener("touchmove", onTouchMove, {passive: true});
|
||||
document.body.addEventListener("touchend", onTouchEnd, {passive: true});
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.body.addEventListener("touchstart", onTouchStart, {passive: true});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.body.removeEventListener("touchstart", onTouchStart);
|
||||
});
|
||||
|
||||
const isPublic = () => document.body.classList.contains("public");
|
||||
|
||||
return {
|
||||
isDevelopment,
|
||||
store,
|
||||
route,
|
||||
sidebar,
|
||||
toggle,
|
||||
onTouchStart,
|
||||
onTouchMove,
|
||||
onTouchEnd,
|
||||
isPublic,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
19
client/components/SidebarToggle.vue
Normal file
19
client/components/SidebarToggle.vue
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<template>
|
||||
<button class="lt" aria-label="Toggle channel list" @click="store.commit('toggleSidebar')" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from "vue";
|
||||
import {useStore} from "../js/store";
|
||||
|
||||
export default defineComponent({
|
||||
name: "SidebarToggle",
|
||||
setup() {
|
||||
const store = useStore();
|
||||
|
||||
return {
|
||||
store,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
45
client/components/Special/ListBans.vue
Normal file
45
client/components/Special/ListBans.vue
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<template>
|
||||
<table class="ban-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="hostmask">Banned</th>
|
||||
<th class="banned_by">Banned By</th>
|
||||
<th class="banned_at">Banned At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="ban in channel.data" :key="ban.hostmask">
|
||||
<td class="hostmask"><ParsedMessage :network="network" :text="ban.hostmask" /></td>
|
||||
<td class="banned_by">{{ ban.banned_by }}</td>
|
||||
<td class="banned_at">{{ localetime(ban.banned_at) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
import localeTime from "../../js/helpers/localetime";
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import type {ClientNetwork, ClientChan} from "../../js/types";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ListBans",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
},
|
||||
props: {
|
||||
network: {type: Object as PropType<ClientNetwork>, required: true},
|
||||
channel: {type: Object as PropType<ClientChan>, required: true},
|
||||
},
|
||||
setup() {
|
||||
const localetime = (date: number | Date) => {
|
||||
return localeTime(date);
|
||||
};
|
||||
|
||||
return {
|
||||
localetime,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
36
client/components/Special/ListChannels.vue
Normal file
36
client/components/Special/ListChannels.vue
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<template>
|
||||
<span v-if="channel.data.text">{{ channel.data.text }}</span>
|
||||
<table v-else class="channel-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="channel">Channel</th>
|
||||
<th class="users">Users</th>
|
||||
<th class="topic">Topic</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="chan in channel.data" :key="chan.channel">
|
||||
<td class="channel"><ParsedMessage :network="network" :text="chan.channel" /></td>
|
||||
<td class="users">{{ chan.num_users }}</td>
|
||||
<td class="topic"><ParsedMessage :network="network" :text="chan.topic" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import {ClientChan, ClientNetwork} from "../../js/types";
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ListChannels",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
},
|
||||
props: {
|
||||
network: {type: Object as PropType<ClientNetwork>, required: true},
|
||||
channel: {type: Object as PropType<ClientChan>, required: true},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
39
client/components/Special/ListIgnored.vue
Normal file
39
client/components/Special/ListIgnored.vue
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<table class="ignore-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="hostmask">Hostmask</th>
|
||||
<th class="when">Ignored At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in channel.data" :key="user.hostmask">
|
||||
<td class="hostmask"><ParsedMessage :network="network" :text="user.hostmask" /></td>
|
||||
<td class="when">{{ localetime(user.when) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
import localetime from "../../js/helpers/localetime";
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import {ClientNetwork, ClientChan} from "../../js/types";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ListIgnored",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
},
|
||||
props: {
|
||||
network: {type: Object as PropType<ClientNetwork>, required: true},
|
||||
channel: {type: Object as PropType<ClientChan>, required: true},
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
localetime,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
43
client/components/Special/ListInvites.vue
Normal file
43
client/components/Special/ListInvites.vue
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<table class="invite-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="hostmask">Invited</th>
|
||||
<th class="invitened_by">Invited By</th>
|
||||
<th class="invitened_at">Invited At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="invite in channel.data" :key="invite.hostmask">
|
||||
<td class="hostmask">
|
||||
<ParsedMessage :network="network" :text="invite.hostmask" />
|
||||
</td>
|
||||
<td class="invitened_by">{{ invite.invited_by }}</td>
|
||||
<td class="invitened_at">{{ localetime(invite.invited_at) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import ParsedMessage from "../ParsedMessage.vue";
|
||||
import localetime from "../../js/helpers/localetime";
|
||||
import {defineComponent, PropType} from "vue";
|
||||
import {ClientNetwork, ClientChan} from "../../js/types";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ListInvites",
|
||||
components: {
|
||||
ParsedMessage,
|
||||
},
|
||||
props: {
|
||||
network: {type: Object as PropType<ClientNetwork>, required: true},
|
||||
channel: {type: Object as PropType<ClientChan>, required: true},
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
localetime: (date: Date) => localetime(date),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
84
client/components/Username.vue
Normal file
84
client/components/Username.vue
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<template>
|
||||
<span
|
||||
:class="['user', {[nickColor]: store.state.settings.coloredNicks}, {active: active}]"
|
||||
:data-name="user.nick"
|
||||
role="button"
|
||||
v-on="onHover ? {mouseenter: hover} : {}"
|
||||
@click.prevent="openContextMenu"
|
||||
@contextmenu.prevent="openContextMenu"
|
||||
><slot>{{ mode }}{{ user.nick }}</slot></span
|
||||
>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {computed, defineComponent, PropType} from "vue";
|
||||
import {UserInMessage} from "../../shared/types/msg";
|
||||
import eventbus from "../js/eventbus";
|
||||
import colorClass from "../js/helpers/colorClass";
|
||||
import type {ClientChan, ClientNetwork} from "../js/types";
|
||||
import {useStore} from "../js/store";
|
||||
|
||||
type UsernameUser = Partial<UserInMessage> & {
|
||||
mode?: string;
|
||||
nick: string;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: "Username",
|
||||
props: {
|
||||
user: {
|
||||
// TODO: UserInMessage shouldn't be necessary here.
|
||||
type: Object as PropType<UsernameUser | UserInMessage>,
|
||||
required: true,
|
||||
},
|
||||
active: Boolean,
|
||||
onHover: {
|
||||
type: Function as PropType<(user: UserInMessage) => void>,
|
||||
required: false,
|
||||
},
|
||||
channel: {type: Object as PropType<ClientChan>, required: false},
|
||||
network: {type: Object as PropType<ClientNetwork>, required: false},
|
||||
},
|
||||
setup(props) {
|
||||
const mode = computed(() => {
|
||||
// Message objects have a singular mode, but user objects have modes array
|
||||
if (props.user.modes) {
|
||||
return props.user.modes[0];
|
||||
}
|
||||
|
||||
return props.user.mode;
|
||||
});
|
||||
|
||||
// TODO: Nick must be ! because our user prop union includes UserInMessage
|
||||
const nickColor = computed(() => colorClass(props.user.nick!));
|
||||
|
||||
const hover = () => {
|
||||
if (props.onHover) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return props.onHover(props.user as UserInMessage);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const openContextMenu = (event: Event) => {
|
||||
eventbus.emit("contextmenu:user", {
|
||||
event: event,
|
||||
user: props.user,
|
||||
network: props.network,
|
||||
channel: props.channel,
|
||||
});
|
||||
};
|
||||
|
||||
const store = useStore();
|
||||
|
||||
return {
|
||||
mode,
|
||||
nickColor,
|
||||
hover,
|
||||
openContextMenu,
|
||||
store,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
66
client/components/VersionChecker.vue
Normal file
66
client/components/VersionChecker.vue
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<template>
|
||||
<div id="version-checker" :class="[store.state.versionStatus]">
|
||||
<p v-if="store.state.versionStatus === 'loading'">Checking for updates…</p>
|
||||
<p v-if="store.state.versionStatus === 'new-version'">
|
||||
The Lounge <b>{{ store.state.versionData?.latest.version }}</b>
|
||||
<template v-if="store.state.versionData?.latest.prerelease"> (pre-release) </template>
|
||||
is now available.
|
||||
<br />
|
||||
|
||||
<a :href="store.state.versionData?.latest.url" target="_blank" rel="noopener">
|
||||
Read more on GitHub
|
||||
</a>
|
||||
</p>
|
||||
<p v-if="store.state.versionStatus === 'new-packages'">
|
||||
The Lounge is up to date, but there are out of date packages Run
|
||||
<code>thelounge upgrade</code> on the server to upgrade packages.
|
||||
</p>
|
||||
<template v-if="store.state.versionStatus === 'up-to-date'">
|
||||
<p>The Lounge is up to date!</p>
|
||||
|
||||
<button
|
||||
v-if="store.state.versionDataExpired"
|
||||
id="check-now"
|
||||
class="btn btn-small"
|
||||
@click="checkNow"
|
||||
>
|
||||
Check now
|
||||
</button>
|
||||
</template>
|
||||
<template v-if="store.state.versionStatus === 'error'">
|
||||
<p>Information about latest release could not be retrieved.</p>
|
||||
|
||||
<button id="check-now" class="btn btn-small" @click="checkNow">Try again</button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, onMounted} from "vue";
|
||||
import socket from "../js/socket";
|
||||
import {useStore} from "../js/store";
|
||||
|
||||
export default defineComponent({
|
||||
name: "VersionChecker",
|
||||
setup() {
|
||||
const store = useStore();
|
||||
|
||||
const checkNow = () => {
|
||||
store.commit("versionData", null);
|
||||
store.commit("versionStatus", "loading");
|
||||
socket.emit("changelog");
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!store.state.versionData) {
|
||||
checkNow();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
store,
|
||||
checkNow,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
93
client/components/Windows/Changelog.vue
Normal file
93
client/components/Windows/Changelog.vue
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<template>
|
||||
<div id="changelog" class="window" aria-label="Changelog">
|
||||
<div class="header">
|
||||
<SidebarToggle />
|
||||
</div>
|
||||
<div class="container">
|
||||
<router-link id="back-to-help" to="/help">« Help</router-link>
|
||||
|
||||
<template
|
||||
v-if="store.state.versionData?.current && store.state.versionData?.current.version"
|
||||
>
|
||||
<h1 class="title">
|
||||
Release notes for {{ store.state.versionData.current.version }}
|
||||
</h1>
|
||||
|
||||
<template v-if="store.state.versionData.current.changelog">
|
||||
<h3>Introduction</h3>
|
||||
<div
|
||||
ref="changelog"
|
||||
class="changelog-text"
|
||||
v-html="store.state.versionData.current.changelog"
|
||||
></div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>Unable to retrieve changelog for current release from GitHub.</p>
|
||||
<p>
|
||||
<a
|
||||
v-if="store.state.serverConfiguration?.version"
|
||||
:href="`https://github.com/thelounge/thelounge/releases/tag/v${store.state.serverConfiguration?.version}`"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>View release notes for this version on GitHub</a
|
||||
>
|
||||
</p>
|
||||
</template>
|
||||
</template>
|
||||
<p v-else>Loading changelog…</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, onMounted, onUpdated, ref} from "vue";
|
||||
import socket from "../../js/socket";
|
||||
import {useStore} from "../../js/store";
|
||||
import SidebarToggle from "../SidebarToggle.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Changelog",
|
||||
components: {
|
||||
SidebarToggle,
|
||||
},
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const changelog = ref<HTMLDivElement | null>(null);
|
||||
|
||||
const patchChangelog = () => {
|
||||
if (!changelog.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const links = changelog.value.querySelectorAll("a");
|
||||
|
||||
links.forEach((link) => {
|
||||
// Make sure all links will open a new tab instead of exiting the application
|
||||
link.setAttribute("target", "_blank");
|
||||
link.setAttribute("rel", "noopener");
|
||||
|
||||
if (link.querySelector("img")) {
|
||||
// Add required metadata to image links, to support built-in image viewer
|
||||
link.classList.add("toggle-thumbnail");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!store.state.versionData) {
|
||||
socket.emit("changelog");
|
||||
}
|
||||
|
||||
patchChangelog();
|
||||
});
|
||||
|
||||
onUpdated(() => {
|
||||
patchChangelog();
|
||||
});
|
||||
|
||||
return {
|
||||
store,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
117
client/components/Windows/Connect.vue
Normal file
117
client/components/Windows/Connect.vue
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<template>
|
||||
<NetworkForm :handle-submit="handleSubmit" :defaults="defaults" :disabled="disabled" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, ref} from "vue";
|
||||
|
||||
import socket from "../../js/socket";
|
||||
import {useStore} from "../../js/store";
|
||||
import NetworkForm, {NetworkFormDefaults} from "../NetworkForm.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Connect",
|
||||
components: {
|
||||
NetworkForm,
|
||||
},
|
||||
props: {
|
||||
queryParams: Object,
|
||||
},
|
||||
setup(props) {
|
||||
const store = useStore();
|
||||
|
||||
const disabled = ref(false);
|
||||
|
||||
const handleSubmit = (data: Record<string, any>) => {
|
||||
disabled.value = true;
|
||||
socket.emit("network:new", data);
|
||||
};
|
||||
|
||||
const parseOverrideParams = (params?: Record<string, string>) => {
|
||||
if (!params) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const parsedParams: Record<string, any> = {};
|
||||
|
||||
for (let key of Object.keys(params)) {
|
||||
let value = params[key];
|
||||
|
||||
// Param can contain multiple values in an array if its supplied more than once
|
||||
if (Array.isArray(value)) {
|
||||
value = value[0];
|
||||
}
|
||||
|
||||
// Support `channels` as a compatibility alias with other clients
|
||||
if (key === "channels") {
|
||||
key = "join";
|
||||
}
|
||||
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(
|
||||
store.state.serverConfiguration?.defaults,
|
||||
key
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// When the network is locked, URL overrides should not affect disabled fields
|
||||
if (
|
||||
store.state.serverConfiguration?.lockNetwork &&
|
||||
["name", "host", "port", "tls", "rejectUnauthorized"].includes(key)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key === "join") {
|
||||
value = value
|
||||
.split(",")
|
||||
.map((chan) => {
|
||||
if (!chan.match(/^[#&!+]/)) {
|
||||
return `#${chan}`;
|
||||
}
|
||||
|
||||
return chan;
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
// Override server provided defaults with parameters passed in the URL if they match the data type
|
||||
switch (typeof store.state.serverConfiguration?.defaults[key]) {
|
||||
case "boolean":
|
||||
if (value === "0" || value === "false") {
|
||||
parsedParams[key] = false;
|
||||
} else {
|
||||
parsedParams[key] = !!value;
|
||||
}
|
||||
|
||||
break;
|
||||
case "number":
|
||||
parsedParams[key] = Number(value);
|
||||
break;
|
||||
case "string":
|
||||
parsedParams[key] = String(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return parsedParams;
|
||||
};
|
||||
|
||||
const defaults = ref<Partial<NetworkFormDefaults>>(
|
||||
Object.assign(
|
||||
{},
|
||||
store.state.serverConfiguration?.defaults,
|
||||
parseOverrideParams(props.queryParams)
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
defaults,
|
||||
disabled,
|
||||
handleSubmit,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
879
client/components/Windows/Help.vue
Normal file
879
client/components/Windows/Help.vue
Normal file
|
|
@ -0,0 +1,879 @@
|
|||
<template>
|
||||
<div id="help" class="window" role="tabpanel" aria-label="Help">
|
||||
<div class="header">
|
||||
<SidebarToggle />
|
||||
</div>
|
||||
<div class="container">
|
||||
<h1 class="title">Help</h1>
|
||||
|
||||
<h2 class="help-version-title">
|
||||
<span>About The Lounge</span>
|
||||
<small>
|
||||
v{{ store.state.serverConfiguration?.version }} (<router-link
|
||||
id="view-changelog"
|
||||
to="/changelog"
|
||||
>release notes</router-link
|
||||
>)
|
||||
</small>
|
||||
</h2>
|
||||
|
||||
<div class="about">
|
||||
<VersionChecker />
|
||||
|
||||
<template v-if="store.state.serverConfiguration?.gitCommit">
|
||||
<p>
|
||||
The Lounge is running from source (<a
|
||||
:href="`https://github.com/thelounge/thelounge/tree/${store.state.serverConfiguration?.gitCommit}`"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>commit <code>{{ store.state.serverConfiguration?.gitCommit }}</code></a
|
||||
>).
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
Compare
|
||||
<a
|
||||
:href="`https://github.com/thelounge/thelounge/compare/${store.state.serverConfiguration?.gitCommit}...master`"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>between
|
||||
<code>{{ store.state.serverConfiguration?.gitCommit }}</code> and
|
||||
<code>master</code></a
|
||||
>
|
||||
to see what you are missing
|
||||
</li>
|
||||
<li>
|
||||
Compare
|
||||
<a
|
||||
:href="`https://github.com/thelounge/thelounge/compare/${store.state.serverConfiguration?.version}...${store.state.serverConfiguration?.gitCommit}`"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>between
|
||||
<code>{{ store.state.serverConfiguration?.version }}</code> and
|
||||
<code>{{ store.state.serverConfiguration?.gitCommit }}</code></a
|
||||
>
|
||||
to see your local changes
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<p>
|
||||
<a
|
||||
href="https://thelounge.chat/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="website-link"
|
||||
>Website</a
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
href="https://thelounge.chat/docs/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="documentation-link"
|
||||
>Documentation</a
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
href="https://github.com/thelounge/thelounge/issues/new"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="report-issue-link"
|
||||
>Report an issue…</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 v-if="isTouch">Gestures</h2>
|
||||
|
||||
<div v-if="isTouch" class="help-item">
|
||||
<div class="subject gesture">Single-Finger Swipe Left</div>
|
||||
<div class="description">
|
||||
<p>Hide sidebar.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isTouch" class="help-item">
|
||||
<div class="subject gesture">Single-Finger Swipe Right</div>
|
||||
<div class="description">
|
||||
<p>Show sidebar.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isTouch" class="help-item">
|
||||
<div class="subject gesture">Two-Finger Swipe Left</div>
|
||||
<div class="description">
|
||||
<p>Switch to the next window in the channel list.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isTouch" class="help-item">
|
||||
<div class="subject gesture">Two-Finger Swipe Right</div>
|
||||
<div class="description">
|
||||
<p>Switch to the previous window in the channel list.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Keyboard Shortcuts</h2>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span v-if="!isApple"><kbd>Alt</kbd> <kbd>Shift</kbd> <kbd>↓</kbd></span>
|
||||
<span v-else><kbd>⌥</kbd> <kbd>⇧</kbd> <kbd>↓</kbd></span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Switch to the next lobby in the channel list.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span v-if="!isApple"><kbd>Alt</kbd> <kbd>Shift</kbd> <kbd>↑</kbd></span>
|
||||
<span v-else><kbd>⌥</kbd> <kbd>⇧</kbd> <kbd>↑</kbd></span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Switch to the previous lobby in the channel list.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span v-if="!isApple"><kbd>Alt</kbd> <kbd>Shift</kbd> <kbd>←</kbd></span>
|
||||
<span v-else><kbd>⌥</kbd> <kbd>⇧</kbd> <kbd>←</kbd></span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Collapse current network.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span v-if="!isApple"><kbd>Alt</kbd> <kbd>Shift</kbd> <kbd>→</kbd></span>
|
||||
<span v-else><kbd>⌥</kbd> <kbd>⇧</kbd> <kbd>→</kbd></span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Expand current network.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span v-if="!isApple"><kbd>Alt</kbd> <kbd>↓</kbd></span>
|
||||
<span v-else><kbd>⌥</kbd> <kbd>↓</kbd></span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Switch to the next window in the channel list.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span v-if="!isApple"><kbd>Alt</kbd> <kbd>↑</kbd></span>
|
||||
<span v-else><kbd>⌥</kbd> <kbd>↑</kbd></span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Switch to the previous window in the channel list.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span v-if="!isApple"><kbd>Alt</kbd> <kbd>Ctrl</kbd> <kbd>↓</kbd></span>
|
||||
<span v-else><kbd>⌥</kbd> <kbd>⌘</kbd> <kbd>↓</kbd></span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Switch to the next window with unread messages in the channel list.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span v-if="!isApple"><kbd>Alt</kbd> <kbd>Ctrl</kbd> <kbd>↑</kbd></span>
|
||||
<span v-else><kbd>⌥</kbd> <kbd>⌘</kbd> <kbd>↑</kbd></span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Switch to the previous window with unread messages in the channel list.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span v-if="!isApple"><kbd>Alt</kbd> <kbd>A</kbd></span>
|
||||
<span v-else><kbd>⌥</kbd> <kbd>A</kbd></span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Switch to the first window with unread messages.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span v-if="!isApple"><kbd>Alt</kbd> <kbd>S</kbd></span>
|
||||
<span v-else><kbd>⌥</kbd> <kbd>S</kbd></span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Toggle sidebar.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span v-if="!isApple"><kbd>Alt</kbd> <kbd>U</kbd></span>
|
||||
<span v-else><kbd>⌥</kbd> <kbd>U</kbd></span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Toggle channel user list.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span v-if="!isApple"><kbd>Alt</kbd> <kbd>J</kbd></span>
|
||||
<span v-else><kbd>⌥</kbd> <kbd>J</kbd></span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Toggle jump to channel switcher.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span v-if="!isApple"><kbd>Alt</kbd> <kbd>M</kbd></span>
|
||||
<span v-else><kbd>⌥</kbd> <kbd>M</kbd></span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Toggle recent mentions popup.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span v-if="!isApple"><kbd>Alt</kbd> <kbd>/</kbd></span>
|
||||
<span v-else><kbd>⌥</kbd> <kbd>/</kbd></span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Switch to the help menu.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span><kbd>Esc</kbd></span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Close current contextual window (context menu, image viewer, topic edit,
|
||||
etc) and remove focus from input.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Formatting Shortcuts</h2>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span v-if="!isApple"><kbd>Ctrl</kbd> <kbd>K</kbd></span>
|
||||
<span v-else><kbd>⌘</kbd> <kbd>K</kbd></span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Mark any text typed after this shortcut to be colored. After hitting this
|
||||
shortcut, enter an integer in the range
|
||||
<code>0—15</code> to select the desired color, or use the autocompletion
|
||||
menu to choose a color name (see below).
|
||||
</p>
|
||||
<p>
|
||||
Background color can be specified by putting a comma and another integer in
|
||||
the range <code>0—15</code> after the foreground color number
|
||||
(autocompletion works too).
|
||||
</p>
|
||||
<p>
|
||||
A color reference can be found
|
||||
<a
|
||||
href="https://modern.ircdocs.horse/formatting.html#colors"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>here</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span v-if="!isApple"><kbd>Ctrl</kbd> <kbd>B</kbd></span>
|
||||
<span v-else><kbd>⌘</kbd> <kbd>B</kbd></span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Mark all text typed after this shortcut as
|
||||
<span class="irc-bold">bold</span>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span v-if="!isApple"><kbd>Ctrl</kbd> <kbd>U</kbd></span>
|
||||
<span v-else><kbd>⌘</kbd> <kbd>U</kbd></span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Mark all text typed after this shortcut as
|
||||
<span class="irc-underline">underlined</span>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span v-if="!isApple"><kbd>Ctrl</kbd> <kbd>I</kbd></span>
|
||||
<span v-else><kbd>⌘</kbd> <kbd>I</kbd></span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Mark all text typed after this shortcut as
|
||||
<span class="irc-italic">italics</span>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span v-if="!isApple"><kbd>Ctrl</kbd> <kbd>S</kbd></span>
|
||||
<span v-else><kbd>⌘</kbd> <kbd>S</kbd></span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Mark all text typed after this shortcut as
|
||||
<span class="irc-strikethrough">struck through</span>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span v-if="!isApple"><kbd>Ctrl</kbd> <kbd>M</kbd></span>
|
||||
<span v-else><kbd>⌘</kbd> <kbd>M</kbd></span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Mark all text typed after this shortcut as
|
||||
<span class="irc-monospace">monospaced</span>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<span v-if="!isApple"><kbd>Ctrl</kbd> <kbd>O</kbd></span>
|
||||
<span v-else><kbd>⌘</kbd> <kbd>O</kbd></span>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Mark all text typed after this shortcut to be reset to its original
|
||||
formatting.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Autocompletion</h2>
|
||||
|
||||
<p>
|
||||
To auto-complete nicknames, channels, commands, and emoji, type one of the
|
||||
characters below to open a suggestion list. Use the <kbd>↑</kbd> and
|
||||
<kbd>↓</kbd> keys to highlight an item, and insert it by pressing <kbd>Tab</kbd> or
|
||||
<kbd>Enter</kbd> (or by clicking the desired item).
|
||||
</p>
|
||||
<p>Autocompletion can be disabled in settings.</p>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>@</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Nickname</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>#</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Channel</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Commands (see list of commands below)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>:</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Emoji (note: requires two search characters, to avoid conflicting with
|
||||
common emoticons like <code>:)</code>)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Commands</h2>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/away [message]</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Mark yourself as away with an optional message.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/back</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Remove your away status (set with <code>/away</code>).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/ban nick</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Ban (<code>+b</code>) a user from the current channel. This can be a
|
||||
nickname or a hostmask.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/banlist</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Load the banlist for the current channel.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/collapse</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Collapse all previews in the current channel (opposite of
|
||||
<code>/expand</code>)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/connect host [port]</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Connect to a new IRC network. If <code>port</code> starts with a
|
||||
<code>+</code> sign, the connection will be made secure using TLS.
|
||||
</p>
|
||||
<p>Alias: <code>/server</code></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/ctcp target cmd [args]</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Send a <abbr title="Client-to-client protocol">CTCP</abbr>
|
||||
request. Read more about this on
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/Client-to-client_protocol"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>the dedicated Wikipedia article</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/deop nick [...nick]</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Remove op (<code>-o</code>) from one or several users in the current
|
||||
channel.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/devoice nick [...nick]</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Remove voice (<code>-v</code>) from one or several users in the current
|
||||
channel.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/disconnect [message]</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Disconnect from the current network with an optionally-provided message.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/expand</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Expand all previews in the current channel (opposite of
|
||||
<code>/collapse</code>)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/invite nick [channel]</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Invite a user to the specified channel. If
|
||||
<code>channel</code> is omitted, user will be invited to the current
|
||||
channel.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/ignore nick</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Block any messages from the specified user on the current network. This can
|
||||
be a nickname or a hostmask.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/ignorelist</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Load the list of ignored users for the current network.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/join channel [password]</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Join a channel. Password is only needed in protected channels and can
|
||||
usually be omitted.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/kick nick [reason]</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Kick a user from the current channel.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/kickban nick [reason]</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Kick and ban (<code>+b</code>) a user from the current channel. Unlike
|
||||
<code>/ban</code>, only nicknames (and not host masks) can be used.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/list</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Retrieve a list of available channels on this network.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/me message</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Send an action message to the current channel. The Lounge will display it
|
||||
inline, as if the message was posted in the third person.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/mode flags [args]</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Set the given flags to the current channel if the active window is a
|
||||
channel, another user if the active window is a private message window, or
|
||||
yourself if the current window is a server window.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/msg channel message</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Send a message to the specified channel.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/mute [...channel]</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Prevent messages from generating any feedback for a channel. This turns off
|
||||
the highlight indicator, hides mentions and inhibits push notifications.
|
||||
Muting a network lobby mutes the entire network. Not specifying any channel
|
||||
target mutes the current channel. Revert with <code>/unmute</code>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/nick newnick</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Change your nickname on the current network.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/notice channel message</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Sends a notice message to the specified channel.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/op nick [...nick]</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Give op (<code>+o</code>) to one or several users in the current channel.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/part [channel]</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Close the specified channel or private message window, or the current
|
||||
channel if <code>channel</code> is omitted.
|
||||
</p>
|
||||
<p>Aliases: <code>/close</code>, <code>/leave</code></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/rejoin</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Leave and immediately rejoin the current channel. Useful to quickly get op
|
||||
from ChanServ in an empty channel, for example.
|
||||
</p>
|
||||
<p>Alias: <code>/cycle</code></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/query nick</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Send a private message to the specified user.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/quit [message]</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Disconnect from the current network with an optional message.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/raw message</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Send a raw message to the current IRC network.</p>
|
||||
<p>Aliases: <code>/quote</code>, <code>/send</code></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/slap nick</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Slap someone in the current channel with a trout!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="store.state.settings.searchEnabled" class="help-item">
|
||||
<div class="subject">
|
||||
<code>/search query</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Search for messages in the current channel / user</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/topic [newtopic]</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Get the topic in the current channel. If <code>newtopic</code> is specified,
|
||||
sets the topic in the current channel.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/unban nick</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Unban (<code>-b</code>) a user from the current channel. This can be a
|
||||
nickname or a hostmask.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/unignore nick</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Unblock messages from the specified user on the current network. This can be
|
||||
a nickname or a hostmask.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/unmute [...channel]</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Un-mutes the given channel(s) or the current channel if no channel is
|
||||
provided. See <code>/mute</code> for more information.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/voice nick [...nick]</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>
|
||||
Give voice (<code>+v</code>) to one or several users in the current channel.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-item">
|
||||
<div class="subject">
|
||||
<code>/whois nick</code>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>Retrieve information about the given user on the current network.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, ref} from "vue";
|
||||
import {useStore} from "../../js/store";
|
||||
import SidebarToggle from "../SidebarToggle.vue";
|
||||
import VersionChecker from "../VersionChecker.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Help",
|
||||
components: {
|
||||
SidebarToggle,
|
||||
VersionChecker,
|
||||
},
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const isApple = navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i) || false;
|
||||
const isTouch = navigator.maxTouchPoints > 0;
|
||||
|
||||
return {
|
||||
isApple,
|
||||
isTouch,
|
||||
store,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
67
client/components/Windows/NetworkEdit.vue
Normal file
67
client/components/Windows/NetworkEdit.vue
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<template>
|
||||
<NetworkForm
|
||||
v-if="networkData"
|
||||
:handle-submit="handleSubmit"
|
||||
:defaults="networkData"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, onMounted, ref, watch} from "vue";
|
||||
import {useRoute} from "vue-router";
|
||||
import {switchToChannel} from "../../js/router";
|
||||
import socket from "../../js/socket";
|
||||
import {useStore} from "../../js/store";
|
||||
import NetworkForm, {NetworkFormDefaults} from "../NetworkForm.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "NetworkEdit",
|
||||
components: {
|
||||
NetworkForm,
|
||||
},
|
||||
setup() {
|
||||
const route = useRoute();
|
||||
const store = useStore();
|
||||
|
||||
const disabled = ref(false);
|
||||
const networkData = ref<NetworkFormDefaults | null>(null);
|
||||
|
||||
const setNetworkData = () => {
|
||||
socket.emit("network:get", String(route.params.uuid || ""));
|
||||
networkData.value = store.getters.findNetwork(String(route.params.uuid || ""));
|
||||
};
|
||||
|
||||
const handleSubmit = (data: {uuid: string; name: string}) => {
|
||||
disabled.value = true;
|
||||
socket.emit("network:edit", data);
|
||||
|
||||
// TODO: move networks to vuex and update state when the network info comes in
|
||||
const network = store.getters.findNetwork(data.uuid);
|
||||
|
||||
if (network) {
|
||||
network.name = network.channels[0].name = data.name;
|
||||
|
||||
switchToChannel(network.channels[0]);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => route.params.uuid,
|
||||
() => {
|
||||
setNetworkData();
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
setNetworkData();
|
||||
});
|
||||
|
||||
return {
|
||||
disabled,
|
||||
networkData,
|
||||
handleSubmit,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
321
client/components/Windows/SearchResults.vue
Normal file
321
client/components/Windows/SearchResults.vue
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
<template>
|
||||
<div id="chat-container" class="window">
|
||||
<div
|
||||
id="chat"
|
||||
:class="{
|
||||
'time-seconds': store.state.settings.showSeconds,
|
||||
'time-12h': store.state.settings.use12hClock,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="chat-view"
|
||||
data-type="search-results"
|
||||
aria-label="Search results"
|
||||
role="tabpanel"
|
||||
>
|
||||
<div v-if="network && channel" class="header">
|
||||
<SidebarToggle />
|
||||
<span class="title"
|
||||
>Searching in <span class="channel-name">{{ channel.name }}</span> for</span
|
||||
>
|
||||
<span class="topic">{{ route.query.q }}</span>
|
||||
<MessageSearchForm :network="network" :channel="channel" />
|
||||
<button
|
||||
class="close"
|
||||
aria-label="Close search window"
|
||||
title="Close search window"
|
||||
@click="closeSearch"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="network && channel" class="chat-content">
|
||||
<div ref="chat" class="chat" tabindex="-1">
|
||||
<div v-show="moreResultsAvailable" class="show-more">
|
||||
<button
|
||||
ref="loadMoreButton"
|
||||
:disabled="
|
||||
!!store.state.messageSearchPendingQuery ||
|
||||
!store.state.isConnected
|
||||
"
|
||||
class="btn"
|
||||
@click="onShowMoreClick"
|
||||
>
|
||||
<span v-if="store.state.messageSearchPendingQuery">Loading…</span>
|
||||
<span v-else>Show older messages</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="store.state.messageSearchPendingQuery && !offset"
|
||||
class="search-status"
|
||||
>
|
||||
Searching…
|
||||
</div>
|
||||
<div v-else-if="!messages.length && !offset" class="search-status">
|
||||
No results found.
|
||||
</div>
|
||||
<div
|
||||
class="messages"
|
||||
role="log"
|
||||
aria-live="polite"
|
||||
aria-relevant="additions"
|
||||
>
|
||||
<div
|
||||
v-for="(message, id) in messages"
|
||||
:key="message.id"
|
||||
class="result"
|
||||
@click="jump(message, id)"
|
||||
>
|
||||
<DateMarker
|
||||
v-if="shouldDisplayDateMarker(message, id)"
|
||||
:key="message.id + '-date'"
|
||||
:message="message"
|
||||
/>
|
||||
<Message
|
||||
:key="message.id"
|
||||
:channel="channel"
|
||||
:network="network"
|
||||
:message="message"
|
||||
:data-id="message.id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.channel-name {
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import socket from "../../js/socket";
|
||||
import eventbus from "../../js/eventbus";
|
||||
|
||||
import SidebarToggle from "../SidebarToggle.vue";
|
||||
import Message from "../Message.vue";
|
||||
import MessageSearchForm from "../MessageSearchForm.vue";
|
||||
import DateMarker from "../DateMarker.vue";
|
||||
import {watch, computed, defineComponent, nextTick, ref, onMounted, onUnmounted} from "vue";
|
||||
import type {ClientMessage} from "../../js/types";
|
||||
|
||||
import {useStore} from "../../js/store";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {switchToChannel} from "../../js/router";
|
||||
import {SearchQuery} from "../../../shared/types/storage";
|
||||
|
||||
export default defineComponent({
|
||||
name: "SearchResults",
|
||||
components: {
|
||||
SidebarToggle,
|
||||
Message,
|
||||
DateMarker,
|
||||
MessageSearchForm,
|
||||
},
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const chat = ref<HTMLDivElement>();
|
||||
|
||||
const loadMoreButton = ref<HTMLButtonElement>();
|
||||
|
||||
const offset = ref(0);
|
||||
const moreResultsAvailable = ref(false);
|
||||
const oldScrollTop = ref(0);
|
||||
const oldChatHeight = ref(0);
|
||||
|
||||
const messages = computed(() => {
|
||||
const results = store.state.messageSearchResults?.results;
|
||||
|
||||
if (!results) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return results;
|
||||
});
|
||||
|
||||
const chan = computed(() => {
|
||||
const chanId = parseInt(String(route.params.id || ""), 10);
|
||||
return store.getters.findChannel(chanId);
|
||||
});
|
||||
|
||||
const network = computed(() => {
|
||||
if (!chan.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return chan.value.network;
|
||||
});
|
||||
|
||||
const channel = computed(() => {
|
||||
if (!chan.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return chan.value.channel;
|
||||
});
|
||||
|
||||
const setActiveChannel = () => {
|
||||
if (!chan.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.commit("activeChannel", chan.value);
|
||||
};
|
||||
|
||||
const closeSearch = () => {
|
||||
if (!channel.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
switchToChannel(channel.value);
|
||||
};
|
||||
|
||||
const shouldDisplayDateMarker = (message: ClientMessage, id: number) => {
|
||||
const previousMessage = messages.value[id - 1];
|
||||
|
||||
if (!previousMessage) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return new Date(previousMessage.time).getDay() !== new Date(message.time).getDay();
|
||||
};
|
||||
|
||||
const clearSearchState = () => {
|
||||
offset.value = 0;
|
||||
store.commit("messageSearchResults", null);
|
||||
store.commit("messageSearchPendingQuery", null);
|
||||
};
|
||||
|
||||
const doSearch = () => {
|
||||
if (!network.value || !channel.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearSearchState(); // this is a new search, so we need to clear anything before that
|
||||
const query: SearchQuery = {
|
||||
networkUuid: network.value.uuid,
|
||||
channelName: channel.value.name,
|
||||
searchTerm: String(route.query.q || ""),
|
||||
offset: offset.value,
|
||||
};
|
||||
store.commit("messageSearchPendingQuery", query);
|
||||
socket.emit("search", query);
|
||||
};
|
||||
|
||||
const onShowMoreClick = () => {
|
||||
if (!chat.value || !network.value || !channel.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
offset.value += 100;
|
||||
|
||||
oldScrollTop.value = chat.value.scrollTop;
|
||||
oldChatHeight.value = chat.value.scrollHeight;
|
||||
|
||||
const query: SearchQuery = {
|
||||
networkUuid: network.value.uuid,
|
||||
channelName: channel.value.name,
|
||||
searchTerm: String(route.query.q || ""),
|
||||
offset: offset.value,
|
||||
};
|
||||
store.commit("messageSearchPendingQuery", query);
|
||||
socket.emit("search", query);
|
||||
};
|
||||
|
||||
const jumpToBottom = async () => {
|
||||
await nextTick();
|
||||
|
||||
const el = chat.value;
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
el.scrollTop = el.scrollHeight;
|
||||
};
|
||||
|
||||
const jump = (message: ClientMessage, id: number) => {
|
||||
// TODO: Implement jumping to messages!
|
||||
// This is difficult because it means client will need to handle a potentially nonlinear message set
|
||||
// (loading IntersectionObserver both before AND after the messages)
|
||||
};
|
||||
|
||||
watch(
|
||||
() => route.params.id,
|
||||
() => {
|
||||
doSearch();
|
||||
setActiveChannel();
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => route.query,
|
||||
() => {
|
||||
doSearch();
|
||||
setActiveChannel();
|
||||
}
|
||||
);
|
||||
|
||||
watch(messages, async () => {
|
||||
moreResultsAvailable.value = !!(
|
||||
messages.value.length && !(messages.value.length % 100)
|
||||
);
|
||||
|
||||
if (!offset.value) {
|
||||
await jumpToBottom();
|
||||
} else {
|
||||
await nextTick();
|
||||
|
||||
const el = chat.value;
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentChatHeight = el.scrollHeight;
|
||||
el.scrollTop = oldScrollTop.value + currentChatHeight - oldChatHeight.value;
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
setActiveChannel();
|
||||
doSearch();
|
||||
|
||||
eventbus.on("escapekey", closeSearch);
|
||||
eventbus.on("re-search", doSearch);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
eventbus.off("escapekey", closeSearch);
|
||||
eventbus.off("re-search", doSearch);
|
||||
clearSearchState();
|
||||
});
|
||||
|
||||
return {
|
||||
chat,
|
||||
loadMoreButton,
|
||||
messages,
|
||||
moreResultsAvailable,
|
||||
network,
|
||||
channel,
|
||||
route,
|
||||
offset,
|
||||
store,
|
||||
setActiveChannel,
|
||||
closeSearch,
|
||||
shouldDisplayDateMarker,
|
||||
doSearch,
|
||||
onShowMoreClick,
|
||||
jumpToBottom,
|
||||
jump,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
56
client/components/Windows/Settings.vue
Normal file
56
client/components/Windows/Settings.vue
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<template>
|
||||
<div id="settings" class="window" role="tabpanel" aria-label="Settings">
|
||||
<div class="header">
|
||||
<SidebarToggle />
|
||||
</div>
|
||||
<Navigation />
|
||||
|
||||
<div class="container">
|
||||
<form ref="settingsForm" autocomplete="off" @change="onChange" @submit.prevent>
|
||||
<router-view></router-view>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent} from "vue";
|
||||
import SidebarToggle from "../SidebarToggle.vue";
|
||||
import Navigation from "../Settings/Navigation.vue";
|
||||
import {useStore} from "../../js/store";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Settings",
|
||||
components: {
|
||||
SidebarToggle,
|
||||
Navigation,
|
||||
},
|
||||
setup() {
|
||||
const store = useStore();
|
||||
|
||||
const onChange = (event: Event) => {
|
||||
const ignore = ["old_password", "new_password", "verify_password"];
|
||||
|
||||
const name = (event.target as HTMLInputElement).name;
|
||||
|
||||
if (ignore.includes(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let value: boolean | string;
|
||||
|
||||
if ((event.target as HTMLInputElement).type === "checkbox") {
|
||||
value = (event.target as HTMLInputElement).checked;
|
||||
} else {
|
||||
value = (event.target as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
void store.dispatch("settings/update", {name, value, sync: true});
|
||||
};
|
||||
|
||||
return {
|
||||
onChange,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue