Compare commits
1481 commits
feat/video
...
develop
Author | SHA1 | Date | |
---|---|---|---|
|
96a24ec625 | ||
|
91f833d1f0 | ||
|
92b04ba7df | ||
|
895eda3714 | ||
|
fb63e81ed1 | ||
|
a4ea0a30bf | ||
|
1041a38f14 | ||
|
0bb69d7fe0 | ||
|
d175e86901 | ||
|
c2a4051d72 | ||
|
169d13680a | ||
|
4d15cbcbbd | ||
|
3b02566e16 | ||
|
87311cff09 | ||
|
6951fda0d6 | ||
|
2d99cbc640 | ||
|
22c70ae22a | ||
|
d8324dd80b | ||
|
caacaf238c | ||
|
e4b8aaece6 | ||
|
bfc7b6af8f | ||
|
fce9c5eeb2 | ||
|
6de87e8b65 | ||
|
041c72b07c | ||
|
741a59e0cc | ||
|
4ddb6189dc | ||
|
0e56ac1c2b | ||
|
853f5145be | ||
|
b213d25711 | ||
|
bd77f3a1a6 | ||
|
78817e37f7 | ||
|
2b0dd2cbae | ||
|
a613447105 | ||
|
a3233e31d0 | ||
|
f71f101fce | ||
|
afdc61b9b7 | ||
|
1d1ea7e703 | ||
|
052ad2fe3f | ||
|
a0099ecb66 | ||
|
9940739f1f | ||
|
afbe1a96ac | ||
|
e029c2864f | ||
|
dadf2f407f | ||
|
6751c22a23 | ||
|
1943991077 | ||
|
115170f35d | ||
|
c3690b456e | ||
|
70593e71e6 | ||
|
1c60609547 | ||
|
b84acfd7b7 | ||
|
97e072d93a | ||
|
2179054384 | ||
|
a3f48fc3f4 | ||
|
de2f968645 | ||
|
e1483488c7 | ||
|
a1822f073d | ||
|
218b15b5fd | ||
|
bc029b0fa2 | ||
|
2d7f242713 | ||
|
0afc955ebd | ||
|
e80e53d9d2 | ||
|
afcfcce2e6 | ||
|
3171241c6f | ||
|
3e0bb91ff2 | ||
|
5bbc0e0bb5 | ||
|
3799983d4f | ||
|
3f04ebd89a | ||
|
b93e5437bd | ||
|
64d56e6515 | ||
|
d9d4d8954e | ||
|
b132581b3a | ||
|
f20ae34400 | ||
|
dc8bef7928 | ||
|
650e69c336 | ||
|
f21dc21a83 | ||
|
9afbb12f95 | ||
|
7b10e47b21 | ||
|
4b630c3c36 | ||
|
bdd240a230 | ||
|
c57af7e242 | ||
|
f706234d77 | ||
|
805615d52b | ||
|
8424b772b0 | ||
|
115f38c422 | ||
|
48fd8a66ad | ||
|
53cde52027 | ||
|
75f6506bc7 | ||
|
1d77063a4b | ||
|
ccd7378347 | ||
|
8e711e0587 | ||
|
87d420a92b | ||
|
d6bbccdd71 | ||
|
913749739f | ||
|
0b7a8dca15 | ||
|
dd4672dc9a | ||
|
9ac7046521 | ||
|
e4c804fac0 | ||
|
fd77c583bf | ||
|
b319c0c72b | ||
|
c5551e834b | ||
|
e58422889b | ||
|
b7755314b1 | ||
|
3fb647b34b | ||
|
0eb9c019e4 | ||
|
aa0b2e0723 | ||
|
4539feed40 | ||
|
20c14a1d99 | ||
|
9793002070 | ||
|
01d8fa4e54 | ||
|
961ca3a71b | ||
|
08811e5a27 | ||
|
a6fae395da | ||
|
d690b88c1c | ||
|
322ec8681a | ||
|
e1bfa6fbd3 | ||
|
db0e1a2534 | ||
|
d524e98348 | ||
|
7afa6c9f40 | ||
|
538903f9d8 | ||
|
c5a6f40dff | ||
|
b817e09ee8 | ||
|
6b5791fda6 | ||
|
3250e59266 | ||
|
e5ae0671ce | ||
|
9d7a7e2019 | ||
|
c2cf13fc00 | ||
|
c3546ea856 | ||
|
edb66ecade | ||
|
c6a4a0a320 | ||
|
6a31962ca0 | ||
|
fa99abf106 | ||
|
9be06d9f71 | ||
|
966919874e | ||
|
a253c95170 | ||
|
c6e0dcf08e | ||
|
0147226b3d | ||
|
1571053fba | ||
|
623aa3d20d | ||
|
636a55e72d | ||
|
304f871332 | ||
|
2cd8a3ec8d | ||
|
971cec024c | ||
|
7a17eb7fec | ||
|
4cb14c257c | ||
|
1187727b60 | ||
|
54fd7e2be6 | ||
|
0ef58696bf | ||
|
9478a462a7 | ||
|
4e2fd7baf9 | ||
|
66fb3987d6 | ||
|
1e60a491c4 | ||
|
12ce58a5a0 | ||
|
2f24f3312d | ||
|
1cc35b6df8 | ||
|
4f2be206df | ||
|
6074ad67ab | ||
|
c5cb76ac3b | ||
|
e4b010321d | ||
|
5e4ff5de7c | ||
|
6b14b645be | ||
|
79258f61be | ||
|
3ea442667f | ||
|
6c120cc576 | ||
|
89d6e624f2 | ||
|
060aa41f35 | ||
|
67f5cf3c03 | ||
|
5948d20f00 | ||
|
b3ed29ff02 | ||
|
5d973df5bd | ||
|
26bfbdc2ad | ||
|
50ea6dd142 | ||
|
6f1d953642 | ||
|
86f1b88d79 | ||
|
4610b6e547 | ||
|
be02516776 | ||
|
4e100b9ea2 | ||
|
8ae1f7e192 | ||
|
fea0c91f74 | ||
|
051d51bcd9 | ||
|
5718c6491e | ||
|
a97c07bfdf | ||
|
d815f984fb | ||
|
4993dc37e2 | ||
|
0671aa0dd0 | ||
|
b62653c202 | ||
|
be4244acde | ||
|
8311d4deba | ||
|
e51144809f | ||
|
cd4ad2df11 | ||
|
b632d740c1 | ||
|
123de8aad0 | ||
|
186078ae2a | ||
|
23c80794d5 | ||
|
3e1415ef89 | ||
|
2439f55342 | ||
|
b0babee98a | ||
|
47bb08514b | ||
|
60eb164bdc | ||
|
3101456a85 | ||
|
5e851e643b | ||
|
bfa62db274 | ||
|
b349bcd427 | ||
|
7e6e6292f8 | ||
|
34592fa63a | ||
|
71c0b59866 | ||
|
a97db1efd6 | ||
|
e34d71fc1f | ||
|
89efb0d2f4 | ||
|
1b204012ea | ||
|
b67a557a8c | ||
|
5fb302d0f1 | ||
|
7e21853cca | ||
|
7dd1a0dd30 | ||
|
90b066a744 | ||
|
1128cc463c | ||
|
22d8961c5e | ||
|
76727cd39c | ||
|
5829cd98af | ||
|
49fa9c47e9 | ||
|
495960c83a | ||
|
3b4afdf567 | ||
|
a485ebc2bb | ||
|
6980e4ddf1 | ||
|
cb19db1006 | ||
|
ddf6c9cef0 | ||
|
9f3a983fef | ||
|
839627ffc4 | ||
|
f3269cdc10 | ||
|
3502d374e3 | ||
|
0190a36070 | ||
|
23a6b86ef3 | ||
|
29cd8fbd3b | ||
|
a36673a6a8 | ||
|
544db06294 | ||
|
d9030b4fdd | ||
|
a7570f5eb2 | ||
|
f96e5882d1 | ||
|
f16f35a4d4 | ||
|
51b14cc615 | ||
|
80bd6433aa | ||
|
50b3a30d1a | ||
|
5930b667a1 | ||
|
9b5ae4d4a3 | ||
|
22c8ad4583 | ||
|
726fdbea15 | ||
|
67c2b52682 | ||
|
238e557d2b | ||
|
c8d87e7ef9 | ||
|
a83b07a629 | ||
|
3bd2f3c36d | ||
|
465c5f194c | ||
|
ec3df8e5f2 | ||
|
c194568daf | ||
|
63039aea37 | ||
|
fb803b2ff2 | ||
|
fabac8524d | ||
|
35ad2a1957 | ||
|
e4d40fbba4 | ||
|
d4076e5df5 | ||
|
2ced27e734 | ||
|
04d4f5525c | ||
|
949f11ea1e | ||
|
00a5dfc36f | ||
|
e29f33ed0c | ||
|
6946d09f55 | ||
|
450145dd6b | ||
|
551b8f3690 | ||
|
48178bdc53 | ||
|
a511250b63 | ||
|
415a823af0 | ||
|
0e323ef248 | ||
|
d157f67381 | ||
|
fa20ea76b1 | ||
|
e7f5033c7d | ||
|
9432fcec7d | ||
|
5768806d1b | ||
|
f8c5cbcd0d | ||
|
20880cdf0b | ||
|
cc5cff2038 | ||
|
2a510205c3 | ||
|
0db5a5a581 | ||
|
ba858a894c | ||
|
654996fdbe | ||
|
cebb4224ac | ||
|
9b27ac9aaf | ||
|
863255d52f | ||
|
c4bd004cbc | ||
|
244174a32b | ||
|
22bdcda9c0 | ||
|
ba8598858b | ||
|
17863f54fe | ||
|
26670e9003 | ||
|
10cd03c718 | ||
|
f1db5e8f4b | ||
|
d78c8e8ea4 | ||
|
e560fbc935 | ||
|
4adffb4835 | ||
|
0e4a7c3d05 | ||
|
8780246ce5 | ||
|
61bb69c88f | ||
|
bdf631c2c4 | ||
|
3addc36c96 | ||
|
f851bc92c9 | ||
|
2e54cf12c7 | ||
|
ff5f69b8fd | ||
|
d7da9f80a7 | ||
|
6b990ba368 | ||
|
cd3e6d0073 | ||
|
0aaef50ee5 | ||
|
05b2351e08 | ||
|
d9a9f97751 | ||
|
8c0deb905e | ||
|
d15d24c11c | ||
|
31c4300456 | ||
|
84a3cd92a3 | ||
|
ace1f5067c | ||
|
cd0f6a4f78 | ||
|
414ee55957 | ||
|
0f2fd8a352 | ||
|
0582f19e7c | ||
|
7e1e8ea429 | ||
|
7a8c975f61 | ||
|
1e41c2b99e | ||
|
f0f1066bc8 | ||
|
2b7f12613e | ||
|
fe0ed7e8f0 | ||
|
514d7d3f88 | ||
|
77b55a559b | ||
|
77bb0b5530 | ||
|
39909c8a85 | ||
|
f4b36a9ebf | ||
|
67319d0e5b | ||
|
cf58df17f6 | ||
|
8bb97fbfeb | ||
|
3a5ad18aca | ||
|
e3d602fdcc | ||
|
c07c0b2260 | ||
|
f626da838a | ||
|
9623b0e140 | ||
|
b1b9260a1d | ||
|
9c1814d122 | ||
|
0300db6c63 | ||
|
c7690aecd0 | ||
|
1b32bb9c51 | ||
|
9bc7d99e16 | ||
|
965bc5573f | ||
|
2559d03d7e | ||
|
58d0f9678b | ||
|
ddee8bb686 | ||
|
56616787ec | ||
|
d3659b5597 | ||
|
373c30be69 | ||
|
7b60adb480 | ||
|
5864dc52f7 | ||
|
a31ff20f50 | ||
|
2a97bdb39d | ||
|
86e3aefdab | ||
|
a8d1987686 | ||
|
0d073d607c | ||
|
769a9a14fe | ||
|
a61f6e1590 | ||
|
ee86f56469 | ||
|
17d6f1b53b | ||
|
befd4d5fc7 | ||
|
4c7edfc9a9 | ||
|
d361a4d7dc | ||
|
571e73a346 | ||
|
39ecb33883 | ||
|
0f01931309 | ||
|
a9830ff491 | ||
|
9a6363431d | ||
|
ea6114f63c | ||
|
bfb3a4364b | ||
|
6d3229b1a1 | ||
|
06042569f1 | ||
|
8ade11783a | ||
|
a5e20a4eb2 | ||
|
dd75e43c57 | ||
|
0e6af68a0f | ||
|
f886135bb7 | ||
|
c3f1765b21 | ||
|
7cc0d0763c | ||
|
c8983d5606 | ||
|
0c60b31eee | ||
|
aaf0b985ad | ||
|
9fde13c968 | ||
|
8896afd8d8 | ||
|
95007059d1 | ||
|
1d4b1b296e | ||
|
76547fe66d | ||
|
d1f02221cb | ||
|
efd558f394 | ||
|
28ee5721bb | ||
|
94c37e8adf | ||
|
e5f731c97d | ||
|
3cf7f9e3c8 | ||
|
56de3d2f52 | ||
|
c551e3e697 | ||
|
f7e2ac1c48 | ||
|
2f8aae371b | ||
|
9ea0f10abb | ||
|
182fcca5da | ||
|
6509542dbd | ||
|
2ac78219ee | ||
|
756f7bf7c2 | ||
|
4cd27acf7f | ||
|
030c374def | ||
|
056f5f547a | ||
|
af2a408b78 | ||
|
d22e04eaf6 | ||
|
4587f37dd7 | ||
|
a20f1794d0 | ||
|
b4cfda4a20 | ||
|
ea0887a15e | ||
|
d36b45ad43 | ||
|
d2730d5bad | ||
|
1c75c74c91 | ||
|
ef684dff61 | ||
|
241b4957e1 | ||
|
45eda03d1c | ||
|
1d0e4bada8 | ||
|
3c8ced53b9 | ||
|
80dd6b2500 | ||
|
ad64b91d66 | ||
|
4988268f5f | ||
|
50adf0ddf2 | ||
|
ef5bbc4e5f | ||
|
370f1e55ad | ||
|
a8a82ad12f | ||
|
1c53528433 | ||
|
8af1f08539 | ||
|
25a8b48bf2 | ||
|
59bab829a6 | ||
|
13468f2a89 | ||
|
5bb471a68e | ||
|
ff5ed29ec1 | ||
|
fa75a3a615 | ||
|
057e3dac85 | ||
|
6e57170626 | ||
|
98da3ad124 | ||
|
144cee6d34 | ||
|
0543c8d536 | ||
|
9319666f04 | ||
|
ca9652b30b | ||
|
21af736fe1 | ||
|
78ba8be969 | ||
|
29f229daad | ||
|
5049ee575f | ||
|
32ed71501a | ||
|
a7a736c7b8 | ||
|
5cbb71e588 | ||
|
fa2e5deae2 | ||
|
a3bfa63d05 | ||
|
1ef2bb93fe | ||
|
c38ab7234d | ||
|
791293c709 | ||
|
8574db1cf1 | ||
|
90d553f4be | ||
|
d3139a92b3 | ||
|
bc08f998cf | ||
|
f72671a1aa | ||
|
738e7923e4 | ||
|
b3f15fe3e1 | ||
|
761f91f7ef | ||
|
c509ed357a | ||
|
18871684c7 | ||
|
33e2bcce31 | ||
|
4d529c13ba | ||
|
0e53b2916e | ||
|
04a49e4c42 | ||
|
f57f61ca53 | ||
|
4302db5975 | ||
|
cdcefc2b73 | ||
|
4d73eaa6ce | ||
|
39494439d3 | ||
|
c3576211cb | ||
|
cbb34e2b0e | ||
|
e98a2af39e | ||
|
dbdc5e050f | ||
|
49fe334186 | ||
|
14ec7d6a41 | ||
|
7cc19ef2ea | ||
|
0507eb6550 | ||
|
830a03a0d1 | ||
|
34d265467a | ||
|
f5823a96e9 | ||
|
777f6c0162 | ||
|
0087d33c75 | ||
|
299c00cf74 | ||
|
07c12ae162 | ||
|
c8a7b6f433 | ||
|
5431d8fe55 | ||
|
8cc1ad67df | ||
|
6aa6f6f300 | ||
|
17d2eed06a | ||
|
f16658adfc | ||
|
68b4323181 | ||
|
7d67e8f1cc | ||
|
530ac4442b | ||
|
4465de5241 | ||
|
97e86381c8 | ||
|
4c974f5ca2 | ||
|
6c6df29ed3 | ||
|
add5921b8b | ||
|
2182af4058 | ||
|
2cfff1b8b9 | ||
|
4686993334 | ||
|
435f80133a | ||
|
ef277ae4e2 | ||
|
f35e3d0f3f | ||
|
179af131ee | ||
|
425919a0d2 | ||
|
ba961b784f | ||
|
891611816c | ||
|
373b14e1e4 | ||
|
a0eaac2216 | ||
|
b67db47c88 | ||
|
dd3fe61cf3 | ||
|
a2f21f4e13 | ||
|
628b99d117 | ||
|
5118eee19a | ||
|
4ba8d95a10 | ||
|
4016182b89 | ||
|
a258182522 | ||
|
6125dc885a | ||
|
19475ba356 | ||
|
cd9dd352e3 | ||
|
c6831a3810 | ||
|
8fe4355a6b | ||
|
6b8b9c017f | ||
|
44b741e270 | ||
|
8bab8658e8 | ||
|
bfe31e20ea | ||
|
5c2744b426 | ||
|
b68fb7738b | ||
|
f35c090caa | ||
|
c1293c3afa | ||
|
f15599e6e5 | ||
|
90345f158f | ||
|
85e2f8f78c | ||
|
b2ebfc1fd6 | ||
|
e1361a1cae | ||
|
e654fead23 | ||
|
9c4957268d | ||
|
6689fed513 | ||
|
4ecbb58086 | ||
|
0a3ce9cc8b | ||
|
dcfd178314 | ||
|
1b26c713ef | ||
|
9e9ab5cec9 | ||
|
a96a62929d | ||
|
1717a3aaf2 | ||
|
25bf28f051 | ||
|
ad3a2fd4e5 | ||
|
139a0d1562 | ||
|
cab0095989 | ||
|
338134acfb | ||
|
d7a53aec61 | ||
|
4aac0125e5 | ||
|
7e3393b5a2 | ||
|
5047663c51 | ||
|
e67f295497 | ||
|
312a237ca4 | ||
|
4639e30cb8 | ||
|
cbccea0546 | ||
|
b88e6b8ab0 | ||
|
8fa0331771 | ||
|
1668315bf8 | ||
|
1fcccd7570 | ||
|
0c10145242 | ||
|
67c9d8bd55 | ||
|
adfe56a3a3 | ||
|
63c22ad131 | ||
|
7309f8ce1a | ||
|
c21b1cf898 | ||
|
a3c703bd37 | ||
|
636dbdaba8 | ||
|
1fdfc42159 | ||
|
609dc5da0c | ||
|
bebafa1a2c | ||
|
e825021ef1 | ||
|
9c70f3e4df | ||
|
2c60a9b638 | ||
|
18fb7516cc | ||
|
418f029789 | ||
|
90a188f2c3 | ||
|
cd44556750 | ||
|
ca6c7d5b10 | ||
|
24f3681ac1 | ||
|
647e4476f9 | ||
|
c1bd36dc6f | ||
|
ffc501eb23 | ||
|
9421501c1e | ||
|
5834790d0b | ||
|
f819227bed | ||
|
255f47fe56 | ||
|
f883d2f75c | ||
|
b84aeff6bf | ||
|
cc00af7a31 | ||
|
0f73e96194 | ||
|
0263834faa | ||
|
6bff7cc6ef | ||
|
0260693f51 | ||
|
394fd462dc | ||
|
c6c478f4cf | ||
|
aec867b300 | ||
|
566964992a | ||
|
5c655b6675 | ||
|
8c8237418c | ||
|
963f1679e0 | ||
|
a3b8e7ad99 | ||
|
b87a9d6675 | ||
|
7ae85c8318 | ||
|
0ae3985a52 | ||
|
2f383c2c01 | ||
|
73127f0e25 | ||
|
9ea370033a | ||
|
3abd357694 | ||
|
0583a6b863 | ||
|
6bc9886db4 | ||
|
ccdf892483 | ||
|
38d9ea8b17 | ||
|
5740a79dbd | ||
|
e6d5ddcbb6 | ||
|
59d046b163 | ||
|
c3e122ff6f | ||
|
7d6fc044fb | ||
|
6199788f28 | ||
|
8045d1866e | ||
|
6090327236 | ||
|
5e83672274 | ||
|
aa38223e87 | ||
|
8e9f5d7580 | ||
|
50aa379038 | ||
|
04fa1f0b2d | ||
|
aec05686d0 | ||
|
b0ae32e309 | ||
|
22c8f71945 | ||
|
1923ed84d4 | ||
|
a2459c2187 | ||
|
be79643bcf | ||
|
35dedf8416 | ||
|
5970ddf9ac | ||
|
20ce646852 | ||
|
2725a0c639 | ||
|
bd98ecb3f0 | ||
|
2a2483f4c9 | ||
|
008e711e11 | ||
|
21477d07e9 | ||
|
ed824d964e | ||
|
e0cde9a29b | ||
|
0604b1d5b7 | ||
|
32d1a0e181 | ||
|
7bd18cda64 | ||
|
e95412a03c | ||
|
0ca0e642a4 | ||
|
4e96af0442 | ||
|
80220c1b07 | ||
|
c039656460 | ||
|
dc611dffdb | ||
|
159bbed2f9 | ||
|
245addb530 | ||
|
0741d1d93d | ||
|
477e22aa9c | ||
|
7341b8a551 | ||
|
711bf0910a | ||
|
330665dacb | ||
|
e338f6be75 | ||
|
eda817cc18 | ||
|
1e0479b171 | ||
|
64aba422d5 | ||
|
98da8fd441 | ||
|
b50a9a1d56 | ||
|
fdd4be3dcb | ||
|
407bdbf996 | ||
|
cec13609cd | ||
|
e55644f153 | ||
|
98d12beb9e | ||
|
d7607792fe | ||
|
f28f632e86 | ||
|
62ba237217 | ||
|
e053ac9865 | ||
|
e8449166e1 | ||
|
fd3b806c24 | ||
|
39b6214447 | ||
|
f8fde93c51 | ||
|
a6f66cfa2a | ||
|
acfd70bd3e | ||
|
303cf39142 | ||
|
31e342a005 | ||
|
61b60f8aa3 | ||
|
98735bf340 | ||
|
4cebc94324 | ||
|
be09a42253 | ||
|
3a0d4fdc24 | ||
|
6d40c4f9be | ||
|
5e82b7e316 | ||
|
dc81367260 | ||
|
e262103e7d | ||
|
2d8c325926 | ||
|
78fbee36aa | ||
|
099b5a7d38 | ||
|
55fa353469 | ||
|
70cef8d9b5 | ||
|
e958c8e943 | ||
|
a0453f7af8 | ||
|
225f8c44d6 | ||
|
8830a1652c | ||
|
3f68af086a | ||
|
b780f76b37 | ||
|
b406ebdc39 | ||
|
323cc8271f | ||
|
0578467ecf | ||
|
35438d93f0 | ||
|
110a37f68f | ||
|
4d54ae91d7 | ||
|
62679e24ab | ||
|
c1da12e2cd | ||
|
5ef0184469 | ||
|
b65ee94f93 | ||
|
8a9115b58e | ||
|
b6e8c12dbc | ||
|
e73cb423b6 | ||
|
e47d5ba53b | ||
|
8d46fd78c7 | ||
|
95e74319e1 | ||
|
709b75198d | ||
|
caed89f0ae | ||
|
72956e2343 | ||
|
4b18e0f36e | ||
|
b479d80366 | ||
|
9e8513b312 | ||
|
52835cf8bf | ||
|
905b9771ec | ||
|
fca885e665 | ||
|
6e687c0663 | ||
|
180da297f6 | ||
|
b774472fff | ||
|
76a2e6befb | ||
|
1f5f612163 | ||
|
509ec99574 | ||
|
ced9c0fa7e | ||
|
e00c3ccf36 | ||
|
b486788ed9 | ||
|
7acad87806 | ||
|
2d10084939 | ||
|
7a46e81edf | ||
|
0775970476 | ||
|
3f4487b5b8 | ||
|
2435d93d2d | ||
|
29faa6f124 | ||
|
b97dfec5f6 | ||
|
43b4223f16 | ||
|
86a851849a | ||
|
3dca3639fd | ||
|
06d0254cc5 | ||
|
40ac9ef499 | ||
|
33777fab47 | ||
|
b0789fd6fd | ||
|
433ea02a18 | ||
|
09ef284af7 | ||
|
87903fbf6d | ||
|
6b250762f0 | ||
|
f0641d05df | ||
|
5fdc4a1904 | ||
|
adafae977a | ||
|
2f549774ab | ||
|
d695dcaff9 | ||
|
61dcdbf992 | ||
|
1afda1ac6d | ||
|
0c77a3e1d6 | ||
|
4e56e64034 | ||
|
8b96ea9377 | ||
|
4e3c4ec1db | ||
|
bb7d89cd8c | ||
|
16beb3cbda | ||
|
9701a28a34 | ||
|
5be18d177d | ||
|
5d9f1fa76f | ||
|
d3ae0b3b97 | ||
|
e11e23e6f9 | ||
|
dc37f07fe2 | ||
|
2367e7ce8f | ||
|
5872e3dd54 | ||
|
2392307290 | ||
|
e199f26632 | ||
|
d716026f54 | ||
|
9801906ea1 | ||
|
cf35a9697e | ||
|
c97ad72cf0 | ||
|
41034141d8 | ||
|
0a4efeb843 | ||
|
fb1e57b6b6 | ||
|
ecac5bb015 | ||
|
ba8a2f4a20 | ||
|
7fa7809c50 | ||
|
615925e53d | ||
|
bb1bcfd084 | ||
|
f8e2fde99b | ||
|
35d2a809d2 | ||
|
fa6aba1dbd | ||
|
2e6a7c9fb8 | ||
|
b0d450075d | ||
|
6829c92f63 | ||
|
47507b72dc | ||
|
37944a19c3 | ||
|
65e510c3f2 | ||
|
f28d71d769 | ||
|
73b053db5c | ||
|
a450772039 | ||
|
fe1d90ebc3 | ||
|
e9ead1bfdd | ||
|
7ef1db1556 | ||
|
3739c58855 | ||
|
3a07fe2572 | ||
|
8a9913c5f6 | ||
|
0a0bb6078b | ||
|
08a44ee4a1 | ||
|
822d73c221 | ||
|
deac610df6 | ||
|
a7d64d038a | ||
|
f70921b984 | ||
|
d839c1ac89 | ||
|
aacd909846 | ||
|
d57ee274de | ||
|
8163c7b55b | ||
|
4c36ac12b6 | ||
|
d262f208dc | ||
|
feb40ec5ff | ||
|
5ce298ead4 | ||
|
47719571e4 | ||
|
a00212a3bb | ||
|
c682c1730f | ||
|
19fc7dda9e | ||
|
184364c7e0 | ||
|
2da37f15ab | ||
|
8e88d8110b | ||
|
1f0ac68fcd | ||
|
3870a30aea | ||
|
5d3bf43fdc | ||
|
c6d4c20982 | ||
|
5c064ccf55 | ||
|
0475e1c61c | ||
|
088683538d | ||
|
badb2196a2 | ||
|
a8967d85bd | ||
|
90afcd3420 | ||
|
2e7bd99444 | ||
|
3d95ea6acb | ||
|
fada49768d | ||
|
914b4eb593 | ||
|
395e12cbc6 | ||
|
dda95543e8 | ||
|
bd5b62b107 | ||
|
4baa397ed0 | ||
|
8a590f9269 | ||
|
6281241b92 | ||
|
c14c144cc8 | ||
|
b4f5df9ce5 | ||
|
fb183adc74 | ||
|
becacf0643 | ||
|
0673511fc2 | ||
|
30057a4944 | ||
|
d1ab424ebc | ||
|
ae159f6ad8 | ||
|
11a036d6d6 | ||
|
f6af4c43f6 | ||
|
6d7b5b157b | ||
|
fc5483f764 | ||
|
a90910be8f | ||
|
846e58c3d2 | ||
|
91f93d4a55 | ||
|
670abd633f | ||
|
b4782ad159 | ||
|
92a9ce67c5 | ||
|
aa5cb3d1d2 | ||
|
237f272d15 | ||
|
a83fdbbd59 | ||
|
49aa10e1c0 | ||
|
09fe160e8b | ||
|
7c37f495f6 | ||
|
cd2f5ced31 | ||
|
59db4582b0 | ||
|
20b755d57e | ||
|
a7a69d08a7 | ||
|
2e7529cf50 | ||
|
6fefa5a9c5 | ||
|
cef6ecb916 | ||
|
acc08932cd | ||
|
74caf42ed7 | ||
|
34d18ac0c4 | ||
|
dd9e18fd34 | ||
|
59aaade7fc | ||
|
096747a5dc | ||
|
93785634a7 | ||
|
51a78e8b8a | ||
|
ecb211606c | ||
|
29dae3c12e | ||
|
3f23aecd10 | ||
|
5faca01261 | ||
|
485f4b899c | ||
|
e14b9ddc02 | ||
|
3d013630ae | ||
|
589ab6510c | ||
|
e8b8c3cc49 | ||
|
2f60c6a821 | ||
|
98cb9abac7 | ||
|
5ddfc787ed | ||
|
226ec1c5be | ||
|
3983ea79cd | ||
|
20997d6cfc | ||
|
d5f191db38 | ||
|
d0b1a68f86 | ||
|
a89710452a | ||
|
663362db56 | ||
|
a7f055a875 | ||
|
67f3532ac9 | ||
|
ee1cf36d52 | ||
|
7834ff52b1 | ||
|
47770ed715 | ||
|
e720d4dd8a | ||
|
aa240f935f | ||
|
265bb2cd41 | ||
|
23a5c601a7 | ||
|
b4580d086f | ||
|
a636e53404 | ||
|
2f8d4c7406 | ||
|
3d25946bbf | ||
|
c3b267f2b9 | ||
|
44bddf6cd2 | ||
|
7e11093fcd | ||
|
cba48476ad | ||
|
033b7eaeb9 | ||
|
6acf812101 | ||
|
376d431681 | ||
|
0dc6937bf8 | ||
|
a848462f19 | ||
|
cccdda3a7b | ||
|
e85fa160c7 | ||
|
586c538aa0 | ||
|
21e2b3ce0e | ||
|
11081c2870 | ||
|
4c845a1a99 | ||
|
0a3f40eebb | ||
|
63317a2fdf | ||
|
0ffb7b67ed | ||
|
1a836c8527 | ||
|
7f3fd9ca21 | ||
|
c7018057f1 | ||
|
398aac6558 | ||
|
01275fbac0 | ||
|
4859e63a89 | ||
|
b2a96417cf | ||
|
da807a12fb | ||
|
8b7c367b04 | ||
|
13ff99881b | ||
|
1506b97e35 | ||
|
647d75f27c | ||
|
213c5637d4 | ||
|
6f3acb3c1b | ||
|
7d4c7e3b3f | ||
|
78f8147aa6 | ||
|
982c799b6f | ||
|
66f3e72b54 | ||
|
8958f386be | ||
|
de66267a07 | ||
|
6e4a0d408c | ||
|
2576b75059 | ||
|
11963de288 | ||
|
79e4df99dc | ||
|
58a92c1b7d | ||
|
286527b489 | ||
|
bc9cd4170d | ||
|
c4fb123d07 | ||
|
3b6a30ec9f | ||
|
238e9dcd35 | ||
|
c3e5fd5fa1 | ||
|
05e5bb6404 | ||
|
836fc4d205 | ||
|
c8240a48d5 | ||
|
d39e918cd3 | ||
|
da47d9a43d | ||
|
0c2b425682 | ||
|
ffd673d4a5 | ||
|
6df5459ec9 | ||
|
c5477e489b | ||
|
cdd632f04c | ||
|
e9c5e06f50 | ||
|
a9cbf3eafe | ||
|
ac43a8145b | ||
|
21eac51029 | ||
|
600ff9a67b | ||
|
a5def5cf56 | ||
|
24f7cbf3c9 | ||
|
916d4e0496 | ||
|
1036395dfc | ||
|
58528605c8 | ||
|
9a074fbdfa | ||
|
e8fb0f313e | ||
|
67a0d83568 | ||
|
7c295e1e0f | ||
|
a02b769c8f | ||
|
7b6f68ebcd | ||
|
9cf487497e | ||
|
eb2975b64d | ||
|
272ba8f7a9 | ||
|
28ebb8b0ae | ||
|
27a537d307 | ||
|
daa9f211a6 | ||
|
60a8a89f5b | ||
|
0358284ebf | ||
|
11c7355749 | ||
|
f614da2abb | ||
|
831cf9eafb | ||
|
e14917e28d | ||
|
02ab803725 | ||
|
9bf80cc7be | ||
|
62dcf34e0d | ||
|
8011556c28 | ||
|
dcba920f92 | ||
|
b76a68e622 | ||
|
36e56354e4 | ||
|
9656c9b969 | ||
|
fe78ed78fd | ||
|
9a8bc245a6 | ||
|
48bef143d8 | ||
|
64fa662644 | ||
|
adc3b17fe0 | ||
|
835eaf33b1 | ||
|
8c82bb61f1 | ||
|
38e5eec122 | ||
|
f52bca04c9 | ||
|
0ee0551a47 | ||
|
65dbf7b85d | ||
|
728726cf17 | ||
|
cdb9b4aea2 | ||
|
5ab96ae0d2 | ||
|
1a89ec5d8a | ||
|
83db5f0916 | ||
|
4f69c41fa5 | ||
|
8729c66374 | ||
|
6670668a7c | ||
|
c1de7b60e1 | ||
|
bdee1277b1 | ||
|
12991f915f | ||
|
2053b3396a | ||
|
e98c19c725 | ||
|
3681d6bbd1 | ||
|
e3b02de6fb | ||
|
fa6cfc8238 | ||
|
2509bfc5e6 | ||
|
d50798e8ce | ||
|
b5dca9bbe2 | ||
|
3987020216 | ||
|
271f9e539a | ||
|
a2dff27737 | ||
|
467efc4639 | ||
|
646ff273ee | ||
|
2b6b5ff227 | ||
|
cf796d8f5c | ||
|
762204f3be | ||
|
d1f51d946b | ||
|
837df4f5a5 | ||
|
76809e9d24 | ||
|
1847315353 | ||
|
07b31a7d9c | ||
|
9780e1fc11 | ||
|
21f624609f | ||
|
3e7d1fbf23 | ||
|
ef6e2087ae | ||
|
bc23266d70 | ||
|
4b0308ad1f | ||
|
9d45db959f | ||
|
9bfc3c275c | ||
|
34d8893f1c | ||
|
a36922ae1b | ||
|
3df84ccd5b | ||
|
243383852b | ||
|
2312f6c15a | ||
|
f01dce99df | ||
|
481c71517e | ||
|
8d9bf3efc8 | ||
|
93af7ee36a | ||
|
3359d4ddf6 | ||
|
a2f1ad93f5 | ||
|
10399c193a | ||
|
466fc84e82 | ||
|
0cf06cb9ec | ||
|
81bac9f8ac | ||
|
5cb41c7175 | ||
|
c197756767 | ||
|
34c65e3bcf | ||
|
6a469963b0 | ||
|
e8cebb568b | ||
|
22546ea813 | ||
|
5077549c73 | ||
|
32ed09bae5 | ||
|
6d9c79fb6d | ||
|
39c4672175 | ||
|
4451ac9f7c | ||
|
bdb994d450 | ||
|
51e6f5e255 | ||
|
415119cda9 | ||
|
99e12091b7 | ||
|
83bc74de0a | ||
|
674c302350 | ||
|
45e08f83aa | ||
|
3b037218b2 | ||
|
506cf0902e | ||
|
bfeb8ed14b | ||
|
ffd0bbbf16 | ||
|
ff93f43468 | ||
|
56927c5a09 | ||
|
c74ee1153d | ||
|
ccf357c206 | ||
|
99c1fb0068 | ||
|
dc90db91fd | ||
|
0c3f62642b | ||
|
6b7f82174c | ||
|
a8d3ce4671 | ||
|
95b911d7a0 | ||
|
f1066c393f | ||
|
215e3753e1 | ||
|
426441b51c | ||
|
fc4a17aa8c | ||
|
f30450be12 | ||
|
17abcda2aa | ||
|
0f8587463c | ||
|
3d8af7256b | ||
|
60598f9bda | ||
|
800964ea6b | ||
|
06d05b7b82 | ||
|
ac45e410d3 | ||
|
4667137fff | ||
|
037185a463 | ||
|
33e3ada7da | ||
|
00c762474f | ||
|
ee0748bff5 | ||
|
2bbb01e751 | ||
|
2ac23d8318 | ||
|
3dcbc2c294 | ||
|
0f2841a98a | ||
|
8a260f4102 | ||
|
c20c418e6d | ||
|
fdcd4828ed | ||
|
6923d5f983 | ||
|
35773e2339 | ||
|
291d9929f8 | ||
|
a3f6476ae1 | ||
|
be2e6f39aa | ||
|
6111affc61 | ||
|
4f54b0123a | ||
|
a5b961ed31 | ||
|
ed5c1c0a4c | ||
|
061a500e0a | ||
|
78b052b6fc | ||
|
66ad886e33 | ||
|
c823800a67 | ||
|
d9b4c87dda | ||
|
60121775f2 | ||
|
8fbf91f3e8 | ||
|
e713e17e9d | ||
|
a4a0695c57 | ||
|
9657a5a098 | ||
|
55b0b81252 | ||
|
ae3ded9185 | ||
|
8140501e37 | ||
|
71b0ef8f1b | ||
|
0dc0c816ea | ||
|
2478b5a306 | ||
|
a464135cbd | ||
|
3f452a5462 | ||
|
431e9369c2 | ||
|
c789db82df | ||
|
171462b246 | ||
|
0cccb39710 | ||
|
a7b2c5cdad | ||
|
a6c93c5eab | ||
|
badbd27e27 | ||
|
1116b2cd32 | ||
|
9cb83648e3 | ||
|
42c747a342 | ||
|
2427757184 | ||
|
5d95376bef | ||
|
46320090e3 | ||
|
fa01030641 | ||
|
f4b18df644 | ||
|
15ea735006 | ||
|
0d53af786b | ||
|
b537032e6d | ||
|
4dde9c4d52 | ||
|
fdfb8810c1 | ||
|
c655699935 | ||
|
10e2648142 | ||
|
655d985a82 | ||
|
133294dc2a | ||
|
397622078f | ||
|
f1b36f3b1a | ||
|
c4b098be0e | ||
|
cf3ad0632a | ||
|
848b77acaa | ||
|
bd1564765c | ||
|
c17a0523d5 | ||
|
e0d08490a4 | ||
|
2506b2629a | ||
|
e98e285f50 | ||
|
767db567ad | ||
|
a4e76067db | ||
|
8f118c01a4 | ||
|
00b93c0947 | ||
|
95ad9efff3 | ||
|
aec6dc7858 | ||
|
6fbd6ee9ea | ||
|
83319904c7 | ||
|
a68899b4a9 | ||
|
4ac2bc1c44 | ||
|
32f77cfbd7 | ||
|
e6e3b752d6 | ||
|
276ef31145 | ||
|
5dfcf2acac | ||
|
1172feaa72 | ||
|
aea172a6f7 | ||
|
bee77ffdb2 | ||
|
ebe3b38a90 | ||
|
2137d6395a | ||
|
e771fd8f13 | ||
|
1d178b7341 | ||
|
04af772c1e | ||
|
a6e495e413 | ||
|
38c32c7f7a | ||
|
8b775f94a9 | ||
|
7b99d98c55 | ||
|
14ce0c1c07 | ||
|
11f7b818b4 | ||
|
f5186e1a33 | ||
|
838cbd8715 | ||
|
4922a8ded9 | ||
|
45180d4069 | ||
|
0089d1e8d1 | ||
|
9684bf6e94 | ||
|
4001c075d4 | ||
|
b1ab09b348 | ||
|
419df9d446 | ||
|
1cd222d85c | ||
|
1495db084a | ||
|
0f386ccbc7 | ||
|
d770bab1b0 | ||
|
cd784396e7 | ||
|
b66b4124f4 | ||
|
5c47aeeccf | ||
|
d905a6cb70 | ||
|
dd3c8631bf | ||
|
49f7c84e3f | ||
|
fa2884a805 | ||
|
6cdf347ed1 | ||
|
fb5d3d3292 | ||
|
3e1c828ba9 | ||
|
476154d0ee | ||
|
563377b59c | ||
|
b536c6e3c5 | ||
|
41dbdc2b28 | ||
|
1d2ba946b6 | ||
|
d150dae5d1 | ||
|
e6ca489d30 | ||
|
c1c207788a | ||
|
fb80dbbc77 | ||
|
553155fc49 | ||
|
3963f240d5 | ||
|
9ff48b88bf | ||
|
3220e72eb9 | ||
|
def1bd0676 | ||
|
9f96b592d4 | ||
|
82f17961d1 | ||
|
56c1d65384 | ||
|
60983ae42b | ||
|
15ea9d8c91 | ||
|
dbeecd18ac | ||
|
b485d21ba9 | ||
|
4e4b258c54 | ||
|
d126eddfca | ||
|
22bc0c0840 | ||
|
0206b2bcc5 | ||
|
979efb301a | ||
|
19fd1d4a1e | ||
|
e351665bb3 | ||
|
b6a8ca44ef | ||
|
bdf2f36f11 | ||
|
08e6e66642 | ||
|
3db218dec1 | ||
|
1fa046126e | ||
|
350eb489c2 | ||
|
f0a66448ee | ||
|
a1e7852c10 | ||
|
5ab1b0ab0e | ||
|
279205ade5 | ||
|
bcb6ee328f | ||
|
757706425a | ||
|
5e8db7ed93 | ||
|
5254fdba75 | ||
|
590f8e1754 | ||
|
78e5a63922 | ||
|
e798e9a417 | ||
|
148789767a | ||
|
40ce3a9bd8 | ||
|
8ae77e14d0 | ||
|
64da9a8e69 | ||
|
8b1213ea1e | ||
|
994b49ddfe | ||
|
02bb015c4f | ||
|
4cf4de8d28 | ||
|
5095143c47 | ||
|
1c7ec321d9 | ||
|
599256a6be | ||
|
66d07b6d08 | ||
|
ea300a7a1c | ||
|
891593169f | ||
|
6040c1ebfc | ||
|
9d1d695627 | ||
|
918563d15c | ||
|
8c5586dda4 | ||
|
fb00f8459c | ||
|
0653f18994 | ||
|
4e1f2aeabc | ||
|
633349ddff | ||
|
0f8a7037ea | ||
|
8225717a7c | ||
|
3ead79ddb4 | ||
|
6acb61f296 | ||
|
93f9ab6f2b | ||
|
cb3578fcca | ||
|
a84db4bd8f | ||
|
ef36ca44f6 | ||
|
2e6c51dfd4 | ||
|
0ac9d81788 | ||
|
fc7dfb3b9e | ||
|
7c4af4ce3f | ||
|
b6cf2bcefd | ||
|
6d7ecd7d80 | ||
|
7007659e05 | ||
|
24d85ce6dc | ||
|
85dc4002a1 | ||
|
1403f20f9f | ||
|
2c441c7922 | ||
|
e2c4816feb | ||
|
22f3801be7 | ||
|
9ef4d473cb | ||
|
048e67f5fc | ||
|
eb04ed865e | ||
|
1b50d700aa | ||
|
f561e75553 | ||
|
1ec41302f7 | ||
|
6aa7445ea7 | ||
|
7495c6b698 | ||
|
a50cd7e37d | ||
|
3cbaa00449 | ||
|
b19c2eb0fb | ||
|
046c60cb61 | ||
|
e8650d3409 | ||
|
8b3a7ae8c0 | ||
|
3814218277 | ||
|
350f25016f | ||
|
cf65ecb99d | ||
|
f31df6406a | ||
|
54987b0bc8 | ||
|
750680d1d8 | ||
|
4835f567d4 | ||
|
81a59feab1 | ||
|
e695506c51 | ||
|
a664fde02f | ||
|
76a9a51620 | ||
|
c7dddb1ec3 | ||
|
29ff0be92c | ||
|
4df033a3ba | ||
|
85326ee9e2 | ||
|
def04380da | ||
|
0e14cd6102 | ||
|
d86cd3e850 | ||
|
3ca729d098 | ||
|
e5bd1c20b0 | ||
|
069a4b0801 | ||
|
e7a67e33ad | ||
|
6a3c775006 | ||
|
09da102a4c | ||
|
20bdf32658 | ||
|
ba25c618cc | ||
|
0042cf3809 | ||
|
3e86b891e1 | ||
|
6cf15dce96 | ||
|
a0da6c31d9 | ||
|
ff2fe5c68c | ||
|
6dec154450 | ||
|
b12b6a84a4 | ||
|
14dc0eef3b | ||
|
7e4d1b0abb | ||
|
dff7e38154 | ||
|
f110258fa1 | ||
|
71fca987a8 | ||
|
e9f13895b9 | ||
|
d11ecc5987 | ||
|
38fa2111f9 | ||
|
c9033e0351 | ||
|
f10abd29f0 | ||
|
cd6adac59a | ||
|
fffce4aa98 | ||
|
14a2328b85 | ||
|
b3e1cb81a3 | ||
|
970bfa2bdd | ||
|
beec425cce | ||
|
c55e09a513 | ||
|
887ff4dec3 | ||
|
32ddb38736 | ||
|
ad6b7148d4 | ||
|
aca8570b86 | ||
|
3f119c6875 | ||
|
6bd38c7d6f | ||
|
414558665f | ||
|
8a34ff2957 | ||
|
a463959a36 | ||
|
68c2a5b18f | ||
|
c17012cfe1 | ||
|
1675f1a133 | ||
|
91778a0276 | ||
|
f174f289a9 | ||
|
3781e521e6 | ||
|
787f52e06e | ||
|
3dacef944c | ||
|
bb59b8ee56 | ||
|
cff202241b | ||
|
f1e1f20a8d | ||
|
fee654f1ee | ||
|
0c0722a092 | ||
|
20362546d1 | ||
|
f9977dbb3c | ||
|
5942001626 | ||
|
3dbc1bedbf | ||
|
b725df6352 | ||
|
0fee9a8fe7 | ||
|
70a5619496 | ||
|
ee620f6028 | ||
|
20d33c2fbc | ||
|
1b811d8b93 | ||
|
d6fe4f6a4f | ||
|
38189ee838 | ||
|
c00c20a31f | ||
|
8c4514013d | ||
|
92caaa2d7f | ||
|
679a1f5e1c | ||
|
afb2241a5b | ||
|
8975589bca | ||
|
947d7cd6f2 | ||
|
fa91766519 | ||
|
1ec9cde963 | ||
|
da63894072 | ||
|
26c42484f0 | ||
|
002cd61353 | ||
|
a0aa45d328 | ||
|
31a9da05ff | ||
|
8a0f280cd8 | ||
|
0758569625 | ||
|
f9ac23b9a9 | ||
|
fa2b680855 | ||
|
40ca0b394e | ||
|
b8afdb4c02 | ||
|
e331db79e5 | ||
|
c89ac79140 | ||
|
3fb35e8123 | ||
|
5b403ba7d1 | ||
|
4d080a1654 | ||
|
d939f2ffbc | ||
|
1b6eee0497 | ||
|
2d914c331e | ||
|
12519a54b5 | ||
|
08444c3903 | ||
|
e3d0b2f137 | ||
|
a52ae1ebeb | ||
|
64c180c838 | ||
|
f892b26d73 | ||
|
40c4a49223 | ||
|
99db89fa17 | ||
|
2810f10d14 | ||
|
50bb8865c5 |
363 changed files with 30504 additions and 10469 deletions
2
.babelrc
2
.babelrc
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"presets": ["@babel/preset-env"],
|
"presets": ["@babel/preset-env"],
|
||||||
"plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-transform-vue-jsx"],
|
"plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-jsx"],
|
||||||
"comments": false
|
"comments": false
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# This file is a template, and might need editing before it works on your project.
|
# This file is a template, and might need editing before it works on your project.
|
||||||
# Official framework image. Look for the different tagged releases at:
|
# Official framework image. Look for the different tagged releases at:
|
||||||
# https://hub.docker.com/r/library/node/tags/
|
# https://hub.docker.com/r/library/node/tags/
|
||||||
image: node:10
|
image: node:12
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- lint
|
- lint
|
||||||
|
|
1
.mailmap
Normal file
1
.mailmap
Normal file
|
@ -0,0 +1 @@
|
||||||
|
rinpatch <rin@patch.cx> <rinpatch@sdf.org>
|
176
CHANGELOG.md
176
CHANGELOG.md
|
@ -3,16 +3,185 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
## [Unreleased]
|
## Unreleased
|
||||||
## [Unreleased patch]
|
### Fixed
|
||||||
|
- AdminFE button no longer scrolls page to top when clicked
|
||||||
|
- Pinned statuses no longer appear at bottom of user timeline (still appear as part of the timeline when fetched deep enough)
|
||||||
|
- Fixed many many bugs related to new mentions, including spacing and alignment issues
|
||||||
|
- Links in profile bios now properly open in new tabs
|
||||||
|
- Inline images now respect their intended width/height attributes
|
||||||
|
- Links with `&` in them work properly now
|
||||||
|
- Interaction list popovers now properly emojify names
|
||||||
|
- Completely hidden posts still had 1px border
|
||||||
|
- Attachments are ALWAYS in same order as user uploaded, no more "videos first"
|
||||||
|
- Attachment description is prefilled with backend-provided default when uploading
|
||||||
|
- Proper visual feedback that next image is loading when browsing
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- (You)s are optional (opt-in) now, bolding your nickname is also optional (opt-out)
|
||||||
|
- User highlight background now also covers the `@`
|
||||||
|
- Reverted back to textual `@`, svg version is opt-in.
|
||||||
|
- Settings window has been throughly rearranged to make make more sense and make navication settings easier.
|
||||||
|
- Uploaded attachments are uniform with displayed attachments
|
||||||
|
- Flash is watchable in media-modal (takes up nearly full screen though due to sizing issues)
|
||||||
|
- Notifications about likes/repeats/emoji reacts are now minimized so they always take up same amount of space irrelevant to size of post.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Options to show domains in mentions
|
||||||
|
- Option to show user avatars in mention links (opt-in)
|
||||||
|
- Option to disable the tooltip for mentions
|
||||||
|
- Option to completely hide muted threads
|
||||||
|
- Ability to open videos in modal even if you disabled that feature, via an icon button
|
||||||
|
- New button on attachment that indicates that attachment has a description and shows a bar filled with description
|
||||||
|
- Attachments are truncated just like post contents
|
||||||
|
- Media modal now also displays description and counter position in gallery (i.e. 1/5)
|
||||||
|
- Ability to rearrange order of attachments when uploading
|
||||||
|
- Enabled users to zoom and pan images in media viewer with mouse and touch
|
||||||
|
|
||||||
|
|
||||||
|
## [2.4.2] - 2022-01-09
|
||||||
|
### Added
|
||||||
|
- Added Apply and Reset buttons to the bottom of theme tab to minimize UI travel
|
||||||
|
- Implemented user option to always show floating New Post button (normally mobile-only)
|
||||||
|
- Display reasons for instance specific policies
|
||||||
|
- Added functionality to cancel follow request
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed link to external profile not working on user profiles
|
||||||
|
- Fixed mobile shoutbox display
|
||||||
|
- Fixed favicon badge not working in Chrome
|
||||||
|
- Escape html more properly in subject/display name
|
||||||
|
|
||||||
|
|
||||||
|
## [2.4.0] - 2021-08-08
|
||||||
|
### Added
|
||||||
|
- Added a quick settings to timeline header for easier access
|
||||||
|
- Added option to mark posts as sensitive by default
|
||||||
|
- Added quick filters for notifications
|
||||||
|
- Implemented user option to change sidebar position to the right side
|
||||||
|
- Implemented user option to hide floating shout panel
|
||||||
|
- Implemented "edit profile" button if viewing own profile which opens profile settings
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed follow request count showing in the wrong location in mobile view
|
||||||
|
|
||||||
|
|
||||||
|
## [2.3.0] - 2021-03-01
|
||||||
|
### Fixed
|
||||||
|
- Button to remove uploaded media in post status form is now properly placed and sized.
|
||||||
|
- Fixed shoutbox not working in mobile layout
|
||||||
|
- Fixed missing highlighted border in expanded conversations again
|
||||||
|
- Fixed some UI jumpiness when opening images particularly in chat view
|
||||||
|
- Fixed chat unread badge looking weird
|
||||||
|
- Fixed punycode names not working properly
|
||||||
|
- Fixed notifications crashing on an invalid notification
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Display 'people voted' instead of 'votes' for multi-choice polls
|
||||||
|
- Changed the "Timelines" link in side panel to toggle show all timeline options inside the panel
|
||||||
|
- Renamed "Timeline" to "Home Timeline" to be more clear
|
||||||
|
- Optimized chat to not get horrible performance after keeping the same chat open for a long time
|
||||||
|
- When opening emoji picker or react picker, it automatically focuses the search field
|
||||||
|
- Language picker now uses native language names
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Added reason field for registration when approval is required
|
||||||
|
- Group staff members by role in the About page
|
||||||
|
|
||||||
|
|
||||||
|
## [2.2.3] - 2021-01-18
|
||||||
|
### Added
|
||||||
|
- Added Report button to status ellipsis menu for easier reporting
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Follows/Followers tabs on user profiles now display the content properly.
|
||||||
|
- Handle punycode in screen names
|
||||||
|
- Fixed local dev mode having non-functional websockets in some cases
|
||||||
|
- Show notices for websocket events (errors, abnormal closures, reconnections)
|
||||||
|
- Fix not being able to re-enable websocket until page refresh
|
||||||
|
- Fix annoying issue where timeline might have few posts when streaming is enabled
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Don't filter own posts when they hit your wordfilter
|
||||||
|
|
||||||
|
|
||||||
|
## [2.2.2] - 2020-12-22
|
||||||
|
### Added
|
||||||
|
- Mouseover titles for emojis in reaction picker
|
||||||
|
- Support to input emoji into the search box in reaction picker
|
||||||
|
- Added some missing unicode emoji
|
||||||
|
- Added the upload limit to the Features panel in the About page
|
||||||
|
- Support for solid color wallpaper, instance doesn't have to define a wallpaper anymore
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed the occasional bug where screen would scroll 1px when typing into a reply form
|
||||||
|
- Fixed timeline errors locking timelines
|
||||||
|
- Fixed missing highlighted border in expanded conversations
|
||||||
|
- Fixed custom emoji not working in profile field names
|
||||||
|
- Fixed pinned statuses not appearing in user profiles
|
||||||
|
- Fixed some elements not being keyboard navigation friendly
|
||||||
|
- Fixed error handling when updating various profile images
|
||||||
|
- Fixed your latest chat messages disappearing when closing chat view and opening it again during the same session
|
||||||
|
- Fixed custom emoji not showing in poll options before voting
|
||||||
|
- Fixed link color not applied to instance name in topbar
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Errors when fetching are now shown with popup errors instead of "Error fetching updates" in panel headers
|
||||||
|
- Made reply/fav/repeat etc buttons easier to hit
|
||||||
|
- Adjusted timeline menu clickable area to match the visible button
|
||||||
|
- Moved external source link from status heading to the ellipsis menu
|
||||||
|
- Disabled horizontal textarea resize
|
||||||
|
- Wallpaper is now top-aligned, horizontally centered.
|
||||||
|
|
||||||
|
|
||||||
|
## [2.2.1] - 2020-11-11
|
||||||
|
### Fixed
|
||||||
|
- Fixed regression in react popup alignment and overflowing
|
||||||
|
|
||||||
|
|
||||||
|
## [2.2.0] - 2020-11-06
|
||||||
|
### Added
|
||||||
|
- New option to optimize timeline rendering to make the site more responsive (enabled by default)
|
||||||
|
- New instance option `logoLeft` to move logo to the left side in desktop nav bar
|
||||||
|
- Import/export a muted users
|
||||||
|
- Proper handling of deletes when using websocket streaming
|
||||||
|
- Added optimistic chat message sending, so you can start writing next message before the previous one has been sent
|
||||||
|
- Added a small red badge to the favicon when there's unread notifications
|
||||||
|
- Added the NSFW alert to link previews
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed clicking NSFW hider through status popover
|
||||||
|
- Fixed chat-view back button being hard to click
|
||||||
|
- Fixed fresh chat notifications being cleared immediately while leaving the chat view and not having time to actually see the messages
|
||||||
|
- Fixed multiple regressions in CSS styles
|
||||||
|
- Fixed multiple issues with input fields when using CJK font as default
|
||||||
|
- Fixed search field in navbar infringing into logo in some cases
|
||||||
|
- Fixed not being able to load the chat history in vertical screens when the message list doesn't take the full height of the scrollable container on the first fetch.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Clicking immediately when timeline shifts is now blocked to prevent misclicks
|
||||||
|
- Icons changed from fontello (FontAwesome 4 + others) to FontAwesome 5 due to problems with fontello.
|
||||||
|
- Some icons changed for better accessibility (lock, globe)
|
||||||
|
- Logo is now clickable
|
||||||
|
- Changed default logo to SVG version
|
||||||
|
|
||||||
|
|
||||||
|
## [2.1.2] - 2020-09-17
|
||||||
|
### Fixed
|
||||||
|
- Fixed chats list not updating its order when new messages come in
|
||||||
|
- Fixed chat messages sometimes getting lost when you receive a message at the same time
|
||||||
|
|
||||||
|
|
||||||
|
## [2.1.1] - 2020-09-08
|
||||||
### Changed
|
### Changed
|
||||||
- Polls will be hidden with status content if "Collapse posts with subjects" is enabled and the post is collapsed.
|
- Polls will be hidden with status content if "Collapse posts with subjects" is enabled and the post is collapsed.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
- Network fetches don't pile up anymore but wait for previous ones to finish to reduce throttling.
|
||||||
- Autocomplete won't stop at the second @, so it'll still work with "@lain@l" and not start over.
|
- Autocomplete won't stop at the second @, so it'll still work with "@lain@l" and not start over.
|
||||||
- Fixed weird autocomplete behavior when you write ":custom_emoji: ?"
|
- Fixed weird autocomplete behavior when you write ":custom_emoji: ?"
|
||||||
|
|
||||||
|
|
||||||
## [2.1.0] - 2020-08-28
|
## [2.1.0] - 2020-08-28
|
||||||
### Added
|
### Added
|
||||||
- Autocomplete domains from list of known instances
|
- Autocomplete domains from list of known instances
|
||||||
|
@ -128,8 +297,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Ability to change user's email
|
- Ability to change user's email
|
||||||
- About page
|
- About page
|
||||||
- Added remote user redirect
|
- Added remote user redirect
|
||||||
- Bookmarks
|
|
||||||
### Changed
|
### Changed
|
||||||
- changed the way fading effects for user profile/long statuses works, now uses css-mask instead of gradient background hacks which weren't exactly compatible with semi-transparent themes
|
- changed the way fading effects for user profile/long statuses works, now uses css-mask instead of gradient background hacks which weren't exactly compatible with semi-transparent themes
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- improved hotkey behavior on autocomplete popup
|
- improved hotkey behavior on autocomplete popup
|
||||||
|
|
|
@ -3,6 +3,7 @@ Contributors of this project.
|
||||||
- Constance Variable (lambadalambda@social.heldscal.la): Code
|
- Constance Variable (lambadalambda@social.heldscal.la): Code
|
||||||
- Coco Snuss (cocosnuss@social.heldscal.la): Code
|
- Coco Snuss (cocosnuss@social.heldscal.la): Code
|
||||||
- wakarimasen (wakarimasen@shitposter.club): NSFW hiding image
|
- wakarimasen (wakarimasen@shitposter.club): NSFW hiding image
|
||||||
|
- eris (eris@disqordia.space): Code
|
||||||
- dtluna (dtluna@social.heldscal.la): Code
|
- dtluna (dtluna@social.heldscal.la): Code
|
||||||
- sonyam (sonyam@social.heldscal.la): Background images
|
- sonyam (sonyam@social.heldscal.la): Background images
|
||||||
- hakui (hakui@freezepeach.xyz): CSS and styling
|
- hakui (hakui@freezepeach.xyz): CSS and styling
|
||||||
|
|
|
@ -21,6 +21,7 @@ var compiler = webpack(webpackConfig)
|
||||||
|
|
||||||
var devMiddleware = require('webpack-dev-middleware')(compiler, {
|
var devMiddleware = require('webpack-dev-middleware')(compiler, {
|
||||||
publicPath: webpackConfig.output.publicPath,
|
publicPath: webpackConfig.output.publicPath,
|
||||||
|
writeToDisk: true,
|
||||||
stats: {
|
stats: {
|
||||||
colors: true,
|
colors: true,
|
||||||
chunks: false
|
chunks: false
|
||||||
|
|
|
@ -3,7 +3,8 @@ var config = require('../config')
|
||||||
var utils = require('./utils')
|
var utils = require('./utils')
|
||||||
var projectRoot = path.resolve(__dirname, '../')
|
var projectRoot = path.resolve(__dirname, '../')
|
||||||
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin')
|
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin')
|
||||||
var FontelloPlugin = require("fontello-webpack-plugin")
|
var CopyPlugin = require('copy-webpack-plugin');
|
||||||
|
var { VueLoaderPlugin } = require('vue-loader')
|
||||||
|
|
||||||
var env = process.env.NODE_ENV
|
var env = process.env.NODE_ENV
|
||||||
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
|
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
|
||||||
|
@ -29,16 +30,16 @@ module.exports = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.js', '.vue'],
|
extensions: ['.js', '.jsx', '.vue'],
|
||||||
modules: [
|
modules: [
|
||||||
path.join(__dirname, '../node_modules')
|
path.join(__dirname, '../node_modules')
|
||||||
],
|
],
|
||||||
alias: {
|
alias: {
|
||||||
'vue$': 'vue/dist/vue.runtime.common',
|
|
||||||
'static': path.resolve(__dirname, '../static'),
|
'static': path.resolve(__dirname, '../static'),
|
||||||
'src': path.resolve(__dirname, '../src'),
|
'src': path.resolve(__dirname, '../src'),
|
||||||
'assets': path.resolve(__dirname, '../src/assets'),
|
'assets': path.resolve(__dirname, '../src/assets'),
|
||||||
'components': path.resolve(__dirname, '../src/components')
|
'components': path.resolve(__dirname, '../src/components'),
|
||||||
|
'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
|
@ -58,9 +59,28 @@ module.exports = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
enforce: 'post',
|
||||||
|
test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files
|
||||||
|
type: 'javascript/auto',
|
||||||
|
loader: '@intlify/vue-i18n-loader',
|
||||||
|
include: [ // Use `Rule.include` to specify the files of locale messages to be pre-compiled
|
||||||
|
path.resolve(__dirname, '../src/i18n')
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.vue$/,
|
test: /\.vue$/,
|
||||||
use: 'vue-loader'
|
loader: 'vue-loader',
|
||||||
|
options: {
|
||||||
|
compilerOptions: {
|
||||||
|
isCustomElement(tag) {
|
||||||
|
if (tag === 'pinch-zoom') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.jsx?$/,
|
test: /\.jsx?$/,
|
||||||
|
@ -95,13 +115,19 @@ module.exports = {
|
||||||
entry: path.join(__dirname, '..', 'src/sw.js'),
|
entry: path.join(__dirname, '..', 'src/sw.js'),
|
||||||
filename: 'sw-pleroma.js'
|
filename: 'sw-pleroma.js'
|
||||||
}),
|
}),
|
||||||
new FontelloPlugin({
|
new VueLoaderPlugin(),
|
||||||
config: require('../static/fontello.json'),
|
// This copies Ruffle's WASM to a directory so that JS side can access it
|
||||||
name: 'fontello',
|
new CopyPlugin({
|
||||||
output: {
|
patterns: [
|
||||||
css: 'static/[name].' + now + '.css', // [hash] is not supported. Use the current timestamp instead for versioning.
|
{
|
||||||
font: 'static/font/[name].' + now + '.[ext]'
|
from: "node_modules/ruffle-mirror/*",
|
||||||
}
|
to: "static/ruffle",
|
||||||
|
flatten: true
|
||||||
|
},
|
||||||
|
],
|
||||||
|
options: {
|
||||||
|
concurrency: 100,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,9 @@ module.exports = merge(baseWebpackConfig, {
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env': config.dev.env,
|
'process.env': config.dev.env,
|
||||||
'COMMIT_HASH': JSON.stringify('DEV'),
|
'COMMIT_HASH': JSON.stringify('DEV'),
|
||||||
'DEV_OVERRIDES': JSON.stringify(config.dev.settings)
|
'DEV_OVERRIDES': JSON.stringify(config.dev.settings),
|
||||||
|
'__VUE_OPTIONS_API__': true,
|
||||||
|
'__VUE_PROD_DEVTOOLS__': false
|
||||||
}),
|
}),
|
||||||
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
||||||
new webpack.HotModuleReplacementPlugin(),
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
|
|
|
@ -36,7 +36,9 @@ var webpackConfig = merge(baseWebpackConfig, {
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env': env,
|
'process.env': env,
|
||||||
'COMMIT_HASH': JSON.stringify(commitHash),
|
'COMMIT_HASH': JSON.stringify(commitHash),
|
||||||
'DEV_OVERRIDES': JSON.stringify(undefined)
|
'DEV_OVERRIDES': JSON.stringify(undefined),
|
||||||
|
'__VUE_OPTIONS_API__': true,
|
||||||
|
'__VUE_PROD_DEVTOOLS__': false
|
||||||
}),
|
}),
|
||||||
// extract css into its own file
|
// extract css into its own file
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
|
|
|
@ -3,6 +3,11 @@ const path = require('path')
|
||||||
let settings = {}
|
let settings = {}
|
||||||
try {
|
try {
|
||||||
settings = require('./local.json')
|
settings = require('./local.json')
|
||||||
|
if (settings.target && settings.target.endsWith('/')) {
|
||||||
|
// replacing trailing slash since it can conflict with some apis
|
||||||
|
// and that's how actual BE reports its url
|
||||||
|
settings.target = settings.target.replace(/\/$/, '')
|
||||||
|
}
|
||||||
console.log('Using local dev server settings (/config/local.json):')
|
console.log('Using local dev server settings (/config/local.json):')
|
||||||
console.log(JSON.stringify(settings, null, 2))
|
console.log(JSON.stringify(settings, null, 2))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -47,7 +52,10 @@ module.exports = {
|
||||||
target,
|
target,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
cookieDomainRewrite: 'localhost',
|
cookieDomainRewrite: 'localhost',
|
||||||
ws: true
|
ws: true,
|
||||||
|
headers: {
|
||||||
|
'Origin': target
|
||||||
|
}
|
||||||
},
|
},
|
||||||
'/oauth/revoke': {
|
'/oauth/revoke': {
|
||||||
target,
|
target,
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
# Pleroma-FE configuration and customization for instance administrators
|
# Pleroma-FE configuration and customization for instance administrators
|
||||||
|
|
||||||
* *For user configuration, see [Pleroma-FE user guide](USER_GUIDE.md)*
|
* *For user configuration, see [Pleroma-FE user guide](../user_guide)*
|
||||||
* *For local development server configuration, see [Hacking, tweaking, contributing](HACKING.md)*
|
* *For local development server configuration, see [Hacking, tweaking, contributing](HACKING.md)*
|
||||||
|
|
||||||
## Where configuration is stored
|
## Where configuration is stored
|
||||||
|
|
||||||
PleromaFE gets its configuration from several sources, in order of preference (the one above overrides ones below it)
|
PleromaFE gets its configuration from several sources, in order of preference (the one above overrides ones below it)
|
||||||
|
|
||||||
1. `/api/statusnet/config.json` - this is generated on Backend and contains multiple things including instance name, char limit etc. It also contains FE/Client-specific data, PleromaFE uses `pleromafe` field of it. For more info on changing config on BE, look [here](https://docs-develop.pleroma.social/config.html#frontend_configurations)
|
1. `/api/statusnet/config.json` - this is generated on Backend and contains multiple things including instance name, char limit etc. It also contains FE/Client-specific data, PleromaFE uses `pleromafe` field of it. For more info on changing config on BE, look [here](../backend/configuration/cheatsheet.md#frontend_configurations)
|
||||||
2. `/static/config.json` - this is a static FE-provided file, containing only FE specific configuration. This file is completely optional and could be removed but is useful as a fallback if some configuration JSON property isn't present in BE-provided config. It's also a reference point to check what default configuration are and what JSON properties even exist. In local dev mode it could be used to override BE configuration, more about that in HACKING.md. File is located [here](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/static/config.json).
|
2. `/static/config.json` - this is a static FE-provided file, containing only FE specific configuration. This file is completely optional and could be removed but is useful as a fallback if some configuration JSON property isn't present in BE-provided config. It's also a reference point to check what default configuration are and what JSON properties even exist. In local dev mode it could be used to override BE configuration, more about that in HACKING.md. File is located [here](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/static/config.json).
|
||||||
3. Built-in defaults. Those are hard-coded defaults that are used when `/static/config.json` is not available and BE-provided configuration JSON is missing some JSON properties. ( [Code](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/modules/instance.js) )
|
3. Built-in defaults. Those are hard-coded defaults that are used when `/static/config.json` is not available and BE-provided configuration JSON is missing some JSON properties. ( [Code](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/modules/instance.js) )
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ This could be a bit trickier, you basically need steps 1-4 from *develop build*
|
||||||
|
|
||||||
### Replacing your instance's frontend with custom FE build
|
### Replacing your instance's frontend with custom FE build
|
||||||
|
|
||||||
This is the most easiest way to use and test FE build: you just need to copy or symlink contents of `dist` folder into backend's [static directory](https://docs.pleroma.social/static_dir.html), by default it is located in `instance/static`, or in `/var/lib/pleroma/static` for OTP release installations, create it if it doesn't exist already. Be aware that running `yarn build` wipes the contents of `dist` folder.
|
This is the most easiest way to use and test FE build: you just need to copy or symlink contents of `dist` folder into backend's [static directory](../backend/configuration/static_dir.md), by default it is located in `instance/static`, or in `/var/lib/pleroma/static` for OTP release installations, create it if it doesn't exist already. Be aware that running `yarn build` wipes the contents of `dist` folder.
|
||||||
|
|
||||||
### Running production build locally or on a separate server
|
### Running production build locally or on a separate server
|
||||||
|
|
||||||
|
@ -67,9 +67,9 @@ server {
|
||||||
|
|
||||||
### API, Data, Operations
|
### API, Data, Operations
|
||||||
|
|
||||||
In 99% cases PleromaFE uses [MastoAPI](https://docs.joinmastodon.org/api/) with [Pleroma Extensions](https://docs-develop.pleroma.social/differences_in_mastoapi_responses.html) to fetch the data. The rest is either QvitterAPI leftovers or pleroma-exclusive APIs. QvitterAPI doesn't exactly have documentation and uses different JSON structure and sometimes different parameters and workflows, [this](https://twitter-api.readthedocs.io/en/latest/index.html) could be a good reference though. Some pleroma-exclusive API may still be using QvitterAPI JSON structure.
|
In 99% cases PleromaFE uses [MastoAPI](https://docs.joinmastodon.org/api/) with [Pleroma Extensions](../backend/API/differences_in_mastoapi_responses.md) to fetch the data. The rest is either QvitterAPI leftovers or pleroma-exclusive APIs. QvitterAPI doesn't exactly have documentation and uses different JSON structure and sometimes different parameters and workflows, [this](https://twitter-api.readthedocs.io/en/latest/index.html) could be a good reference though. Some pleroma-exclusive API may still be using QvitterAPI JSON structure.
|
||||||
|
|
||||||
PleromaFE supports both formats by transforming them into internal format which is basically QvitterAPI one with some additions and renaming. All data is passed trough [Entity Normalizer](/src/services/entity_normalizer/entity_normalizer.service.js) which can serve as a reference of API and what's actually used, it's also a host for all the hacks and data transformation.
|
PleromaFE supports both formats by transforming them into internal format which is basically QvitterAPI one with some additions and renaming. All data is passed trough [Entity Normalizer](https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/src/services/entity_normalizer/entity_normalizer.service.js) which can serve as a reference of API and what's actually used, it's also a host for all the hacks and data transformation.
|
||||||
|
|
||||||
For most part, PleromaFE tries to store all the info it can get in global vuex store - every user and post are passed trough updating mechanism where data is either added or merged with existing data, reactively updating the information throughout UI, so if in newest request user's post counter increased, it will be instantly updated in open user profile cards. This is also used to find users, posts and sometimes to build timelines and/or request parameters.
|
For most part, PleromaFE tries to store all the info it can get in global vuex store - every user and post are passed trough updating mechanism where data is either added or merged with existing data, reactively updating the information throughout UI, so if in newest request user's post counter increased, it will be instantly updated in open user profile cards. This is also used to find users, posts and sometimes to build timelines and/or request parameters.
|
||||||
|
|
||||||
|
|
|
@ -1,207 +0,0 @@
|
||||||
# Pleroma-FE user guide
|
|
||||||
|
|
||||||
> Be prepared for breaking changes, unexpected behavior and this user guide becoming obsolete and wrong.
|
|
||||||
|
|
||||||
> If there was no insanity
|
|
||||||
>
|
|
||||||
> it would be necessary to create it.
|
|
||||||
>
|
|
||||||
> --Catbag
|
|
||||||
|
|
||||||
## Posting, reading, basic functions.
|
|
||||||
|
|
||||||
After registering and logging in you're presented with your timeline in right column and new post form with timeline list and notifications in the left column.
|
|
||||||
|
|
||||||
Posts will contain the text you are posting, but some content will be modified:
|
|
||||||
|
|
||||||
1. Mentions: Mentions have the form of @user or @user<span></span>@instance.tld. These will become links to the user's profile. In addition, the mentioned user will always get a notification about the post they have been mentioned in, so only mention users that you want to receive this message.
|
|
||||||
2. URLs: URLs like `http://example.com` will be automatically be turned into a clickable links.
|
|
||||||
3. Hashtags: Hashtags like #cofe will also be turned into links.
|
|
||||||
|
|
||||||
**Depending on your instance some of the options might not be available or have different defaults**
|
|
||||||
|
|
||||||
Let's clear up some basic stuff. When you post something it's called a **post** or it could be called a **status** or even a **toot** or a **prööt** depending on whom you ask. Post has body/content but it also has some other stuff in it - from attachments, visibility scope, subject line.
|
|
||||||
* **Emoji** are small images embedded in text, there are two major types of emoji: [unicode emoji](https://en.wikipedia.org/wiki/Emoji) and custom emoji. While unicode emoji are universal and standardized, they can appear differently depending on where you are using them or may not appear at all on older systems. Custom emoji are more *fun* kind - instance administrator can define many images as *custom emoji* for their users. This works very simple - custom emoji is defined by its *shortcode* and an image, so that any shortcode enclosed in colons get replaced with image if such shortcode exist.
|
|
||||||
Let's say there's `:pleroma:` emoji defined on instance. That means
|
|
||||||
> First time using :pleroma: pleroma!
|
|
||||||
|
|
||||||
will become
|
|
||||||
> First time using ![pleroma](./example_emoji.png) pleroma!
|
|
||||||
|
|
||||||
Note that you can only use emoji defined on your instance, you cannot "copy" someone else's emoji, and will have to ask your administrator to copy emoji from other instance to yours.
|
|
||||||
Lastly, there's two convenience options for emoji: an emoji picker (smiley face to the right of "submit" button) and autocomplete suggestions - when you start typing :shortcode: it will automatically try to suggest you emoj and complete the shortcode for you if you select one. **Note** that if emoji doesn't show up in suggestions nor in emoji picker it means there's no such emoji on your instance, if shortcode doesn't match any defined emoji it will appear as text.
|
|
||||||
* **Attachments** are fairly simple - you can attach any file to a post as long as the file is within maximum size limits. If you're uploading explicit material you can mark all of your attachments as sensitive (or add `#nsfw` tag) - it will hide the images and videos behind a warning so that it won't be displayed instantly.
|
|
||||||
* **Subject line** also known as **CW** (Content Warning) could be used as a header to the post and/or to warn others about contents of the post having something that might upset somebody or something among those lines. Several applications allow to hide post content leaving only subject line visible. Using a subject line will not mark your images as sensitive, you will have to do that explicitly (see above).
|
|
||||||
* **Visiblity scope** controls who will be able to see your posts. There are four scopes available:
|
|
||||||
|
|
||||||
1. `Public`: This is the default, and some fediverse software like GNU Social only supports this. This means that your post is accessible by anyone and will be shown in the public timelines.
|
|
||||||
2. `Unlisted`: This is the same as public, but your post won't appear in the public timelines. The post will still be accessible by anyone who comes across it (for example, by looking at your profile) or by direct linking. They will also appear in public searches.
|
|
||||||
3. `Followers only`: This will show your post only to your followers. Only they will be able to interact with it. Be careful: When somebody follows you, they will be able to see all your previous `followers only` posts as well! If you want to restrict who can follow you, consider [locking your account down to only approved followers](#profle).
|
|
||||||
4. `Direct`: This will only send the message to the people explicitly mentioned in the post.
|
|
||||||
|
|
||||||
A few things to consider about the security and usage of these scopes:
|
|
||||||
|
|
||||||
- None of these options will change the fact that the messages are all saved in the database unencrypted. They will be visible to your server admin and to any other admin of a server who receives this post. Do not share information that you would consider secret or dangerous. Use encrypted messaging systems for these things.
|
|
||||||
- Follower-only posts can lead to fragmented conversations. If you post a follower-only post and somebody else replies to it with a follower-only post, only people following both of you will see the whole conversation thread. Everybody else will only see half of it. Keep this in mind and keep conversations public if possible.
|
|
||||||
- Changing scopes during a thread or adding people to a direct message will not retroactively make them see the whole conversation. If you add someone to a direct message conversation, they will not see the post that happened before they were mentioned.
|
|
||||||
* **Reply-to** if you are replying to someone, your post will also contain a note that your post is referring to the post you're replying to. Person you're replying to will receive a notification *even* if you remove them from mentioned people. You won't receive notifications when replying to your own posts, but it's useful to reply to your own posts to provide people some context if it's a follow-up to a previous post. There's a small "Reply to ..." label under post author's name which you can hover on to see what post it's referring to.
|
|
||||||
|
|
||||||
Sometimes you may encounter posts that seem different than what they are supposed to. For example, you might see a direct message without any mentions in the text. This can happen because internally, the Fediverse has a different addressing mechanism similar to email, with `to` and `cc` fields. While these are not directly accessible in PleromaFE, other software in the Fediverse might generate those posts. Do not worry in these cases, these are normal and not a bug.
|
|
||||||
|
|
||||||
#### Rich text
|
|
||||||
|
|
||||||
By default new posts you make are plaintext, meaning you can't make text **bold** or add custom links or make lists or anything like that. However if your instance allows it you can use Markdown or BBCode or HTML to spice up your text, however there are certain limitations to what HTML tags and what features of Markdown you can use.
|
|
||||||
|
|
||||||
this section will be expanded later
|
|
||||||
|
|
||||||
### Other actions
|
|
||||||
|
|
||||||
In addition to posting you can also *favorite* post also known as *liking* them and *repeat* posts (also known as *retweeting*, *boosting* and even *reprööting*). Favoriting a post increments a counter on it, notifies post author of your affection towards that post and also adds that post to your "favorited" posts list (in your own profile, "Favorites" tab). Reprööting a post does all that and also repeats this post to your followers and your profile page with a note "*user* repeated post".
|
|
||||||
|
|
||||||
Your own posts can be deleted, but this will only reliably delete the post from your own instance. Other instances will receive a deletion notice, but there's no way to force them to actually delete a post. In addition, not all instances that contain the message might even receive the deletion notice, because they might be offline or not known to have the post because they received it through a repeat. Lastly, deletion notice might not reach certain frontends and clients - post will be visible for them until page refresh or cache clear, they probably won't be able to interact with it apart from replying to it (which will have reply-to mark missing).
|
|
||||||
|
|
||||||
If you are a moderator, you can also delete posts by other people. If those people are on your instance, it will delete the post and send out the deletion notice to other servers. If they are not on your instance, it will just remove the post from your local instance.
|
|
||||||
|
|
||||||
There's also an option to report a user via a post (if the feature is available on your instance) which could be used to notify your (and probably other instance's) admin that someone is being naughty.
|
|
||||||
|
|
||||||
## Users
|
|
||||||
|
|
||||||
When you see someone, you can click on their user picture to view their profile, and click on the userpic in that to see *full* profile. You can *follow* them, *mute* and *block* them. Following is self-explanatory, it adds them t your Home Timeline, lists you as a follower and gives you access to follower-only posts if they have any. Muting makes posts and notifications made by them very tiny, giving you an option to see the post if you're curious. However on clients other than PleromaFE their posts will be completely removed. *Blocking* a user removes them from your timeline and notifications and prevents them from following you (automatically unfollows them from you).
|
|
||||||
|
|
||||||
Please note that some users can be "locked", meaning instead of following them you send a follow request they need to approve for you to become their follower.
|
|
||||||
|
|
||||||
## Timelines
|
|
||||||
|
|
||||||
Currently you have several timelines to browse trough:
|
|
||||||
* **Timeline** aka Home Timeline - this timeline contains all posts by people you follow and your own posts, as well as posts mentioning you directly.
|
|
||||||
* **Interactions** all interactions you've had with people on the network, basically same as notifications except grouped in convenient way - mentions separate from favorites with repeats separate from follows
|
|
||||||
* **Direct Messages** all posts with `direct` scope addressed to you or mentioning you.
|
|
||||||
* **Public Timelines** all posts made by users on instance you're on
|
|
||||||
* **The Whole Known Network** also known as **TWKN** or **Federated Timeline** - all posts on the network by everyone, almost. Due to nature of the network your instance may not know *all** the instances on the network, so only posts originating from known instances are shown there.
|
|
||||||
|
|
||||||
## Your profile
|
|
||||||
|
|
||||||
By clicking wrench icon above the post form you can access the profile edit or "user settings" screen.
|
|
||||||
|
|
||||||
### Profle
|
|
||||||
|
|
||||||
Here you can set up how you appear to other users among with some other settings:
|
|
||||||
|
|
||||||
- Name: this is text that displays next to your avatar in posts. Please note that you **cannot** change your *@<span></span>handle*
|
|
||||||
- Bio: this will be displayed under your profile - you can put anything you want there you want for everyone to see.
|
|
||||||
- Restrict your account to approved followers only: this makes your account "locked", when people follow you - you have to approve or deny their follow requests, this gives more control over who sees your followers only posts.
|
|
||||||
- Default visibility scope: this chooses your default post scope for new posts
|
|
||||||
- Strip rich text from all posts: this strips rich text formatting (bold/italics/lists etc) from all incoming posts. Will only affect newly fetched posts.
|
|
||||||
|
|
||||||
If you're admin or moderator on your instance you also get "Show [role] badge in my profile" - this controls whether to show "Admin" or "Moderator** label on your profile page.
|
|
||||||
|
|
||||||
**For all options mentioned above you have to click "Submit" button for changes to take place**
|
|
||||||
|
|
||||||
- Avatar: this changes picture next to your posts. Your avatar shouldn't exceed 2 MiB (2097152 bytes) or it could cause problems with certain instances.
|
|
||||||
- Banner: this changes background on your profile card. Same as avatar it shouldn't exceed 2 MiB limit.
|
|
||||||
- Profile Background: this changes background picture for UI. It isn't shown to anyone else **yet**, but some time later it will be shown when viewing your profile.
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
Here you can change your password, revoke access tokens, configure 2-factor authentication (if available).
|
|
||||||
|
|
||||||
### Notifications
|
|
||||||
|
|
||||||
This screen allows more fine-grained control over what notifications to show to you based on whom it comes from
|
|
||||||
|
|
||||||
### Data Import/Export
|
|
||||||
|
|
||||||
This allows you to export and import a list of people you follow, in case instance's database gets reverted or if you want to move to another server. Note that you **CANNOT export/import list of people who *follow you***, they'll just need to follow you back after you move.
|
|
||||||
|
|
||||||
### Blocks and Mutes
|
|
||||||
|
|
||||||
These screens give access to full list of people you block/mute, useful for *un*blocking/*un*muting people because blocking/muting them most likely removes them out of your sight completely.
|
|
||||||
|
|
||||||
## Other stuff
|
|
||||||
|
|
||||||
By default you can see **ALL** posts made by other users on your Home Timeline, this contrast behavior of Twitter and Mastodon, which shows you only non-reply posts and replies to people you follow. You can set it up to replicate the said behavior, however the option is currently broken.
|
|
||||||
|
|
||||||
You can view other people's profiles and search for users (top-right corner, person with a plus icon). Tag search is possible but not implemented properly yet, right now you can click on tag link in a post to see posts tagged with that post.
|
|
||||||
|
|
||||||
You can also view posts you've favorited on your own profile, but you cannot see favorites by other people.
|
|
||||||
|
|
||||||
Due to nature of how Pleroma (backend) operates you might see old posts appear as if they are new, this is because instance just learned about that post (i.e. your instance is younger that some other ones) and someone interacted with old post. Posts are sorted by date of when they are received, not date they have been posted because it's very easy to spoof the date, so a post claiming it "was" made in year 2077 could hand at top of your TL forever.
|
|
||||||
|
|
||||||
# Customization and configuration
|
|
||||||
|
|
||||||
Clicking on the cog icon in the upper right will go to the settings screen.
|
|
||||||
|
|
||||||
## General
|
|
||||||
|
|
||||||
### Interface
|
|
||||||
|
|
||||||
- Language: Here you can set the interface language. The default language is the one that you set in your browser settings.
|
|
||||||
- Hide instance-specific panel: This hides the panel in the lower left that usually contains general information about the server.
|
|
||||||
|
|
||||||
### Timeline
|
|
||||||
|
|
||||||
- Hide posts of muted users: If this is set, 'muting' a user will completely hide their posts instead of collapsing them.
|
|
||||||
- Collapse posts with subjects: This will collapse posts that contain a subject, hiding their content. Subjects are also sometimes called content warnings.
|
|
||||||
- Enable automatic streaming of new posts when scrolled to the top: With this enabled, new posts will automatically stream in when you are scrolled to the top. Otherwise, you will see a button on the timeline that will let you display the new posts.
|
|
||||||
- Pause streaming when tab is not focused: This pauses the automatic streaming that the previous option enables when the tab is out of focus. This is useful if you don't want to miss any new posts.
|
|
||||||
- Enable automatic loading when scrolled to the bottom: When this is disabled, a button will be shown on the bottom of the timeline that will let you load older posts.
|
|
||||||
- Enable reply-link preview on hover: Status posts in the timeline and notifications contain links to replies and to the post they are a reply to. If this setting is enabled, hovering over that link will display that linked post in a small hovering overlay.
|
|
||||||
|
|
||||||
### Composing
|
|
||||||
|
|
||||||
- Copy scope when replying: When this is activated, the scope of a reply will be the same as the scope of the post it is replying to. This is useful to prevent accidentally moving private discussions to public, or vice versa.
|
|
||||||
- Always show subject field: Whether or not to display the 'subject' input field in the post form. If you do not want to use subjects, you can deactivate this.
|
|
||||||
- Copy subject when replying: This controls if the subject of a post will be copied from the post it is replying to.
|
|
||||||
- Post status content type: Selects the default content type of your post. The options are: Plain text, HTML, BBCode and Markdown.
|
|
||||||
- Minimize scope selection options: Selecting this will reduce the visibility scopes to 'direct', your default post scope and post scope of post you're replying to.
|
|
||||||
- Automatically hide New Post button: Mobile interface only: hide floating "New post" button when scrolling
|
|
||||||
|
|
||||||
### Attachments
|
|
||||||
|
|
||||||
- Hide attachments in timeline: Do not display attachments in timelines. They will still display in expanded conversations. This is useful to save bandwidth and for browsing in public.
|
|
||||||
- Hide attachments in conversations: Also hide attachments in expanded conversations.
|
|
||||||
- Maximum amount of thumbnails per post: Exactly that :)
|
|
||||||
- Enable clickthrough NSFW attachment hiding: Hide attachments that are marked as NSFW/sensitive behind a click-through image.`
|
|
||||||
- Preload images: This will preload the hidden images so that they display faster when clicking through.
|
|
||||||
- Open NSFW attachments with just one click: Directly open NSFW attachments in a maximised state instead of revealing the image thumbnail.
|
|
||||||
- Play-on-hover GIFs: With this activated, GIFs images and avatars will only be animated on mouse hover. Otherwise, they will be always animated. This is very useful if your timeline looks too flashy from people's animated avatars and eases the CPU load.
|
|
||||||
- Loop videos: Whether to loop videos indefinitely.
|
|
||||||
- Loop only videos without sound: Some instances will use videos without sounds instead of GIFs. This will make only those videos autoplay.
|
|
||||||
- Play videos directly in the media viewer: Play videos right in the timeline instead of opening it in a modal
|
|
||||||
- Don't crop the attachment in thumbnails: if enabled, images in attachments will be fit entirely inside the container instead of being zoomed in and cropped.
|
|
||||||
|
|
||||||
### Notifications
|
|
||||||
|
|
||||||
- Enable web push notifications: this enables Web Push notifications, to allow receiving notifications even when the page isn't opened, doesn't affect regular notifications.
|
|
||||||
|
|
||||||
## Theme
|
|
||||||
|
|
||||||
You can change the look and feel of Pleroma Frontend here. You can choose from several instance-provided presets and you can load one from file and save current theme to file. Before you apply new theme you can see what it will look like approximately in preview section.
|
|
||||||
|
|
||||||
Themes engine was made to be easy to use while giving an option for powerful in-depth customization - you can just tweak colors on "Common" tab and leave everything else as is.
|
|
||||||
|
|
||||||
If there's a little check box next to a color picker it means that color is optional and unless checked will be automatically picked based on some other color or defaults.
|
|
||||||
|
|
||||||
For some features you can also adjust transparency of it by changing its opacity, you just need to tick checkbox next to it, otherwise it will be using default opacity.
|
|
||||||
|
|
||||||
Contrast information is also provided - you can see how readable text is based on contrast between text color and background, icons under color pickers represent contrast rating based on [WCAG](https://www.w3.org/TR/2008/REC-WCAG20-20081211/#visual-audio-contrast-contrast) - thumbs up means AAA rating (good), half-filled circle means AA rating (acceptable) and warning icon means it doesn't pass the minimal contrast requirement and probably will be less readable, especially for vision-challenged people, you can hover over icon to see more detailed information. *Please note* that if background is not opaque (opacity != 1) contrast will be measured based on "worst case scenario", i.e. behind semi-transparent background lies some solid color that makes text harder to read, this however is still inaccurate because it doesn't account that background can be noisy/busy, making text even harder to read.
|
|
||||||
|
|
||||||
Apart from colors you can also tweak shadow and lighting, which is used mostly to give buttons proper relief based on their state, give panes their shade, make things glow etc. It's quite powerful, and basically provides somewhat convenient interface for [CSS Shadows](https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow).
|
|
||||||
|
|
||||||
Another thing you can tweak is theme's roundness - some people like sharp edges, some want things more rounded. This is also used if you want circled or square avatars.
|
|
||||||
|
|
||||||
Lastly, you can redefine fonts used in UI without changing fonts in your browser or system, this however requires you to enter font's full name and having that font installed on your system.
|
|
||||||
|
|
||||||
## Filtering
|
|
||||||
|
|
||||||
- Types of notifications to show: This controls what kind of notifications will appear in notification column and which notifications to get in your system outside the web page
|
|
||||||
- Replies in timeline: You may know that other social networks like Twitter will often not display replies to other people in your timeline, even if you are following the poster. Pleroma usually will show these posts to you to encourage conversation. If you do not like this behavior, you can change it here.
|
|
||||||
- Hide post statistics: This hides the number of favorites, number of replies, etc.
|
|
||||||
- Hide user statistics: This hides the number of followers, friends, etc.
|
|
||||||
- Muted words: A list of words that will be muted (i.e. displayed in a collapsed state) on the timeline and in notifications. An easy way to tune down noise in your timeline. Posts can always be expanded when you actually want to see them.
|
|
||||||
- Hide filtered statuses: Selecting this will hide the filtered / muted posts completely instead of collapsing them.
|
|
||||||
|
|
||||||
|
|
||||||
## Version
|
|
||||||
|
|
||||||
Just displays the backend and frontend version. Useful to mention in bug reports.
|
|
Before Width: | Height: | Size: 491 B After Width: | Height: | Size: 491 B |
BIN
docs/assets/example_markdown.png
Normal file
BIN
docs/assets/example_markdown.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
|
@ -5,4 +5,4 @@ Pleroma-FE is the default user-facing frontend for Pleroma. It's user interface
|
||||||
|
|
||||||
## How can I use it?
|
## How can I use it?
|
||||||
|
|
||||||
If your instance uses Pleroma-FE, you can acces it by going to your instance (e.g. <https://pleroma.soykaf.com>). You can read more about it's basic functionality in the [Pleroma-FE User Guide](./USER_GUIDE.md). We also have [a guide for administrators](./CONFIGURATION.md) and for [hackers/contributors](./HACKING.md).
|
If your instance uses Pleroma-FE, you can acces it by going to your instance (e.g. <https://pleroma.soykaf.com>). You can read more about it's basic functionality in the [Pleroma-FE User Guide](./user_guide/). We also have [a guide for administrators](./CONFIGURATION.md) and for [hackers/contributors](./HACKING.md).
|
||||||
|
|
44
docs/user_guide/index.md
Normal file
44
docs/user_guide/index.md
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
# General overview
|
||||||
|
|
||||||
|
> Be prepared for breaking changes, unexpected behavior and this user guide becoming obsolete and wrong.
|
||||||
|
|
||||||
|
> If there was no insanity
|
||||||
|
>
|
||||||
|
> it would be necessary to create it.
|
||||||
|
>
|
||||||
|
> --Catbag
|
||||||
|
|
||||||
|
Pleroma-FE is the default user-facing frontend for Pleroma. If your instance uses Pleroma-FE, you can access it by going to your instance (e.g. <https://pleroma.soykaf.com>). After logging in you will have two columns in front of you. Here we're going to keep it to the default behaviour, but some instances swap the left and right columns. If you're on such an instance what we refer to as the left column will be on your right and vice versa.
|
||||||
|
|
||||||
|
### Left column
|
||||||
|
|
||||||
|
- first block: This section is dedicated to [posting](posting_reading_basic_functions.md)
|
||||||
|
- second block: Here you can switch between the different views for the right column.
|
||||||
|
- Optional third block: This is the Instance panel that can be activated, but is deactivated by default. It's fully customisable by instance admins and by default has links to the Pleroma-FE and Mastodon-FE.
|
||||||
|
- fourth block: This is the Notifications block, here you will get notified whenever somebody mentions you, follows you, repeats or favorites one of your statuses
|
||||||
|
|
||||||
|
### Right column
|
||||||
|
This is where the interesting stuff happens! There are different views depending on what you choose in the second block of the left panel.
|
||||||
|
|
||||||
|
- **Timelines** Depending on the [timeline](timelines.md) you will see different statuses, but each status has a standard structure:
|
||||||
|
- Profile pic, name and link to profile. An optional left-arrow if it's a reply to another status (hovering will reveal the reply-to status). Clicking on the profile pic will uncollapse the user's profile where you can find information about the account and can [follow, mute or block the account](users_follow_mute_block.md).
|
||||||
|
- An arrow icon on the right side allows you to open the status on the instance where it's originating from.
|
||||||
|
- A `+` button on the rightmost side allows you to Expand/Collapse an entire discussion thread.
|
||||||
|
- The text of the status, including mentions and attachments. If you click on a mention, it will automatically open the profile page of that person.
|
||||||
|
- Four buttons (left to right): Reply, Repeat, Favorite and Add Reaction. The three dots next to it are a dropdown menu for extra options including simple moderation, bookmarking, deleting posts, pinning your own posts to your profile and more.
|
||||||
|
- **Interactions** shows all interactions you've had with people on the network, basically same as notifications except grouped in convenient way.
|
||||||
|
- **Chats** is the chat feature. You can find your friends and start chatting with them. At the moment chat are only one-on-one, but once groups are introduced groupchats will also be possible.
|
||||||
|
- **About** is the about-page and lists the staff, the TOS, activated MRF's, and enabled features
|
||||||
|
|
||||||
|
### Top right
|
||||||
|
|
||||||
|
- The magnifier icon opens the search screen
|
||||||
|
- You can search for statuses, people and hashtags.
|
||||||
|
- You can import statuses from remote servers by pasting the url to the post in the search field.
|
||||||
|
- If you want to search for users that your instance doesn't know about yet, you can search for them using the full `name@instance.tld` handle. You can also use the full url from their remote profile.
|
||||||
|
- The gear icon gives you [settings](settings.md)
|
||||||
|
- If you have admin rights, you'll see an icon that opens the admin interface
|
||||||
|
- The last icon is to log out
|
||||||
|
|
||||||
|
### Bottom right
|
||||||
|
On the bottom right you have the Shoutbox. Here you can communicate with people on the same instance in realtime. It is local-only, very basic and will most probably be removed once the Chats functionality allows group chats.
|
76
docs/user_guide/posting_reading_basic_functions.md
Normal file
76
docs/user_guide/posting_reading_basic_functions.md
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
# Posting, reading, basic functions.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
Depending on your instance some of the options might not be available or have different defaults
|
||||||
|
|
||||||
|
After registering and logging in you're presented with your timeline in right column and new post form with timeline list and notifications in the left column.
|
||||||
|
|
||||||
|
Posts will contain the text you are posting, but some content will be modified:
|
||||||
|
|
||||||
|
1. Mentions: Mentions have the form of @user or @user<span></span>@instance.tld. These will become links to the user's profile. In addition, the mentioned user will always get a notification about the post they have been mentioned in, so only mention users that you want to receive this message.
|
||||||
|
2. URLs: URLs like `http://example.com` will be automatically be turned into a clickable links.
|
||||||
|
3. Hashtags: Hashtags like #cofe will also be turned into links.
|
||||||
|
4. There is a default character limit of 5000 characters.
|
||||||
|
|
||||||
|
Let's clear up some basic stuff. When you post something it's called a **post** or it could be called a **status** or even a **toot** or a **prööt** depending on whom you ask. Post has body/content but it also has some other stuff in it - from attachments, visibility scope, subject line...
|
||||||
|
|
||||||
|
**Emoji** are small images embedded in text, there are two major types of emoji: [unicode emoji](https://en.wikipedia.org/wiki/Emoji) and custom emoji. While unicode emoji are universal and standardized, they can appear differently depending on where you are using them or may not appear at all on older systems. Custom emoji are a more *fun* kind - instance administrator can define many images as *custom emoji* for their users. This works very simple - custom emoji is defined by its *shortcode* and an image, so that any shortcode enclosed in colons get replaced with image if such shortcode exist.
|
||||||
|
Let's say there's a `:pleroma:` emoji defined on an instance. That means
|
||||||
|
> First time using :pleroma: pleroma!
|
||||||
|
|
||||||
|
will become
|
||||||
|
> First time using ![pleroma](../assets/example_emoji.png) pleroma!
|
||||||
|
|
||||||
|
Note that you can only use emoji defined on your instance, you cannot "copy" someone else's emoji, and will have to ask your administrator to copy emoji from other instance to yours.
|
||||||
|
Lastly, there's two convenience options for emoji: an emoji picker (smiley face to the right of "submit" button) and autocomplete suggestions - when you start typing :shortcode: it will automatically try to suggest you emoji and complete the shortcode for you if you select one. If emoji doesn't show up in suggestions nor in emoji picker it means there's no such emoji on your instance, if shortcode doesn't match any defined emoji it will appear as text.
|
||||||
|
|
||||||
|
**Attachments** are fairly simple - you can attach any file to a post as long as the file is within maximum size limits. If you're uploading explicit material you can mark all of your attachments as sensitive (or add the `#nsfw` tag) - it will hide the images and videos behind a warning so that it won't be displayed instantly.
|
||||||
|
|
||||||
|
**Subject line** also known as **CW** (Content Warning) could be used as a header to the post and/or to warn others about contents of the post having something that might upset somebody or something among those lines. Several applications allow to hide post content leaving only subject line visible. Using a subject line will not mark your images as sensitive, you will have to do that explicitly (see above).
|
||||||
|
|
||||||
|
**Visiblity scope** controls who will be able to see your posts. There are four scopes available:
|
||||||
|
|
||||||
|
1. `Public`: This is the default, and some fediverse software, like GNU Social, only supports this. This means that your post is accessible by anyone and will be shown in the public timelines.
|
||||||
|
2. `Unlisted`: This is the same as public, but your post won't appear in the public timelines. The post will still be accessible by anyone who comes across it (for example, by looking at your profile) or by direct linking. They will also appear in public searches.
|
||||||
|
3. `Followers only`: This will show your post only to your followers. Only they will be able to interact with it. Be careful: When somebody follows you, they will be able to see all your previous `followers only` posts as well! If you want to restrict who can follow you, consider [locking your account down to only approved followers](../settings#profile).
|
||||||
|
4. `Direct`: This will only send the message to the people explicitly mentioned in the post.
|
||||||
|
|
||||||
|
A few things to consider about the security and usage of these scopes:
|
||||||
|
|
||||||
|
- None of these options will change the fact that the messages are all saved in the database unencrypted. They will be visible to your server admin and to any other admin of a server who receives this post. Do not share information that you would consider secret or dangerous. Use encrypted messaging systems for these things.
|
||||||
|
- Follower-only posts can lead to fragmented conversations. If you post a follower-only post and somebody else replies to it with a follower-only post, only people following both of you will see the whole conversation thread. Everybody else will only see half of it. Keep this in mind and keep conversations public if possible.
|
||||||
|
- Changing scopes during a thread or adding people to a direct message will not retroactively make them see the whole conversation. If you add someone to a direct message conversation, they will not see the post that happened before they were mentioned.
|
||||||
|
* **Reply-to** if you are replying to someone, your post will also contain a note that your post is referring to the post you're replying to. Person you're replying to will receive a notification *even* if you remove them from mentioned people. You won't receive notifications when replying to your own posts, but it's useful to reply to your own posts to provide people some context if it's a follow-up to a previous post. There's a small "Reply to ..." label under post author's name which you can hover on to see what post it's referring to.
|
||||||
|
|
||||||
|
Sometimes you may encounter posts that seem different than what they are supposed to. For example, you might see a direct message without any mentions in the text. This can happen because internally, the Fediverse has a different addressing mechanism similar to email, with `to` and `cc` fields. While these are not directly accessible in PleromaFE, other software in the Fediverse might generate those posts. Do not worry in these cases, these are normal and not a bug.
|
||||||
|
|
||||||
|
## Rich text
|
||||||
|
|
||||||
|
By default new posts you make are plaintext, meaning you can't make text **bold** or add custom links or make lists or anything like that. However if your instance allows it you can use Markdown or BBCode or HTML to spice up your text, however there are certain limitations to what HTML tags and what features of Markdown you can use.
|
||||||
|
|
||||||
|
Here is a small example of some text in markdown.
|
||||||
|
|
||||||
|
```
|
||||||
|
This is an example of markdown text using **bold** and *cursive* text.
|
||||||
|
To get a newline we add two spaces at the end of the previous line.
|
||||||
|
|
||||||
|
Let's also add a list
|
||||||
|
|
||||||
|
* with
|
||||||
|
* some
|
||||||
|
* items
|
||||||
|
```
|
||||||
|
|
||||||
|
If you set the input-method to Markdown, and post this, it will look something like
|
||||||
|
|
||||||
|
![example_markdown](../assets/example_markdown.png)
|
||||||
|
|
||||||
|
## Other actions
|
||||||
|
|
||||||
|
In addition to posting you can also *favorite* posts also known as *liking* them and *repeat* posts (also known as *retweeting*, *boosting* and even *reprööting*). Favoriting a post increments a counter on it, notifies the post author of your affection towards that post and also adds that post to your "favorited" posts list (in your own profile, "Favorites" tab). Reprööting a post does all that and also repeats this post to your followers and your profile page with a note "*user* repeated post".
|
||||||
|
|
||||||
|
Your own posts can be deleted, but this will only reliably delete the post from your own instance. Other instances will receive a deletion notice, but there's no way to force them to actually delete a post. In addition, not all instances that contain the message might even receive the deletion notice, because they might be offline or not known to have the post because they received it through a repeat. Lastly, deletion notice might not reach certain frontends and clients - post will be visible for them until page refresh or cache clear, they probably won't be able to interact with it apart from replying to it (which will have reply-to mark missing).
|
||||||
|
|
||||||
|
If you are a moderator, you can also delete posts by other people. If those people are on your instance, it will delete the post and send out the deletion notice to other servers. If they are not on your instance, it will just remove the post from your local instance.
|
||||||
|
|
||||||
|
There's also an option to report a user's post which can be used to notify your (and optionally the other instance's) admin that someone is being naughty.
|
116
docs/user_guide/settings.md
Normal file
116
docs/user_guide/settings.md
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
# Settings
|
||||||
|
|
||||||
|
On the top-right you will see a gear icon. Click it to open the settings.
|
||||||
|
|
||||||
|
## General
|
||||||
|
|
||||||
|
### Interface
|
||||||
|
|
||||||
|
- **Interface language** is where you can set the interface language. The default language is the one that you set in your browser settings.
|
||||||
|
- **Hide instance-specific panel** hides the panel in the lower left that usually contains general information about the server. This will only be visible if your admin has activated this panel and is deactivated by default.
|
||||||
|
|
||||||
|
### Timeline
|
||||||
|
|
||||||
|
- **Hide posts of muted users** If this is set, 'muting' a user will completely hide their posts instead of collapsing them.
|
||||||
|
- **Collapse posts with subjects** This will collapse posts that contain a subject, hiding their content. Subjects are also sometimes called content warnings.
|
||||||
|
- **Enable automatic streaming of new posts when scrolled to the top** With this enabled, new posts will automatically stream in when you are scrolled to the top. Otherwise, you will see a button on the timeline that will let you display the new posts.
|
||||||
|
- **Pause streaming when tab is not focused** This pauses the automatic streaming that the previous option enables when the tab is out of focus. This is useful if you don't want to miss any new posts.
|
||||||
|
- **Enable automatic loading when scrolled to the bottom** When this is disabled, a button will be shown on the bottom of the timeline that will let you load older posts.
|
||||||
|
- **Enable reply-link preview on hover** Status posts in the timeline and notifications contain links to replies and to the post they are a reply to. If this setting is enabled, hovering over that link will display that linked post in a small hovering overlay.
|
||||||
|
|
||||||
|
### Composing
|
||||||
|
|
||||||
|
- **Copy scope when replying** makes the scope of a reply be the same as the scope of the post it is replying to. This is useful to prevent accidentally moving private discussions to public, or vice versa.
|
||||||
|
- **Always show subject field** Whether or not to display the 'subject' input field in the post form. If you do not want to use subjects, you can deactivate this.
|
||||||
|
- **Copy subject when replying** controls if the subject of a post will be copied from the post it is replying to.
|
||||||
|
- **Post status content type** selects the default content type of your post. The options are: Plain text, HTML, BBCode and Markdown.
|
||||||
|
- **Minimize scope selection options** will reduce the visibility scopes to 'direct', your default post scope and post scope of post you're replying to.
|
||||||
|
- **Automatically hide New Post button** hides the floating "New post" button when scrolling on mobile view.
|
||||||
|
- **Pad emoji with spaces when adding from picker** Will add spaces around emoji you select it from the picker.
|
||||||
|
|
||||||
|
### Attachments
|
||||||
|
|
||||||
|
- **Hide attachments in timeline** Do not display attachments in timelines. They will still display in expanded conversations. This is useful to save bandwidth and for browsing in public.
|
||||||
|
- **Hide attachments in conversations** Also hide attachments in expanded conversations.
|
||||||
|
- **Maximum amount of thumbnails per post** Exactly that :)
|
||||||
|
- **Enable clickthrough NSFW attachment hiding** Hide attachments that are marked as NSFW/sensitive behind a click-through image.`
|
||||||
|
- **Preload images** This will preload the hidden images so that they display faster when clicking through.
|
||||||
|
- **Open NSFW attachments with just one click** Directly open NSFW attachments in a maximised state instead of revealing the image thumbnail.
|
||||||
|
- **Play-on-hover GIFs** With this activated, GIFs images and avatars will only be animated on mouse hover. Otherwise, they will be always animated. This is very useful if your timeline looks too flashy from people's animated avatars and eases the CPU load.
|
||||||
|
- **Loop videos** Whether to loop videos indefinitely.
|
||||||
|
- **Loop only videos without sound** Some instances will use videos without sounds instead of GIFs. This will make only those videos autoplay.
|
||||||
|
- **Play videos directly in the media viewer** Play videos right in the timeline instead of opening it in a modal
|
||||||
|
- **Don't crop the attachment in thumbnails** if enabled, images in attachments will be fit entirely inside the container instead of being zoomed in and cropped.
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
|
||||||
|
- **Enable web push notifications** this enables Web Push notifications, to allow receiving notifications even when the page isn't opened, doesn't affect regular notifications.
|
||||||
|
|
||||||
|
### Fun
|
||||||
|
|
||||||
|
- **Meme arrows** will make `> greentext` be shown in green (using the "green" from the theme that is used).
|
||||||
|
|
||||||
|
## Profile
|
||||||
|
|
||||||
|
Here you can set up how you appear to other users among with some other settings:
|
||||||
|
|
||||||
|
- **Name** is text that displays next to your avatar in posts. Please note that you **cannot** change your *@handle*
|
||||||
|
- **Bio** will be displayed under your profile - you can put anything you want there you want for everyone to see.
|
||||||
|
- **Restrict your account to approved followers only** makes your account "locked", when people follow you - you have to approve or deny their follow requests, this gives more control over who sees your followers only posts.
|
||||||
|
- **Default visibility scope** is your default post scope for new posts
|
||||||
|
- **Strip rich text from all posts** strips rich text formatting (bold/italics/lists etc) from all incoming posts. This will only affect newly fetched posts.
|
||||||
|
|
||||||
|
If you're admin or moderator on your instance you also get **Show [role] badge in my profile** - this controls whether to show "Admin" or "Moderator** label on your profile page.
|
||||||
|
|
||||||
|
**For all options mentioned above you have to click "Submit" button for changes to take place**
|
||||||
|
|
||||||
|
- **Avatar** this changes picture next to your posts. Your avatar shouldn't exceed 2 MiB (2097152 bytes) or it could cause problems with certain instances.
|
||||||
|
- **Banner** this changes background on your profile card. Same as avatar it shouldn't exceed 2 MiB limit.
|
||||||
|
- **Profile Background** this changes background picture for UI. It isn't shown to anyone else *yet*, but some time later it will be shown when viewing your profisle.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
Here you can change your password, revoke access tokens, configure 2-factor authentication (if available).
|
||||||
|
|
||||||
|
## Filtering
|
||||||
|
|
||||||
|
- **Types of notifications to show** This controls what kind of notifications will appear in notification column and which notifications to get in your system outside the web page
|
||||||
|
- **Replies in timeline** You may know that other social networks like Twitter will often not display replies to other people in your timeline, even if you are following the poster. Pleroma usually will show these posts to you to encourage conversation. If you do not like this behavior, you can change it here.
|
||||||
|
- **Hide post statistics** This hides the number of favorites, number of replies, etc.
|
||||||
|
- **Hide user statistics** This hides the number of followers, friends, etc.
|
||||||
|
- **Muted words** allows a list of words that will be muted (i.e. displayed in a collapsed state) on the timeline and in notifications. An easy way to tune down noise in your timeline. By default posts can be expanded if you want to see them.
|
||||||
|
- **Hide filtered statuses** will hide the filtered / muted posts completely instead of collapsing them.
|
||||||
|
|
||||||
|
## Theme
|
||||||
|
|
||||||
|
Here you can change the look and feel of Pleroma-FE. You can choose from several instance-provided presets and you can load one from file and save current theme to file. Before you apply new theme you can see what it will look like approximately in preview section.
|
||||||
|
|
||||||
|
The themes engine was made to be easy to use while giving an option for powerful in-depth customization - you can just tweak colors on "Common" tab and leave everything else as is.
|
||||||
|
|
||||||
|
If there's a little check box next to a color picker it means that color is optional and unless checked will be automatically picked based on some other color or defaults.
|
||||||
|
|
||||||
|
For some features you can also adjust transparency of it by changing its opacity, you just need to tick checkbox next to it, otherwise it will be using default opacity.
|
||||||
|
|
||||||
|
Contrast information is also provided - you can see how readable text is based on contrast between text color and background, icons under color pickers represent contrast rating based on [WCAG](https://www.w3.org/TR/2008/REC-WCAG20-20081211/#visual-audio-contrast-contrast) - thumbs up means AAA rating (good), half-filled circle means AA rating (acceptable) and warning icon means it doesn't pass the minimal contrast requirement and probably will be less readable, especially for vision-challenged people, you can hover over icon to see more detailed information. *Please note* that if background is not opaque (opacity != 1) contrast will be measured based on "worst case scenario", i.e. behind semi-transparent background lies some solid color that makes text harder to read, this however is still inaccurate because it doesn't account that background can be noisy/busy, making text even harder to read.
|
||||||
|
|
||||||
|
Apart from colors you can also tweak shadow and lighting, which is used mostly to give buttons proper relief based on their state, give panes their shade, make things glow etc. It's quite powerful, and basically provides somewhat convenient interface for [CSS Shadows](https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow).
|
||||||
|
|
||||||
|
Another thing you can tweak is theme's roundness - some people like sharp edges, some want things more rounded. This is also used if you want circled or square avatars.
|
||||||
|
|
||||||
|
Lastly, you can redefine fonts used in UI without changing fonts in your browser or system, this however requires you to enter font's full name and having that font installed on your system.
|
||||||
|
|
||||||
|
## Notifications
|
||||||
|
|
||||||
|
This screen allows more fine-grained control over what notifications to show to you based on whom it comes from.
|
||||||
|
|
||||||
|
## Data Import/Export
|
||||||
|
|
||||||
|
This allows you to export and import a list of people you follow and block, in case instance's database gets reverted or if you want to move to another server. Note that you **CANNOT export/import list of people who *follow you***, they'll need to follow you back themselves.
|
||||||
|
|
||||||
|
## Mutes and Blocks
|
||||||
|
|
||||||
|
These screens give access to full list of people you block/mute, useful for *un*blocking/*un*muting people because blocking/muting them most likely removes them out of your sight completely.
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
Just displays the backend and frontend version. Useful to mention in bug reports.
|
13
docs/user_guide/timelines.md
Normal file
13
docs/user_guide/timelines.md
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Timelines
|
||||||
|
|
||||||
|
You have several timelines to browse trough
|
||||||
|
|
||||||
|
- **Timeline** aka Home Timeline - this timeline contains all posts by people you follow and your own posts, as well as posts mentioning you directly.
|
||||||
|
- **Bookmarks** all the posts you've bookmarked. You can bookmark a post by clicking the three dots on the bottom right of the post and choose Bookmark.
|
||||||
|
- **Direct Messages** all posts with `direct` scope addressed to you or mentioning you.
|
||||||
|
- **Public Timelines** all public posts made by users on the instance you're on
|
||||||
|
- **The Whole Known Network** also known as **TWKN** or **Federated Timeline** - all public posts known by your instance. Due to nature of the network your instance may not know *all* the posts on the network, so only posts known by your instance are shown there.
|
||||||
|
|
||||||
|
Note that by default you will see all posts made by other users on your Home Timeline, this contrast behavior of Twitter and Mastodon, which shows you only non-reply posts and replies to people you follow. You can change said behavior in the [settings](settings.md#filtering).
|
||||||
|
|
||||||
|
By default instances will try to send activities (e.g. posts, favorites, etc.) up to 7 days or until the target server received them. For this reason posts that are up to 7 days old and your server didn't know about yet can pop up on your timeline. This is the default behaviour and can be changed by your admin.
|
11
docs/user_guide/users_follow_mute_block.md
Normal file
11
docs/user_guide/users_follow_mute_block.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# Users: follow, mute, block
|
||||||
|
|
||||||
|
When you see someone, you can click on their user picture to view their profile, and click on the userpic in that to see *full* profile. You can **follow** them, **mute** and **block** them.
|
||||||
|
|
||||||
|
**Following** is self-explanatory, it adds them to your Home Timeline, lists you as a follower and gives you access to follower-only posts if they have any.
|
||||||
|
|
||||||
|
**Muting** collapses posts and notifications made by them, giving you an option to see the post if you're curious. Clients other than PleromaFE may completely remove their posts.
|
||||||
|
|
||||||
|
**Blocking** a user removes them from your timeline and notifications and prevents them from following you (automatically unfollows them from you).
|
||||||
|
|
||||||
|
Please note that some users can be "locked", meaning instead of following them you send a follow request they need to approve for you to become their follower.
|
|
@ -3,7 +3,6 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
|
||||||
<title>Pleroma</title>
|
|
||||||
<!--server-generated-meta-->
|
<!--server-generated-meta-->
|
||||||
<link rel="icon" type="image/png" href="/favicon.png">
|
<link rel="icon" type="image/png" href="/favicon.png">
|
||||||
</head>
|
</head>
|
||||||
|
|
195
package.json
195
package.json
|
@ -16,102 +16,111 @@
|
||||||
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
|
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.7.6",
|
"@babel/runtime": "7.17.8",
|
||||||
"@chenfengyuan/vue-qrcode": "^1.0.0",
|
"@chenfengyuan/vue-qrcode": "2.0.0",
|
||||||
"body-scroll-lock": "^2.6.4",
|
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
||||||
"chromatism": "^3.0.0",
|
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||||
"cropperjs": "^1.4.3",
|
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||||
"diff": "^3.0.1",
|
"@fortawesome/vue-fontawesome": "3.0.0-5",
|
||||||
"escape-html": "^1.0.3",
|
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
|
||||||
"localforage": "^1.5.0",
|
"@vuelidate/core": "2.0.0-alpha.35",
|
||||||
"parse-link-header": "^1.0.1",
|
"@vuelidate/validators": "2.0.0-alpha.27",
|
||||||
"phoenix": "^1.3.0",
|
"body-scroll-lock": "2.7.1",
|
||||||
"portal-vue": "^2.1.4",
|
"chromatism": "3.0.0",
|
||||||
"v-click-outside": "^2.1.1",
|
"click-outside-vue3": "4.0.1",
|
||||||
"vue": "^2.6.11",
|
"cropperjs": "1.5.12",
|
||||||
"vue-chat-scroll": "^1.2.1",
|
"diff": "3.5.0",
|
||||||
"vue-i18n": "^7.3.2",
|
"escape-html": "1.0.3",
|
||||||
"vue-router": "^3.0.1",
|
"localforage": "1.10.0",
|
||||||
"vue-template-compiler": "^2.6.11",
|
"parse-link-header": "1.0.1",
|
||||||
"vuelidate": "^0.7.4",
|
"phoenix": "1.6.2",
|
||||||
"vuex": "^3.0.1"
|
"punycode.js": "2.1.0",
|
||||||
|
"qrcode": "1",
|
||||||
|
"ruffle-mirror": "2021.12.31",
|
||||||
|
"vue": "^3.2.31",
|
||||||
|
"vue-i18n": "^9.2.0-beta.34",
|
||||||
|
"vue-router": "4.0.14",
|
||||||
|
"vue-template-compiler": "2.6.11",
|
||||||
|
"vuex": "4.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.7.5",
|
"@babel/core": "7.17.8",
|
||||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
"@babel/plugin-transform-runtime": "7.17.0",
|
||||||
"@babel/preset-env": "^7.7.6",
|
"@babel/preset-env": "7.16.11",
|
||||||
"@babel/register": "^7.7.4",
|
"@babel/register": "7.17.7",
|
||||||
"@ungap/event-target": "^0.1.0",
|
"@intlify/vue-i18n-loader": "^5.0.0",
|
||||||
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
|
"@ungap/event-target": "0.2.3",
|
||||||
"@vue/babel-plugin-transform-vue-jsx": "^1.1.2",
|
"@vue/babel-helper-vue-jsx-merge-props": "1.2.1",
|
||||||
"@vue/test-utils": "^1.0.0-beta.26",
|
"@vue/babel-plugin-jsx": "1.1.1",
|
||||||
"autoprefixer": "^6.4.0",
|
"@vue/compiler-sfc": "^3.1.0",
|
||||||
"babel-eslint": "^7.0.0",
|
"@vue/test-utils": "2.0.0-rc.17",
|
||||||
"babel-loader": "^8.0.6",
|
"autoprefixer": "6.7.7",
|
||||||
"babel-plugin-lodash": "^3.3.4",
|
"babel-eslint": "7.2.3",
|
||||||
"chai": "^3.5.0",
|
"babel-loader": "8.2.4",
|
||||||
"chalk": "^1.1.3",
|
"babel-plugin-lodash": "3.3.4",
|
||||||
"chromedriver": "^2.21.2",
|
"chai": "3.5.0",
|
||||||
"connect-history-api-fallback": "^1.1.0",
|
"chalk": "1.1.3",
|
||||||
"cross-spawn": "^4.0.2",
|
"chromedriver": "87.0.7",
|
||||||
"css-loader": "^0.28.0",
|
"connect-history-api-fallback": "1.6.0",
|
||||||
"custom-event-polyfill": "^1.0.7",
|
"copy-webpack-plugin": "6.4.1",
|
||||||
"eslint": "^5.16.0",
|
"cross-spawn": "4.0.2",
|
||||||
"eslint-config-standard": "^12.0.0",
|
"css-loader": "0.28.11",
|
||||||
"eslint-friendly-formatter": "^2.0.5",
|
"custom-event-polyfill": "1.0.7",
|
||||||
"eslint-loader": "^2.1.0",
|
"eslint": "5.16.0",
|
||||||
"eslint-plugin-import": "^2.13.0",
|
"eslint-config-standard": "12.0.0",
|
||||||
"eslint-plugin-node": "^7.0.0",
|
"eslint-friendly-formatter": "2.0.7",
|
||||||
"eslint-plugin-promise": "^4.0.0",
|
"eslint-loader": "2.2.1",
|
||||||
"eslint-plugin-standard": "^4.0.0",
|
"eslint-plugin-import": "2.25.4",
|
||||||
"eslint-plugin-vue": "^5.2.2",
|
"eslint-plugin-node": "7.0.1",
|
||||||
"eventsource-polyfill": "^0.9.6",
|
"eslint-plugin-promise": "4.3.1",
|
||||||
"express": "^4.13.3",
|
"eslint-plugin-standard": "4.1.0",
|
||||||
"file-loader": "^3.0.1",
|
"eslint-plugin-vue": "5.2.3",
|
||||||
"fontello-webpack-plugin": "https://github.com/w3geo/fontello-webpack-plugin.git#6149eac8f2672ab6da089e8802fbfcac98487186",
|
"eventsource-polyfill": "0.9.6",
|
||||||
"function-bind": "^1.0.2",
|
"express": "4.17.3",
|
||||||
"html-webpack-plugin": "^3.0.0",
|
"file-loader": "3.0.1",
|
||||||
"http-proxy-middleware": "^0.17.2",
|
"function-bind": "1.1.1",
|
||||||
"inject-loader": "^2.0.1",
|
"html-webpack-plugin": "3.2.0",
|
||||||
"iso-639-1": "^2.0.3",
|
"http-proxy-middleware": "0.21.0",
|
||||||
"isparta-loader": "^2.0.0",
|
"inject-loader": "2.0.1",
|
||||||
"json-loader": "^0.5.4",
|
"iso-639-1": "2.1.13",
|
||||||
"karma": "^3.0.0",
|
"isparta-loader": "2.0.0",
|
||||||
"karma-coverage": "^1.1.1",
|
"json-loader": "0.5.7",
|
||||||
"karma-firefox-launcher": "^1.1.0",
|
"karma": "6.3.17",
|
||||||
"karma-mocha": "^1.2.0",
|
"karma-coverage": "1.1.2",
|
||||||
"karma-mocha-reporter": "^2.2.1",
|
"karma-firefox-launcher": "1.3.0",
|
||||||
"karma-sinon-chai": "^2.0.2",
|
"karma-mocha": "2.0.1",
|
||||||
"karma-sourcemap-loader": "^0.3.7",
|
"karma-mocha-reporter": "2.2.5",
|
||||||
"karma-spec-reporter": "0.0.26",
|
"karma-sinon-chai": "2.0.2",
|
||||||
"karma-webpack": "^4.0.0-rc.3",
|
"karma-sourcemap-loader": "0.3.8",
|
||||||
"lodash": "^4.16.4",
|
"karma-spec-reporter": "0.0.33",
|
||||||
"lolex": "^1.4.0",
|
"karma-webpack": "4.0.2",
|
||||||
"mini-css-extract-plugin": "^0.5.0",
|
"lodash": "4.17.21",
|
||||||
"mocha": "^3.1.0",
|
"lolex": "1.6.0",
|
||||||
"nightwatch": "^0.9.8",
|
"mini-css-extract-plugin": "0.12.0",
|
||||||
"opn": "^4.0.2",
|
"mocha": "3.5.3",
|
||||||
"ora": "^0.3.0",
|
"nightwatch": "0.9.21",
|
||||||
"postcss-loader": "^3.0.0",
|
"opn": "4.0.2",
|
||||||
"raw-loader": "^0.5.1",
|
"ora": "0.4.1",
|
||||||
"sass": "^1.17.3",
|
"postcss-loader": "3.0.0",
|
||||||
"sass-loader": "git://github.com/webpack-contrib/sass-loader",
|
"raw-loader": "0.5.1",
|
||||||
|
"sass": "1.20.1",
|
||||||
|
"sass-loader": "7.2.0",
|
||||||
"selenium-server": "2.53.1",
|
"selenium-server": "2.53.1",
|
||||||
"semver": "^5.3.0",
|
"semver": "5.6.0",
|
||||||
"serviceworker-webpack-plugin": "^1.0.0",
|
"serviceworker-webpack-plugin": "1.0.1",
|
||||||
"shelljs": "^0.7.4",
|
"shelljs": "0.8.5",
|
||||||
"sinon": "^2.1.0",
|
"sinon": "2.4.1",
|
||||||
"sinon-chai": "^2.8.0",
|
"sinon-chai": "2.14.0",
|
||||||
"stylelint": "^13.6.1",
|
"stylelint": "13.6.1",
|
||||||
"stylelint-config-standard": "^20.0.0",
|
"stylelint-config-standard": "20.0.0",
|
||||||
"stylelint-rscss": "^0.4.0",
|
"stylelint-rscss": "0.4.0",
|
||||||
"url-loader": "^1.1.2",
|
"url-loader": "1.1.2",
|
||||||
"vue-loader": "^14.0.0",
|
"vue-loader": "^16.0.0",
|
||||||
"vue-style-loader": "^4.0.0",
|
"vue-style-loader": "4.1.2",
|
||||||
"webpack": "^4.0.0",
|
"webpack": "4.46.0",
|
||||||
"webpack-dev-middleware": "^3.6.0",
|
"webpack-dev-middleware": "3.7.3",
|
||||||
"webpack-hot-middleware": "^2.12.2",
|
"webpack-hot-middleware": "2.24.3",
|
||||||
"webpack-merge": "^0.14.1"
|
"webpack-merge": "0.14.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 4.0.0",
|
"node": ">= 4.0.0",
|
||||||
|
|
6
renovate.json
Normal file
6
renovate.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:base"
|
||||||
|
]
|
||||||
|
}
|
90
src/App.js
90
src/App.js
|
@ -1,20 +1,21 @@
|
||||||
import UserPanel from './components/user_panel/user_panel.vue'
|
import UserPanel from './components/user_panel/user_panel.vue'
|
||||||
import NavPanel from './components/nav_panel/nav_panel.vue'
|
import NavPanel from './components/nav_panel/nav_panel.vue'
|
||||||
import Notifications from './components/notifications/notifications.vue'
|
import Notifications from './components/notifications/notifications.vue'
|
||||||
import SearchBar from './components/search_bar/search_bar.vue'
|
|
||||||
import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
|
import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
|
||||||
import FeaturesPanel from './components/features_panel/features_panel.vue'
|
import FeaturesPanel from './components/features_panel/features_panel.vue'
|
||||||
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
||||||
import ChatPanel from './components/chat_panel/chat_panel.vue'
|
import ShoutPanel from './components/shout_panel/shout_panel.vue'
|
||||||
import SettingsModal from './components/settings_modal/settings_modal.vue'
|
import SettingsModal from './components/settings_modal/settings_modal.vue'
|
||||||
import MediaModal from './components/media_modal/media_modal.vue'
|
import MediaModal from './components/media_modal/media_modal.vue'
|
||||||
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||||
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
|
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
|
||||||
import MobileNav from './components/mobile_nav/mobile_nav.vue'
|
import MobileNav from './components/mobile_nav/mobile_nav.vue'
|
||||||
|
import DesktopNav from './components/desktop_nav/desktop_nav.vue'
|
||||||
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
|
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
|
||||||
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
|
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
|
||||||
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
|
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
|
||||||
import { windowWidth, windowHeight } from './services/window_utils/window_utils'
|
import { windowWidth, windowHeight } from './services/window_utils/window_utils'
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'app',
|
name: 'app',
|
||||||
|
@ -22,30 +23,22 @@ export default {
|
||||||
UserPanel,
|
UserPanel,
|
||||||
NavPanel,
|
NavPanel,
|
||||||
Notifications,
|
Notifications,
|
||||||
SearchBar,
|
|
||||||
InstanceSpecificPanel,
|
InstanceSpecificPanel,
|
||||||
FeaturesPanel,
|
FeaturesPanel,
|
||||||
WhoToFollowPanel,
|
WhoToFollowPanel,
|
||||||
ChatPanel,
|
ShoutPanel,
|
||||||
MediaModal,
|
MediaModal,
|
||||||
SideDrawer,
|
SideDrawer,
|
||||||
MobilePostStatusButton,
|
MobilePostStatusButton,
|
||||||
MobileNav,
|
MobileNav,
|
||||||
|
DesktopNav,
|
||||||
SettingsModal,
|
SettingsModal,
|
||||||
UserReportingModal,
|
UserReportingModal,
|
||||||
PostStatusModal,
|
PostStatusModal,
|
||||||
GlobalNoticeList
|
GlobalNoticeList
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
mobileActivePanel: 'timeline',
|
mobileActivePanel: 'timeline'
|
||||||
searchBarHidden: true,
|
|
||||||
supportsMask: window.CSS && window.CSS.supports && (
|
|
||||||
window.CSS.supports('mask-size', 'contain') ||
|
|
||||||
window.CSS.supports('-webkit-mask-size', 'contain') ||
|
|
||||||
window.CSS.supports('-moz-mask-size', 'contain') ||
|
|
||||||
window.CSS.supports('-ms-mask-size', 'contain') ||
|
|
||||||
window.CSS.supports('-o-mask-size', 'contain')
|
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
created () {
|
created () {
|
||||||
// Load the locale from the storage
|
// Load the locale from the storage
|
||||||
|
@ -53,49 +46,26 @@ export default {
|
||||||
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
||||||
window.addEventListener('resize', this.updateMobileState)
|
window.addEventListener('resize', this.updateMobileState)
|
||||||
},
|
},
|
||||||
destroyed () {
|
unmounted () {
|
||||||
window.removeEventListener('resize', this.updateMobileState)
|
window.removeEventListener('resize', this.updateMobileState)
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
currentUser () { return this.$store.state.users.currentUser },
|
currentUser () { return this.$store.state.users.currentUser },
|
||||||
background () {
|
userBackground () { return this.currentUser.background_image },
|
||||||
return this.currentUser.background_image || this.$store.state.instance.background
|
instanceBackground () {
|
||||||
|
return this.mergedConfig.hideInstanceWallpaper
|
||||||
|
? null
|
||||||
|
: this.$store.state.instance.background
|
||||||
},
|
},
|
||||||
enableMask () { return this.supportsMask && this.$store.state.instance.logoMask },
|
background () { return this.userBackground || this.instanceBackground },
|
||||||
logoStyle () {
|
|
||||||
return {
|
|
||||||
'visibility': this.enableMask ? 'hidden' : 'visible'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
logoMaskStyle () {
|
|
||||||
return this.enableMask ? {
|
|
||||||
'mask-image': `url(${this.$store.state.instance.logo})`
|
|
||||||
} : {
|
|
||||||
'background-color': this.enableMask ? '' : 'transparent'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
logoBgStyle () {
|
|
||||||
return Object.assign({
|
|
||||||
'margin': `${this.$store.state.instance.logoMargin} 0`,
|
|
||||||
opacity: this.searchBarHidden ? 1 : 0
|
|
||||||
}, this.enableMask ? {} : {
|
|
||||||
'background-color': this.enableMask ? '' : 'transparent'
|
|
||||||
})
|
|
||||||
},
|
|
||||||
logo () { return this.$store.state.instance.logo },
|
|
||||||
bgStyle () {
|
bgStyle () {
|
||||||
return {
|
if (this.background) {
|
||||||
'background-image': `url(${this.background})`
|
return {
|
||||||
|
'--body-background-image': `url(${this.background})`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bgAppStyle () {
|
shout () { return this.$store.state.shout.joined },
|
||||||
return {
|
|
||||||
'--body-background-image': `url(${this.background})`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sitename () { return this.$store.state.instance.name },
|
|
||||||
chat () { return this.$store.state.chat.channel.state === 'joined' },
|
|
||||||
hideSitename () { return this.$store.state.instance.hideSitename },
|
|
||||||
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
|
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
|
||||||
showInstanceSpecificPanel () {
|
showInstanceSpecificPanel () {
|
||||||
return this.$store.state.instance.showInstanceSpecificPanel &&
|
return this.$store.state.instance.showInstanceSpecificPanel &&
|
||||||
|
@ -103,28 +73,22 @@ export default {
|
||||||
this.$store.state.instance.instanceSpecificPanelContent
|
this.$store.state.instance.instanceSpecificPanelContent
|
||||||
},
|
},
|
||||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||||
|
shoutboxPosition () {
|
||||||
|
return this.$store.getters.mergedConfig.showNewPostButton || false
|
||||||
|
},
|
||||||
|
hideShoutbox () {
|
||||||
|
return this.$store.getters.mergedConfig.hideShoutbox
|
||||||
|
},
|
||||||
isMobileLayout () { return this.$store.state.interface.mobileLayout },
|
isMobileLayout () { return this.$store.state.interface.mobileLayout },
|
||||||
privateMode () { return this.$store.state.instance.private },
|
privateMode () { return this.$store.state.instance.private },
|
||||||
sidebarAlign () {
|
sidebarAlign () {
|
||||||
return {
|
return {
|
||||||
'order': this.$store.state.instance.sidebarRight ? 99 : 0
|
'order': this.$store.getters.mergedConfig.sidebarRight ? 99 : 0
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
...mapGetters(['mergedConfig'])
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
scrollToTop () {
|
|
||||||
window.scrollTo(0, 0)
|
|
||||||
},
|
|
||||||
logout () {
|
|
||||||
this.$router.replace('/main/public')
|
|
||||||
this.$store.dispatch('logout')
|
|
||||||
},
|
|
||||||
onSearchBarToggled (hidden) {
|
|
||||||
this.searchBarHidden = hidden
|
|
||||||
},
|
|
||||||
openSettingsModal () {
|
|
||||||
this.$store.dispatch('openSettingsModal')
|
|
||||||
},
|
|
||||||
updateMobileState () {
|
updateMobileState () {
|
||||||
const mobileLayout = windowWidth() <= 800
|
const mobileLayout = windowWidth() <= 800
|
||||||
const layoutHeight = windowHeight()
|
const layoutHeight = windowHeight()
|
||||||
|
|
336
src/App.scss
336
src/App.scss
|
@ -14,7 +14,9 @@
|
||||||
right: -20px;
|
right: -20px;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: 0 50%;
|
background-color: var(--wallpaper);
|
||||||
|
background-image: var(--body-background-image);
|
||||||
|
background-position: 50% 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
i[class^='icon-'] {
|
i[class^='icon-'] {
|
||||||
|
@ -33,6 +35,7 @@ h4 {
|
||||||
max-width: 980px;
|
max-width: 980px;
|
||||||
align-content: flex-start;
|
align-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.underlay {
|
.underlay {
|
||||||
background-color: rgba(0,0,0,0.15);
|
background-color: rgba(0,0,0,0.15);
|
||||||
background-color: var(--underlay, rgba(0,0,0,0.15));
|
background-color: var(--underlay, rgba(0,0,0,0.15));
|
||||||
|
@ -69,7 +72,7 @@ a {
|
||||||
color: var(--link, $fallback--link);
|
color: var(--link, $fallback--link);
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
.button-default {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--btnText, $fallback--text);
|
color: var(--btnText, $fallback--text);
|
||||||
|
@ -85,7 +88,12 @@ button {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
font-family: var(--interfaceFont, sans-serif);
|
font-family: var(--interfaceFont, sans-serif);
|
||||||
|
|
||||||
i[class*=icon-] {
|
&.-sublime {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
i[class*=icon-],
|
||||||
|
.svg-inline--fa {
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--btnText, $fallback--text);
|
color: var(--btnText, $fallback--text);
|
||||||
}
|
}
|
||||||
|
@ -106,6 +114,8 @@ button {
|
||||||
color: var(--btnPressedText, $fallback--text);
|
color: var(--btnPressedText, $fallback--text);
|
||||||
background-color: $fallback--fg;
|
background-color: $fallback--fg;
|
||||||
background-color: var(--btnPressed, $fallback--fg);
|
background-color: var(--btnPressed, $fallback--fg);
|
||||||
|
|
||||||
|
svg,
|
||||||
i {
|
i {
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--btnPressedText, $fallback--text);
|
color: var(--btnPressedText, $fallback--text);
|
||||||
|
@ -118,6 +128,8 @@ button {
|
||||||
color: var(--btnDisabledText, $fallback--text);
|
color: var(--btnDisabledText, $fallback--text);
|
||||||
background-color: $fallback--fg;
|
background-color: $fallback--fg;
|
||||||
background-color: var(--btnDisabled, $fallback--fg);
|
background-color: var(--btnDisabled, $fallback--fg);
|
||||||
|
|
||||||
|
svg,
|
||||||
i {
|
i {
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--btnDisabledText, $fallback--text);
|
color: var(--btnDisabledText, $fallback--text);
|
||||||
|
@ -131,6 +143,8 @@ button {
|
||||||
background-color: var(--btnToggled, $fallback--fg);
|
background-color: var(--btnToggled, $fallback--fg);
|
||||||
box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||||
box-shadow: var(--buttonPressedShadow);
|
box-shadow: var(--buttonPressedShadow);
|
||||||
|
|
||||||
|
svg,
|
||||||
i {
|
i {
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--btnToggledText, $fallback--text);
|
color: var(--btnToggledText, $fallback--text);
|
||||||
|
@ -146,7 +160,38 @@ button {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input, textarea, .select, .input {
|
.button-unstyled {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
display: inline;
|
||||||
|
text-align: initial;
|
||||||
|
font-size: 100%;
|
||||||
|
font-family: inherit;
|
||||||
|
padding: 0;
|
||||||
|
line-height: unset;
|
||||||
|
cursor: pointer;
|
||||||
|
box-sizing: content-box;
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
|
&.-link {
|
||||||
|
color: $fallback--link;
|
||||||
|
color: var(--link, $fallback--link);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-fullwidth {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-hover-highlight {
|
||||||
|
&:hover svg {
|
||||||
|
color: $fallback--lightText;
|
||||||
|
color: var(--lightText, $fallback--lightText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input, textarea, .input {
|
||||||
|
|
||||||
&.unstyled {
|
&.unstyled {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
@ -176,47 +221,11 @@ input, textarea, .select, .input {
|
||||||
hyphens: none;
|
hyphens: none;
|
||||||
padding: 8px .5em;
|
padding: 8px .5em;
|
||||||
|
|
||||||
&.select {
|
&:disabled, &[disabled=disabled], &.disabled {
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled, &[disabled=disabled] {
|
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-down-open {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
right: 5px;
|
|
||||||
height: 100%;
|
|
||||||
color: $fallback--text;
|
|
||||||
color: var(--inputText, $fallback--text);
|
|
||||||
line-height: 28px;
|
|
||||||
z-index: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
-moz-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: $fallback--text;
|
|
||||||
color: var(--inputText, --text, $fallback--text);
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 2em 0 .2em;
|
|
||||||
font-family: sans-serif;
|
|
||||||
font-family: var(--inputFont, sans-serif);
|
|
||||||
font-size: 14px;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 1;
|
|
||||||
height: 28px;
|
|
||||||
line-height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[type=range] {
|
&[type=range] {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
@ -279,7 +288,7 @@ input, textarea, .select, .input {
|
||||||
+ label::before {
|
+ label::before {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
content: '✔';
|
content: '✓';
|
||||||
transition: color 200ms;
|
transition: color 200ms;
|
||||||
width: 1.1em;
|
width: 1.1em;
|
||||||
height: 1.1em;
|
height: 1.1em;
|
||||||
|
@ -300,6 +309,10 @@ input, textarea, .select, .input {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.resize-height {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
option {
|
option {
|
||||||
|
@ -318,7 +331,7 @@ option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i[class*=icon-] {
|
i[class*=icon-], .svg-inline--fa {
|
||||||
color: $fallback--icon;
|
color: $fallback--icon;
|
||||||
color: var(--icon, $fallback--icon);
|
color: var(--icon, $fallback--icon);
|
||||||
}
|
}
|
||||||
|
@ -356,117 +369,10 @@ i[class*=icon-] {
|
||||||
padding: 0 10px 0 10px;
|
padding: 0 10px 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item {
|
|
||||||
flex: 1;
|
|
||||||
line-height: 50px;
|
|
||||||
height: 50px;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
.nav-icon {
|
|
||||||
margin-left: 0.4em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.right {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.auto-size {
|
.auto-size {
|
||||||
flex: 1
|
flex: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-bar {
|
|
||||||
padding: 0;
|
|
||||||
width: 100%;
|
|
||||||
align-items: center;
|
|
||||||
position: fixed;
|
|
||||||
height: 50px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
button {
|
|
||||||
&, i[class*=icon-] {
|
|
||||||
color: $fallback--text;
|
|
||||||
color: var(--btnTopBarText, $fallback--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: $fallback--fg;
|
|
||||||
background-color: var(--btnPressedTopBar, $fallback--fg);
|
|
||||||
color: $fallback--text;
|
|
||||||
color: var(--btnPressedTopBarText, $fallback--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
color: $fallback--text;
|
|
||||||
color: var(--btnDisabledTopBarText, $fallback--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.toggled {
|
|
||||||
color: $fallback--text;
|
|
||||||
color: var(--btnToggledTopBarText, $fallback--text);
|
|
||||||
background-color: $fallback--fg;
|
|
||||||
background-color: var(--btnToggledTopBar, $fallback--fg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
display: flex;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
|
|
||||||
align-items: stretch;
|
|
||||||
justify-content: center;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
z-index: -1;
|
|
||||||
transition: opacity;
|
|
||||||
transition-timing-function: ease-out;
|
|
||||||
transition-duration: 100ms;
|
|
||||||
|
|
||||||
.mask {
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-position: center;
|
|
||||||
mask-size: contain;
|
|
||||||
background-color: $fallback--fg;
|
|
||||||
background-color: var(--topBarText, $fallback--fg);
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
display: block;
|
|
||||||
flex: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.inner-nav {
|
|
||||||
position: relative;
|
|
||||||
margin: auto;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding-left: 10px;
|
|
||||||
padding-right: 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-basis: 970px;
|
|
||||||
height: 50px;
|
|
||||||
|
|
||||||
a, a i {
|
|
||||||
color: $fallback--link;
|
|
||||||
color: var(--topBarLink, $fallback--link);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main-router {
|
main-router {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
@ -546,6 +452,7 @@ main-router {
|
||||||
color: $fallback--faint;
|
color: $fallback--faint;
|
||||||
color: var(--panelFaint, $fallback--faint);
|
color: var(--panelFaint, $fallback--faint);
|
||||||
}
|
}
|
||||||
|
|
||||||
.faint-link {
|
.faint-link {
|
||||||
color: $fallback--faint;
|
color: $fallback--faint;
|
||||||
color: var(--faintLink, $fallback--faint);
|
color: var(--faintLink, $fallback--faint);
|
||||||
|
@ -557,11 +464,8 @@ main-router {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
.button-default,
|
||||||
flex-shrink: 0;
|
.alert {
|
||||||
}
|
|
||||||
|
|
||||||
button, .alert {
|
|
||||||
// height: 100%;
|
// height: 100%;
|
||||||
line-height: 21px;
|
line-height: 21px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
@ -572,8 +476,11 @@ main-router {
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
.button-default {
|
||||||
&, i[class*=icon-] {
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&,
|
||||||
|
i[class*=icon-] {
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--btnPanelText, $fallback--text);
|
color: var(--btnPanelText, $fallback--text);
|
||||||
}
|
}
|
||||||
|
@ -596,7 +503,8 @@ main-router {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a,
|
||||||
|
.-link {
|
||||||
color: $fallback--link;
|
color: $fallback--link;
|
||||||
color: var(--panelLink, $fallback--link)
|
color: var(--panelLink, $fallback--link)
|
||||||
}
|
}
|
||||||
|
@ -607,19 +515,31 @@ main-router {
|
||||||
border-radius: var(--panelRadius, $fallback--panelRadius);
|
border-radius: var(--panelRadius, $fallback--panelRadius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-footer {
|
/* TODO Should remove timeline-footer from here when we refactor panels into
|
||||||
|
* separate component and utilize slots
|
||||||
|
*/
|
||||||
|
.panel-footer, .timeline-footer {
|
||||||
|
display: flex;
|
||||||
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
|
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
|
||||||
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
||||||
|
flex: none;
|
||||||
|
padding: 0.6em 0.6em;
|
||||||
|
text-align: left;
|
||||||
|
line-height: 28px;
|
||||||
|
align-items: baseline;
|
||||||
|
border-width: 1px 0 0 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: var(--border, $fallback--border);
|
||||||
|
|
||||||
.faint {
|
.faint {
|
||||||
color: $fallback--faint;
|
color: $fallback--faint;
|
||||||
color: var(--panelFaint, $fallback--faint);
|
color: var(--panelFaint, $fallback--faint);
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a,
|
||||||
|
.-link {
|
||||||
color: $fallback--link;
|
color: $fallback--link;
|
||||||
color: var(--panelLink, $fallback--link)
|
color: var(--panelLink, $fallback--link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -646,12 +566,13 @@ nav {
|
||||||
color: var(--faint, $fallback--faint);
|
color: var(--faint, $fallback--faint);
|
||||||
box-shadow: 0px 0px 4px rgba(0,0,0,.6);
|
box-shadow: 0px 0px 4px rgba(0,0,0,.6);
|
||||||
box-shadow: var(--topBarShadow);
|
box-shadow: var(--topBarShadow);
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-active, .fade-leave-active {
|
.fade-enter-active, .fade-leave-active {
|
||||||
transition: opacity .2s
|
transition: opacity .2s
|
||||||
}
|
}
|
||||||
.fade-enter, .fade-leave-active {
|
.fade-enter-from, .fade-leave-active {
|
||||||
opacity: 0
|
opacity: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -707,19 +628,24 @@ nav {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
|
box-sizing: border-box;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-radius: 99px;
|
border-radius: 99px;
|
||||||
min-width: 22px;
|
max-width: 10em;
|
||||||
max-width: 22px;
|
min-width: 1.7em;
|
||||||
min-height: 22px;
|
height: 1.3em;
|
||||||
max-height: 22px;
|
padding: 0.15em 0.15em;
|
||||||
font-size: 15px;
|
|
||||||
line-height: 22px;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 0.9em;
|
||||||
|
line-height: 1;
|
||||||
|
text-align: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding: 0;
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
&.badge-notification {
|
&.badge-notification {
|
||||||
background-color: $fallback--cRed;
|
background-color: $fallback--cRed;
|
||||||
|
@ -760,6 +686,15 @@ nav {
|
||||||
color: var(--alertWarningPanelText, $fallback--text);
|
color: var(--alertWarningPanelText, $fallback--text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.success {
|
||||||
|
background-color: var(--alertSuccess, $fallback--alertWarning);
|
||||||
|
color: var(--alertSuccessText, $fallback--text);
|
||||||
|
|
||||||
|
.panel-heading & {
|
||||||
|
color: var(--alertSuccessPanelText, $fallback--text);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.faint {
|
.faint {
|
||||||
|
@ -776,16 +711,6 @@ nav {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and (min-width: 800px) {
|
|
||||||
.logo {
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.item.right {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.visibility-notice {
|
.visibility-notice {
|
||||||
padding: .5em;
|
padding: .5em;
|
||||||
border: 1px solid $fallback--faint;
|
border: 1px solid $fallback--faint;
|
||||||
|
@ -807,8 +732,16 @@ nav {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-icon {
|
.fa-scale-110 {
|
||||||
font-size: 1.2em;
|
&.svg-inline--fa {
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-old-padding {
|
||||||
|
&.svg-inline--fa {
|
||||||
|
padding: 0 0.3em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shakeError {
|
@keyframes shakeError {
|
||||||
|
@ -865,13 +798,6 @@ nav {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-multiple {
|
|
||||||
display: flex;
|
|
||||||
.option-list {
|
|
||||||
margin: 0;
|
|
||||||
padding-left: .5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setting-list,
|
.setting-list,
|
||||||
.option-list{
|
.option-list{
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
|
@ -898,7 +824,7 @@ nav {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.btn-default {
|
.btn.button-default {
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -918,29 +844,10 @@ nav {
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-status-notification {
|
.new-status-notification {
|
||||||
position:relative;
|
position: relative;
|
||||||
margin-top: -1px;
|
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
border-width: 1px 0 0 0;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: var(--border, $fallback--border);
|
|
||||||
padding: 10px;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
background-color: $fallback--fg;
|
flex: 1;
|
||||||
background-color: var(--panel, $fallback--fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.unread-chat-count {
|
|
||||||
font-size: 0.9em;
|
|
||||||
font-weight: bolder;
|
|
||||||
font-style: normal;
|
|
||||||
position: absolute;
|
|
||||||
right: 0.6rem;
|
|
||||||
padding: 0 0.3em;
|
|
||||||
min-width: 1.3rem;
|
|
||||||
min-height: 1.3rem;
|
|
||||||
max-height: 1.3rem;
|
|
||||||
line-height: 1.3rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-layout {
|
.chat-layout {
|
||||||
|
@ -948,6 +855,11 @@ nav {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
// Get rid of scrollbar on body as scrolling happens on different element
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
// Ensures the fixed position of the mobile browser bars on scroll up / down events.
|
// Ensures the fixed position of the mobile browser bars on scroll up / down events.
|
||||||
// Prevents the mobile browser bars from overlapping or hiding the message posting form.
|
// Prevents the mobile browser bars from overlapping or hiding the message posting form.
|
||||||
@media all and (max-width: 800px) {
|
@media all and (max-width: 800px) {
|
||||||
|
|
83
src/App.vue
83
src/App.vue
|
@ -1,82 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
id="app"
|
id="app-loaded"
|
||||||
:style="bgAppStyle"
|
:style="bgStyle"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
id="app_bg_wrapper"
|
id="app_bg_wrapper"
|
||||||
class="app-bg-wrapper"
|
class="app-bg-wrapper"
|
||||||
:style="bgStyle"
|
|
||||||
/>
|
/>
|
||||||
<MobileNav v-if="isMobileLayout" />
|
<MobileNav v-if="isMobileLayout" />
|
||||||
<nav
|
<DesktopNav v-else />
|
||||||
v-else
|
|
||||||
id="nav"
|
|
||||||
class="nav-bar container"
|
|
||||||
@click="scrollToTop()"
|
|
||||||
>
|
|
||||||
<div class="inner-nav">
|
|
||||||
<div
|
|
||||||
class="logo"
|
|
||||||
:style="logoBgStyle"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mask"
|
|
||||||
:style="logoMaskStyle"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
:src="logo"
|
|
||||||
:style="logoStyle"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<router-link
|
|
||||||
v-if="!hideSitename"
|
|
||||||
class="site-name"
|
|
||||||
:to="{ name: 'root' }"
|
|
||||||
active-class="home"
|
|
||||||
>
|
|
||||||
{{ sitename }}
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
<div class="item right">
|
|
||||||
<search-bar
|
|
||||||
v-if="currentUser || !privateMode"
|
|
||||||
class="nav-icon mobile-hidden"
|
|
||||||
@toggled="onSearchBarToggled"
|
|
||||||
@click.stop.native
|
|
||||||
/>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
class="mobile-hidden"
|
|
||||||
@click.stop="openSettingsModal"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="button-icon icon-cog nav-icon"
|
|
||||||
:title="$t('nav.preferences')"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
v-if="currentUser && currentUser.role === 'admin'"
|
|
||||||
href="/pleroma/admin/#/login-pleroma"
|
|
||||||
class="mobile-hidden"
|
|
||||||
target="_blank"
|
|
||||||
><i
|
|
||||||
class="button-icon icon-gauge nav-icon"
|
|
||||||
:title="$t('nav.administration')"
|
|
||||||
/></a>
|
|
||||||
<a
|
|
||||||
v-if="currentUser"
|
|
||||||
href="#"
|
|
||||||
class="mobile-hidden"
|
|
||||||
@click.prevent="logout"
|
|
||||||
><i
|
|
||||||
class="button-icon icon-logout nav-icon"
|
|
||||||
:title="$t('login.logout')"
|
|
||||||
/></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
<div class="app-bg-wrapper app-container-wrapper" />
|
<div class="app-bg-wrapper app-container-wrapper" />
|
||||||
<div
|
<div
|
||||||
id="content"
|
id="content"
|
||||||
|
@ -117,16 +49,17 @@
|
||||||
</div>
|
</div>
|
||||||
<media-modal />
|
<media-modal />
|
||||||
</div>
|
</div>
|
||||||
<chat-panel
|
<shout-panel
|
||||||
v-if="currentUser && chat"
|
v-if="currentUser && shout && !hideShoutbox"
|
||||||
:floating="true"
|
:floating="true"
|
||||||
class="floating-chat mobile-hidden"
|
class="floating-shout mobile-hidden"
|
||||||
|
:class="{ 'left': shoutboxPosition }"
|
||||||
/>
|
/>
|
||||||
<MobilePostStatusButton />
|
<MobilePostStatusButton />
|
||||||
<UserReportingModal />
|
<UserReportingModal />
|
||||||
<PostStatusModal />
|
<PostStatusModal />
|
||||||
<SettingsModal />
|
<SettingsModal />
|
||||||
<portal-target name="modal" />
|
<div id="modal" />
|
||||||
<GlobalNoticeList />
|
<GlobalNoticeList />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -30,3 +30,5 @@ $fallback--attachmentRadius: 10px;
|
||||||
$fallback--chatMessageRadius: 10px;
|
$fallback--chatMessageRadius: 10px;
|
||||||
|
|
||||||
$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||||
|
|
||||||
|
$status-margin: 0.75em;
|
||||||
|
|
|
@ -1,12 +1,19 @@
|
||||||
import Vue from 'vue'
|
import { createApp } from 'vue'
|
||||||
import VueRouter from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import routes from './routes'
|
import vClickOutside from 'click-outside-vue3'
|
||||||
|
|
||||||
|
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||||
|
|
||||||
import App from '../App.vue'
|
import App from '../App.vue'
|
||||||
|
import routes from './routes'
|
||||||
|
import VBodyScrollLock from 'src/directives/body_scroll_lock'
|
||||||
|
|
||||||
import { windowWidth } from '../services/window_utils/window_utils'
|
import { windowWidth } from '../services/window_utils/window_utils'
|
||||||
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
|
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
|
||||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||||
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
|
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
|
||||||
import { applyTheme } from '../services/style_setter/style_setter.js'
|
import { applyTheme } from '../services/style_setter/style_setter.js'
|
||||||
|
import FaviconService from '../services/favicon_service/favicon_service.js'
|
||||||
|
|
||||||
let staticInitialResults = null
|
let staticInitialResults = null
|
||||||
|
|
||||||
|
@ -50,6 +57,7 @@ const getInstanceConfig = async ({ store }) => {
|
||||||
const vapidPublicKey = data.pleroma.vapid_public_key
|
const vapidPublicKey = data.pleroma.vapid_public_key
|
||||||
|
|
||||||
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
|
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
|
||||||
|
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required })
|
||||||
|
|
||||||
if (vapidPublicKey) {
|
if (vapidPublicKey) {
|
||||||
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
||||||
|
@ -113,6 +121,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
||||||
copyInstanceOption('nsfwCensorImage')
|
copyInstanceOption('nsfwCensorImage')
|
||||||
copyInstanceOption('background')
|
copyInstanceOption('background')
|
||||||
copyInstanceOption('hidePostStats')
|
copyInstanceOption('hidePostStats')
|
||||||
|
copyInstanceOption('hideBotIndication')
|
||||||
copyInstanceOption('hideUserStats')
|
copyInstanceOption('hideUserStats')
|
||||||
copyInstanceOption('hideFilteredStatuses')
|
copyInstanceOption('hideFilteredStatuses')
|
||||||
copyInstanceOption('logo')
|
copyInstanceOption('logo')
|
||||||
|
@ -130,6 +139,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
||||||
? 0
|
? 0
|
||||||
: config.logoMargin
|
: config.logoMargin
|
||||||
})
|
})
|
||||||
|
copyInstanceOption('logoLeft')
|
||||||
store.commit('authFlow/setInitialStrategy', config.loginMethod)
|
store.commit('authFlow/setInitialStrategy', config.loginMethod)
|
||||||
|
|
||||||
copyInstanceOption('redirectRootNoLogin')
|
copyInstanceOption('redirectRootNoLogin')
|
||||||
|
@ -237,7 +247,7 @@ const getNodeInfo = async ({ store }) => {
|
||||||
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations })
|
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations })
|
||||||
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
|
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
|
||||||
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
|
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
|
||||||
store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') })
|
store.dispatch('setInstanceOption', { name: 'shoutAvailable', value: features.includes('chat') })
|
||||||
store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
|
store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
|
||||||
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
||||||
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
||||||
|
@ -325,6 +335,8 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
||||||
const width = windowWidth()
|
const width = windowWidth()
|
||||||
store.dispatch('setMobileLayout', width <= 800)
|
store.dispatch('setMobileLayout', width <= 800)
|
||||||
|
|
||||||
|
FaviconService.initFaviconService()
|
||||||
|
|
||||||
const overrides = window.___pleromafe_dev_overrides || {}
|
const overrides = window.___pleromafe_dev_overrides || {}
|
||||||
const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin
|
const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin
|
||||||
store.dispatch('setInstanceOption', { name: 'server', value: server })
|
store.dispatch('setInstanceOption', { name: 'server', value: server })
|
||||||
|
@ -361,25 +373,32 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
||||||
getTOS({ store })
|
getTOS({ store })
|
||||||
getStickers({ store })
|
getStickers({ store })
|
||||||
|
|
||||||
const router = new VueRouter({
|
const router = createRouter({
|
||||||
mode: 'history',
|
history: createWebHistory(),
|
||||||
routes: routes(store),
|
routes: routes(store),
|
||||||
scrollBehavior: (to, _from, savedPosition) => {
|
scrollBehavior: (to, _from, savedPosition) => {
|
||||||
if (to.matched.some(m => m.meta.dontScroll)) {
|
if (to.matched.some(m => m.meta.dontScroll)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return savedPosition || { x: 0, y: 0 }
|
return savedPosition || { left: 0, top: 0 }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/* eslint-disable no-new */
|
const app = createApp(App)
|
||||||
return new Vue({
|
|
||||||
router,
|
app.use(router)
|
||||||
store,
|
app.use(store)
|
||||||
i18n,
|
app.use(i18n)
|
||||||
el: '#app',
|
|
||||||
render: h => h(App)
|
app.use(vClickOutside)
|
||||||
})
|
app.use(VBodyScrollLock)
|
||||||
|
|
||||||
|
app.component('FAIcon', FontAwesomeIcon)
|
||||||
|
app.component('FALayers', FontAwesomeLayers)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
|
|
||||||
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
export default afterStoreSetup
|
export default afterStoreSetup
|
||||||
|
|
|
@ -16,7 +16,7 @@ import FollowRequests from 'components/follow_requests/follow_requests.vue'
|
||||||
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
|
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
|
||||||
import Notifications from 'components/notifications/notifications.vue'
|
import Notifications from 'components/notifications/notifications.vue'
|
||||||
import AuthForm from 'components/auth_form/auth_form.js'
|
import AuthForm from 'components/auth_form/auth_form.js'
|
||||||
import ChatPanel from 'components/chat_panel/chat_panel.vue'
|
import ShoutPanel from 'components/shout_panel/shout_panel.vue'
|
||||||
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
|
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
|
||||||
import About from 'components/about/about.vue'
|
import About from 'components/about/about.vue'
|
||||||
import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue'
|
import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue'
|
||||||
|
@ -46,7 +46,7 @@ export default (store) => {
|
||||||
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
|
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
|
||||||
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
||||||
{ name: 'remote-user-profile-acct',
|
{ name: 'remote-user-profile-acct',
|
||||||
path: '/remote-users/(@?):username([^/@]+)@:hostname([^/@]+)',
|
path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)',
|
||||||
component: RemoteUserResolver,
|
component: RemoteUserResolver,
|
||||||
beforeEnter: validateAuthenticatedRoute
|
beforeEnter: validateAuthenticatedRoute
|
||||||
},
|
},
|
||||||
|
@ -64,12 +64,12 @@ export default (store) => {
|
||||||
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'login', path: '/login', component: AuthForm },
|
{ name: 'login', path: '/login', component: AuthForm },
|
||||||
{ name: 'chat-panel', path: '/chat-panel', component: ChatPanel, props: () => ({ floating: false }) },
|
{ name: 'shout-panel', path: '/shout-panel', component: ShoutPanel, props: () => ({ floating: false }) },
|
||||||
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
|
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
|
||||||
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
|
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
|
||||||
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'about', path: '/about', component: About },
|
{ name: 'about', path: '/about', component: About },
|
||||||
{ name: 'user-profile', path: '/(users/)?:name', component: UserProfile }
|
{ name: 'user-profile', path: '/:_(users)?/:name', component: UserProfile }
|
||||||
]
|
]
|
||||||
|
|
||||||
if (store.state.instance.pleromaChatMessagesAvailable) {
|
if (store.state.instance.pleromaChatMessagesAvailable) {
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import ProgressButton from '../progress_button/progress_button.vue'
|
import ProgressButton from '../progress_button/progress_button.vue'
|
||||||
import Popover from '../popover/popover.vue'
|
import Popover from '../popover/popover.vue'
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faEllipsisV
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faEllipsisV
|
||||||
|
)
|
||||||
|
|
||||||
const AccountActions = {
|
const AccountActions = {
|
||||||
props: [
|
props: [
|
||||||
|
@ -27,7 +35,7 @@ const AccountActions = {
|
||||||
this.$store.dispatch('unblockUser', this.user.id)
|
this.$store.dispatch('unblockUser', this.user.id)
|
||||||
},
|
},
|
||||||
reportUser () {
|
reportUser () {
|
||||||
this.$store.dispatch('openUserReportingModal', this.user.id)
|
this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
|
||||||
},
|
},
|
||||||
openChat () {
|
openChat () {
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
|
|
|
@ -1,26 +1,24 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="account-actions">
|
<div class="AccountActions">
|
||||||
<Popover
|
<Popover
|
||||||
trigger="click"
|
trigger="click"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
:bound-to="{ x: 'container' }"
|
:bound-to="{ x: 'container' }"
|
||||||
|
remove-padding
|
||||||
>
|
>
|
||||||
<div
|
<template v-slot:content>
|
||||||
slot="content"
|
|
||||||
class="account-tools-popover"
|
|
||||||
>
|
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<template v-if="relationship.following">
|
<template v-if="relationship.following">
|
||||||
<button
|
<button
|
||||||
v-if="relationship.showing_reblogs"
|
v-if="relationship.showing_reblogs"
|
||||||
class="btn btn-default dropdown-item"
|
class="btn button-default dropdown-item"
|
||||||
@click="hideRepeats"
|
@click="hideRepeats"
|
||||||
>
|
>
|
||||||
{{ $t('user_card.hide_repeats') }}
|
{{ $t('user_card.hide_repeats') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="!relationship.showing_reblogs"
|
v-if="!relationship.showing_reblogs"
|
||||||
class="btn btn-default dropdown-item"
|
class="btn button-default dropdown-item"
|
||||||
@click="showRepeats"
|
@click="showRepeats"
|
||||||
>
|
>
|
||||||
{{ $t('user_card.show_repeats') }}
|
{{ $t('user_card.show_repeats') }}
|
||||||
|
@ -32,39 +30,41 @@
|
||||||
</template>
|
</template>
|
||||||
<button
|
<button
|
||||||
v-if="relationship.blocking"
|
v-if="relationship.blocking"
|
||||||
class="btn btn-default btn-block dropdown-item"
|
class="btn button-default btn-block dropdown-item"
|
||||||
@click="unblockUser"
|
@click="unblockUser"
|
||||||
>
|
>
|
||||||
{{ $t('user_card.unblock') }}
|
{{ $t('user_card.unblock') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-else
|
v-else
|
||||||
class="btn btn-default btn-block dropdown-item"
|
class="btn button-default btn-block dropdown-item"
|
||||||
@click="blockUser"
|
@click="blockUser"
|
||||||
>
|
>
|
||||||
{{ $t('user_card.block') }}
|
{{ $t('user_card.block') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-default btn-block dropdown-item"
|
class="btn button-default btn-block dropdown-item"
|
||||||
@click="reportUser"
|
@click="reportUser"
|
||||||
>
|
>
|
||||||
{{ $t('user_card.report') }}
|
{{ $t('user_card.report') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="pleromaChatMessagesAvailable"
|
v-if="pleromaChatMessagesAvailable"
|
||||||
class="btn btn-default btn-block dropdown-item"
|
class="btn button-default btn-block dropdown-item"
|
||||||
@click="openChat"
|
@click="openChat"
|
||||||
>
|
>
|
||||||
{{ $t('user_card.message') }}
|
{{ $t('user_card.message') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<div
|
<template v-slot:trigger>
|
||||||
slot="trigger"
|
<button class="button-unstyled ellipsis-button">
|
||||||
class="btn btn-default ellipsis-button"
|
<FAIcon
|
||||||
>
|
class="icon"
|
||||||
<i class="icon-ellipsis trigger-button" />
|
icon="ellipsis-v"
|
||||||
</div>
|
/>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -73,22 +73,21 @@
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
.account-actions {
|
.AccountActions {
|
||||||
margin: 0 .8em;
|
button.dropdown-item {
|
||||||
}
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.account-actions button.dropdown-item {
|
.ellipsis-button {
|
||||||
margin-left: 0;
|
width: 2.5em;
|
||||||
}
|
margin: -0.5em 0;
|
||||||
|
padding: 0.5em 0;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
.account-actions .trigger-button {
|
&:not(:hover) .icon {
|
||||||
color: $fallback--lightText;
|
color: $fallback--lightText;
|
||||||
color: var(--lightText, $fallback--lightText);
|
color: var(--lightText, $fallback--lightText);
|
||||||
opacity: .8;
|
}
|
||||||
cursor: pointer;
|
|
||||||
&:hover {
|
|
||||||
color: $fallback--text;
|
|
||||||
color: var(--text, $fallback--text);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
{{ $t('general.error_retry') }}
|
{{ $t('general.error_retry') }}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
class="btn"
|
class="btn button-default"
|
||||||
@click="retry"
|
@click="retry"
|
||||||
>
|
>
|
||||||
{{ $t('general.retry') }}
|
{{ $t('general.retry') }}
|
||||||
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
emits: ['resetAsyncComponent'],
|
||||||
methods: {
|
methods: {
|
||||||
retry () {
|
retry () {
|
||||||
this.$emit('resetAsyncComponent')
|
this.$emit('resetAsyncComponent')
|
||||||
|
|
|
@ -1,36 +1,88 @@
|
||||||
import StillImage from '../still-image/still-image.vue'
|
import StillImage from '../still-image/still-image.vue'
|
||||||
|
import Flash from '../flash/flash.vue'
|
||||||
import VideoAttachment from '../video_attachment/video_attachment.vue'
|
import VideoAttachment from '../video_attachment/video_attachment.vue'
|
||||||
import nsfwImage from '../../assets/nsfw.png'
|
import nsfwImage from '../../assets/nsfw.png'
|
||||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faFile,
|
||||||
|
faMusic,
|
||||||
|
faImage,
|
||||||
|
faVideo,
|
||||||
|
faPlayCircle,
|
||||||
|
faTimes,
|
||||||
|
faStop,
|
||||||
|
faSearchPlus,
|
||||||
|
faTrashAlt,
|
||||||
|
faPencilAlt,
|
||||||
|
faAlignRight
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faFile,
|
||||||
|
faMusic,
|
||||||
|
faImage,
|
||||||
|
faVideo,
|
||||||
|
faPlayCircle,
|
||||||
|
faTimes,
|
||||||
|
faStop,
|
||||||
|
faSearchPlus,
|
||||||
|
faTrashAlt,
|
||||||
|
faPencilAlt,
|
||||||
|
faAlignRight
|
||||||
|
)
|
||||||
|
|
||||||
const Attachment = {
|
const Attachment = {
|
||||||
props: [
|
props: [
|
||||||
'attachment',
|
'attachment',
|
||||||
|
'description',
|
||||||
|
'hideDescription',
|
||||||
'nsfw',
|
'nsfw',
|
||||||
'size',
|
'size',
|
||||||
'allowPlay',
|
|
||||||
'setMedia',
|
'setMedia',
|
||||||
'naturalSizeLoad'
|
'remove',
|
||||||
|
'shiftUp',
|
||||||
|
'shiftDn',
|
||||||
|
'edit'
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
localDescription: this.description || this.attachment.description,
|
||||||
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
|
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
|
||||||
hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
|
hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
|
||||||
preloadImage: this.$store.getters.mergedConfig.preloadImage,
|
preloadImage: this.$store.getters.mergedConfig.preloadImage,
|
||||||
loading: false,
|
loading: false,
|
||||||
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
|
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
|
||||||
modalOpen: false,
|
modalOpen: false,
|
||||||
showHidden: false
|
showHidden: false,
|
||||||
|
flashLoaded: false,
|
||||||
|
showDescription: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
Flash,
|
||||||
StillImage,
|
StillImage,
|
||||||
VideoAttachment
|
VideoAttachment
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
classNames () {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'-loading': this.loading,
|
||||||
|
'-nsfw-placeholder': this.hidden,
|
||||||
|
'-editable': this.edit !== undefined
|
||||||
|
},
|
||||||
|
'-type-' + this.type,
|
||||||
|
this.size && '-size-' + this.size,
|
||||||
|
`-${this.useContainFit ? 'contain' : 'cover'}-fit`
|
||||||
|
]
|
||||||
|
},
|
||||||
usePlaceholder () {
|
usePlaceholder () {
|
||||||
return this.size === 'hide' || this.type === 'unknown'
|
return this.size === 'hide'
|
||||||
|
},
|
||||||
|
useContainFit () {
|
||||||
|
return this.$store.getters.mergedConfig.useContainFit
|
||||||
},
|
},
|
||||||
placeholderName () {
|
placeholderName () {
|
||||||
if (this.attachment.description === '' || !this.attachment.description) {
|
if (this.attachment.description === '' || !this.attachment.description) {
|
||||||
|
@ -39,10 +91,10 @@ const Attachment = {
|
||||||
return this.attachment.description
|
return this.attachment.description
|
||||||
},
|
},
|
||||||
placeholderIconClass () {
|
placeholderIconClass () {
|
||||||
if (this.type === 'image') return 'icon-picture'
|
if (this.type === 'image') return 'image'
|
||||||
if (this.type === 'video') return 'icon-video'
|
if (this.type === 'video') return 'video'
|
||||||
if (this.type === 'audio') return 'icon-music'
|
if (this.type === 'audio') return 'music'
|
||||||
return 'icon-doc'
|
return 'file'
|
||||||
},
|
},
|
||||||
referrerpolicy () {
|
referrerpolicy () {
|
||||||
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
|
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
|
||||||
|
@ -54,24 +106,33 @@ const Attachment = {
|
||||||
return this.nsfw && this.hideNsfwLocal && !this.showHidden
|
return this.nsfw && this.hideNsfwLocal && !this.showHidden
|
||||||
},
|
},
|
||||||
isEmpty () {
|
isEmpty () {
|
||||||
return (this.type === 'html' && !this.attachment.oembed) || this.type === 'unknown'
|
return (this.type === 'html' && !this.attachment.oembed)
|
||||||
},
|
|
||||||
isSmall () {
|
|
||||||
return this.size === 'small'
|
|
||||||
},
|
|
||||||
fullwidth () {
|
|
||||||
if (this.size === 'hide') return false
|
|
||||||
return this.type === 'html' || this.type === 'audio' || this.type === 'unknown'
|
|
||||||
},
|
},
|
||||||
useModal () {
|
useModal () {
|
||||||
const modalTypes = this.size === 'hide' ? ['image', 'video', 'audio']
|
let modalTypes = []
|
||||||
: this.mergedConfig.playVideosInModal
|
switch (this.size) {
|
||||||
? ['image', 'video']
|
case 'hide':
|
||||||
: ['image']
|
case 'small':
|
||||||
|
modalTypes = ['image', 'video', 'audio', 'flash']
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
modalTypes = this.mergedConfig.playVideosInModal
|
||||||
|
? ['image', 'video', 'flash']
|
||||||
|
: ['image']
|
||||||
|
break
|
||||||
|
}
|
||||||
return modalTypes.includes(this.type)
|
return modalTypes.includes(this.type)
|
||||||
},
|
},
|
||||||
|
videoTag () {
|
||||||
|
return this.useModal ? 'button' : 'span'
|
||||||
|
},
|
||||||
...mapGetters(['mergedConfig'])
|
...mapGetters(['mergedConfig'])
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
localDescription (newVal) {
|
||||||
|
this.onEdit(newVal)
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
linkClicked ({ target }) {
|
linkClicked ({ target }) {
|
||||||
if (target.tagName === 'A') {
|
if (target.tagName === 'A') {
|
||||||
|
@ -80,12 +141,37 @@ const Attachment = {
|
||||||
},
|
},
|
||||||
openModal (event) {
|
openModal (event) {
|
||||||
if (this.useModal) {
|
if (this.useModal) {
|
||||||
event.stopPropagation()
|
this.$emit('setMedia')
|
||||||
event.preventDefault()
|
this.$store.dispatch('setCurrentMedia', this.attachment)
|
||||||
this.setMedia()
|
} else if (this.type === 'unknown') {
|
||||||
this.$store.dispatch('setCurrent', this.attachment)
|
window.open(this.attachment.url)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
openModalForce (event) {
|
||||||
|
this.$emit('setMedia')
|
||||||
|
this.$store.dispatch('setCurrentMedia', this.attachment)
|
||||||
|
},
|
||||||
|
onEdit (event) {
|
||||||
|
this.edit && this.edit(this.attachment, event)
|
||||||
|
},
|
||||||
|
onRemove () {
|
||||||
|
this.remove && this.remove(this.attachment)
|
||||||
|
},
|
||||||
|
onShiftUp () {
|
||||||
|
this.shiftUp && this.shiftUp(this.attachment)
|
||||||
|
},
|
||||||
|
onShiftDn () {
|
||||||
|
this.shiftDn && this.shiftDn(this.attachment)
|
||||||
|
},
|
||||||
|
stopFlash () {
|
||||||
|
this.$refs.flash.closePlayer()
|
||||||
|
},
|
||||||
|
setFlashLoaded (event) {
|
||||||
|
this.flashLoaded = event
|
||||||
|
},
|
||||||
|
toggleDescription () {
|
||||||
|
this.showDescription = !this.showDescription
|
||||||
|
},
|
||||||
toggleHidden (event) {
|
toggleHidden (event) {
|
||||||
if (
|
if (
|
||||||
(this.mergedConfig.useOneClickNsfw && !this.showHidden) &&
|
(this.mergedConfig.useOneClickNsfw && !this.showHidden) &&
|
||||||
|
@ -112,7 +198,7 @@ const Attachment = {
|
||||||
onImageLoad (image) {
|
onImageLoad (image) {
|
||||||
const width = image.naturalWidth
|
const width = image.naturalWidth
|
||||||
const height = image.naturalHeight
|
const height = image.naturalHeight
|
||||||
this.naturalSizeLoad && this.naturalSizeLoad({ width, height })
|
this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
268
src/components/attachment/attachment.scss
Normal file
268
src/components/attachment/attachment.scss
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.Attachment {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
align-self: flex-start;
|
||||||
|
line-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
border-radius: $fallback--attachmentRadius;
|
||||||
|
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
|
||||||
|
border-color: $fallback--border;
|
||||||
|
border-color: var(--border, $fallback--border);
|
||||||
|
|
||||||
|
.attachment-wrapper {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-container {
|
||||||
|
flex: 0 1 0;
|
||||||
|
display: flex;
|
||||||
|
padding-top: 0.5em;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
p {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 0.5em;
|
||||||
|
margin: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-static {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
background: var(--popover);
|
||||||
|
box-shadow: var(--popupShadow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-field {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .placeholder-container,
|
||||||
|
& .image-container,
|
||||||
|
& .audio-container,
|
||||||
|
& .video-container,
|
||||||
|
& .flash-container,
|
||||||
|
& .oembed-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
.image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .flash-container,
|
||||||
|
& .video-container {
|
||||||
|
& .flash,
|
||||||
|
& video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
|
||||||
|
audio {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.play-icon {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 64px;
|
||||||
|
top: calc(50% - 32px);
|
||||||
|
left: calc(50% - 32px);
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
text-shadow: 0 0 2px rgba(0, 0, 0, 0.4);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-buttons {
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
.attachment-button {
|
||||||
|
padding: 0;
|
||||||
|
border-radius: $fallback--tooltipRadius;
|
||||||
|
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||||
|
text-align: center;
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
font-size: 1.25em;
|
||||||
|
// TODO: theming? hard to theme with unknown background image color
|
||||||
|
background: rgba(230, 230, 230, 0.7);
|
||||||
|
|
||||||
|
.svg-inline--fa {
|
||||||
|
color: rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .svg-inline--fa {
|
||||||
|
color: rgba(0, 0, 0, 0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.oembed-container {
|
||||||
|
line-height: 1.2em;
|
||||||
|
flex: 1 0 100%;
|
||||||
|
width: 100%;
|
||||||
|
margin-right: 15px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
flex: 1;
|
||||||
|
img {
|
||||||
|
border: 0px;
|
||||||
|
border-radius: 5px;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
flex: 2;
|
||||||
|
margin: 8px;
|
||||||
|
word-break: break-all;
|
||||||
|
h1 {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-size-small {
|
||||||
|
.play-icon {
|
||||||
|
zoom: 0.5;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-buttons {
|
||||||
|
zoom: 0.7;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-editable {
|
||||||
|
padding: 0.5em;
|
||||||
|
|
||||||
|
& .description-container,
|
||||||
|
& .attachment-buttons {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-placeholder {
|
||||||
|
display: inline-block;
|
||||||
|
color: $fallback--link;
|
||||||
|
color: var(--postLink, $fallback--link);
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
height: auto;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
&:not(.-editable) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-editable {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
& .description-container,
|
||||||
|
& .attachment-buttons {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-container {
|
||||||
|
flex: 1;
|
||||||
|
padding-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-buttons {
|
||||||
|
order: 99;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-loading {
|
||||||
|
cursor: progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-contain-fit {
|
||||||
|
img,
|
||||||
|
canvas {
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-cover-fit {
|
||||||
|
img,
|
||||||
|
canvas {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<button
|
||||||
v-if="usePlaceholder"
|
v-if="usePlaceholder"
|
||||||
:class="{ 'fullwidth': fullwidth }"
|
class="Attachment -placeholder button-unstyled"
|
||||||
|
:class="classNames"
|
||||||
@click="openModal"
|
@click="openModal"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
|
@ -11,296 +12,257 @@
|
||||||
:href="attachment.url"
|
:href="attachment.url"
|
||||||
:alt="attachment.description"
|
:alt="attachment.description"
|
||||||
:title="attachment.description"
|
:title="attachment.description"
|
||||||
|
@click.prevent
|
||||||
>
|
>
|
||||||
<span :class="placeholderIconClass" />
|
<FAIcon :icon="placeholderIconClass" />
|
||||||
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ placeholderName }}
|
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ edit ? '' : placeholderName }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
<div
|
||||||
|
v-if="edit || remove"
|
||||||
|
class="attachment-buttons"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="remove"
|
||||||
|
class="button-unstyled attachment-button"
|
||||||
|
@click.prevent="onRemove"
|
||||||
|
>
|
||||||
|
<FAIcon icon="trash-alt" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="size !== 'hide' && !hideDescription && (edit || localDescription || showDescription)"
|
||||||
|
class="description-container"
|
||||||
|
:class="{ '-static': !edit }"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-if="edit"
|
||||||
|
v-model="localDescription"
|
||||||
|
type="text"
|
||||||
|
class="description-field"
|
||||||
|
:placeholder="$t('post_status.media_description')"
|
||||||
|
@keydown.enter.prevent=""
|
||||||
|
>
|
||||||
|
<p v-else>
|
||||||
|
{{ localDescription }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
v-show="!isEmpty"
|
class="Attachment"
|
||||||
class="attachment"
|
:class="classNames"
|
||||||
:class="{[type]: true, loading, 'fullwidth': fullwidth, 'nsfw-placeholder': hidden}"
|
|
||||||
>
|
>
|
||||||
<a
|
|
||||||
v-if="hidden"
|
|
||||||
class="image-attachment"
|
|
||||||
:href="attachment.url"
|
|
||||||
:alt="attachment.description"
|
|
||||||
:title="attachment.description"
|
|
||||||
@click.prevent="toggleHidden"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
:key="nsfwImage"
|
|
||||||
class="nsfw"
|
|
||||||
:src="nsfwImage"
|
|
||||||
:class="{'small': isSmall}"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
v-if="type === 'video'"
|
|
||||||
class="play-icon icon-play-circled"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<div
|
<div
|
||||||
v-if="nsfw && hideNsfwLocal && !hidden"
|
v-show="!isEmpty"
|
||||||
class="hider"
|
class="attachment-wrapper"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="#"
|
v-if="hidden"
|
||||||
@click.prevent="toggleHidden"
|
class="image-container"
|
||||||
>Hide</a>
|
:href="attachment.url"
|
||||||
</div>
|
|
||||||
|
|
||||||
<a
|
|
||||||
v-if="type === 'image' && (!hidden || preloadImage)"
|
|
||||||
class="image-attachment"
|
|
||||||
:class="{'hidden': hidden && preloadImage }"
|
|
||||||
:href="attachment.url"
|
|
||||||
target="_blank"
|
|
||||||
@click="openModal"
|
|
||||||
>
|
|
||||||
<StillImage
|
|
||||||
class="image"
|
|
||||||
:referrerpolicy="referrerpolicy"
|
|
||||||
:mimetype="attachment.mimetype"
|
|
||||||
:src="attachment.large_thumb_url || attachment.url"
|
|
||||||
:image-load-handler="onImageLoad"
|
|
||||||
:alt="attachment.description"
|
:alt="attachment.description"
|
||||||
/>
|
:title="attachment.description"
|
||||||
</a>
|
@click.prevent.stop="toggleHidden"
|
||||||
|
|
||||||
<a
|
|
||||||
v-if="type === 'video' && !hidden"
|
|
||||||
class="video-container"
|
|
||||||
:class="{'small': isSmall}"
|
|
||||||
:href="allowPlay ? undefined : attachment.url"
|
|
||||||
@click="openModal"
|
|
||||||
>
|
|
||||||
<VideoAttachment
|
|
||||||
class="video"
|
|
||||||
:attachment="attachment"
|
|
||||||
:controls="allowPlay"
|
|
||||||
/>
|
|
||||||
<i
|
|
||||||
v-if="!allowPlay"
|
|
||||||
class="play-icon icon-play-circled"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<audio
|
|
||||||
v-if="type === 'audio'"
|
|
||||||
:src="attachment.url"
|
|
||||||
:alt="attachment.description"
|
|
||||||
:title="attachment.description"
|
|
||||||
controls
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="type === 'html' && attachment.oembed"
|
|
||||||
class="oembed"
|
|
||||||
@click.prevent="linkClicked"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="attachment.thumb_url"
|
|
||||||
class="image"
|
|
||||||
>
|
>
|
||||||
<img :src="attachment.thumb_url">
|
<img
|
||||||
|
:key="nsfwImage"
|
||||||
|
class="nsfw"
|
||||||
|
:src="nsfwImage"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
v-if="type === 'video'"
|
||||||
|
class="play-icon"
|
||||||
|
icon="play-circle"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<div
|
||||||
|
v-if="!hidden"
|
||||||
|
class="attachment-buttons"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="type === 'flash' && flashLoaded"
|
||||||
|
class="button-unstyled attachment-button"
|
||||||
|
:title="$t('status.attachment_stop_flash')"
|
||||||
|
@click.prevent="stopFlash"
|
||||||
|
>
|
||||||
|
<FAIcon icon="stop" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="attachment.description && size !== 'small' && !edit && type !== 'unknown'"
|
||||||
|
class="button-unstyled attachment-button"
|
||||||
|
:title="$t('status.show_attachment_description')"
|
||||||
|
@click.prevent="toggleDescription"
|
||||||
|
>
|
||||||
|
<FAIcon icon="align-right" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="!useModal && type !== 'unknown'"
|
||||||
|
class="button-unstyled attachment-button"
|
||||||
|
:title="$t('status.show_attachment_in_modal')"
|
||||||
|
@click.prevent="openModalForce"
|
||||||
|
>
|
||||||
|
<FAIcon icon="search-plus" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="nsfw && hideNsfwLocal"
|
||||||
|
class="button-unstyled attachment-button"
|
||||||
|
:title="$t('status.hide_attachment')"
|
||||||
|
@click.prevent="toggleHidden"
|
||||||
|
>
|
||||||
|
<FAIcon icon="times" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="shiftUp"
|
||||||
|
class="button-unstyled attachment-button"
|
||||||
|
:title="$t('status.move_up')"
|
||||||
|
@click.prevent="onShiftUp"
|
||||||
|
>
|
||||||
|
<FAIcon icon="chevron-left" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="shiftDn"
|
||||||
|
class="button-unstyled attachment-button"
|
||||||
|
:title="$t('status.move_down')"
|
||||||
|
@click.prevent="onShiftDn"
|
||||||
|
>
|
||||||
|
<FAIcon icon="chevron-right" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="remove"
|
||||||
|
class="button-unstyled attachment-button"
|
||||||
|
:title="$t('status.remove_attachment')"
|
||||||
|
@click.prevent="onRemove"
|
||||||
|
>
|
||||||
|
<FAIcon icon="trash-alt" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="text">
|
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<a
|
||||||
<h1><a :href="attachment.url">{{ attachment.oembed.title }}</a></h1>
|
v-if="type === 'image' && (!hidden || preloadImage)"
|
||||||
<div v-html="attachment.oembed.oembedHTML" />
|
class="image-container"
|
||||||
<!-- eslint-enable vue/no-v-html -->
|
:class="{'-hidden': hidden && preloadImage }"
|
||||||
|
:href="attachment.url"
|
||||||
|
target="_blank"
|
||||||
|
@click.stop.prevent="openModal"
|
||||||
|
>
|
||||||
|
<StillImage
|
||||||
|
class="image"
|
||||||
|
:referrerpolicy="referrerpolicy"
|
||||||
|
:mimetype="attachment.mimetype"
|
||||||
|
:src="attachment.large_thumb_url || attachment.url"
|
||||||
|
:image-load-handler="onImageLoad"
|
||||||
|
:alt="attachment.description"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
v-if="type === 'unknown' && !hidden"
|
||||||
|
class="placeholder-container"
|
||||||
|
:href="attachment.url"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
size="5x"
|
||||||
|
:icon="placeholderIconClass"
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
{{ localDescription }}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<component
|
||||||
|
:is="videoTag"
|
||||||
|
v-if="type === 'video' && !hidden"
|
||||||
|
class="video-container"
|
||||||
|
:class="{ 'button-unstyled': 'isModal' }"
|
||||||
|
:href="attachment.url"
|
||||||
|
@click.stop.prevent="openModal"
|
||||||
|
>
|
||||||
|
<VideoAttachment
|
||||||
|
class="video"
|
||||||
|
:attachment="attachment"
|
||||||
|
:controls="!useModal"
|
||||||
|
@play="$emit('play')"
|
||||||
|
@pause="$emit('pause')"
|
||||||
|
/>
|
||||||
|
<FAIcon
|
||||||
|
v-if="useModal"
|
||||||
|
class="play-icon"
|
||||||
|
icon="play-circle"
|
||||||
|
/>
|
||||||
|
</component>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="type === 'audio' && !hidden"
|
||||||
|
class="audio-container"
|
||||||
|
:href="attachment.url"
|
||||||
|
@click.stop.prevent="openModal"
|
||||||
|
>
|
||||||
|
<audio
|
||||||
|
v-if="type === 'audio'"
|
||||||
|
:src="attachment.url"
|
||||||
|
:alt="attachment.description"
|
||||||
|
:title="attachment.description"
|
||||||
|
controls
|
||||||
|
@play="$emit('play')"
|
||||||
|
@pause="$emit('pause')"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="type === 'html' && attachment.oembed"
|
||||||
|
class="oembed-container"
|
||||||
|
@click.prevent="linkClicked"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="attachment.thumb_url"
|
||||||
|
class="image"
|
||||||
|
>
|
||||||
|
<img :src="attachment.thumb_url">
|
||||||
|
</div>
|
||||||
|
<div class="text">
|
||||||
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
|
<h1><a :href="attachment.url">{{ attachment.oembed.title }}</a></h1>
|
||||||
|
<div v-html="attachment.oembed.oembedHTML" />
|
||||||
|
<!-- eslint-enable vue/no-v-html -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="type === 'flash' && !hidden"
|
||||||
|
class="flash-container"
|
||||||
|
:href="attachment.url"
|
||||||
|
@click.stop.prevent="openModal"
|
||||||
|
>
|
||||||
|
<Flash
|
||||||
|
ref="flash"
|
||||||
|
class="flash"
|
||||||
|
:src="attachment.large_thumb_url || attachment.url"
|
||||||
|
@playerOpened="setFlashLoaded(true)"
|
||||||
|
@playerClosed="setFlashLoaded(false)"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="size !== 'hide' && !hideDescription && (edit || (localDescription && showDescription))"
|
||||||
|
class="description-container"
|
||||||
|
:class="{ '-static': !edit }"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-if="edit"
|
||||||
|
v-model="localDescription"
|
||||||
|
type="text"
|
||||||
|
class="description-field"
|
||||||
|
:placeholder="$t('post_status.media_description')"
|
||||||
|
@keydown.enter.prevent=""
|
||||||
|
>
|
||||||
|
<p v-else>
|
||||||
|
{{ localDescription }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./attachment.js"></script>
|
<script src="./attachment.js"></script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style src="./attachment.scss" lang="scss"></style>
|
||||||
@import '../../_variables.scss';
|
|
||||||
|
|
||||||
.attachments {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
.non-gallery {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.3em 1em 0.3em 0;
|
|
||||||
color: $fallback--link;
|
|
||||||
color: var(--postLink, $fallback--link);
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nsfw-placeholder {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.loading {
|
|
||||||
cursor: progress;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment {
|
|
||||||
position: relative;
|
|
||||||
margin-top: 0.5em;
|
|
||||||
align-self: flex-start;
|
|
||||||
line-height: 0;
|
|
||||||
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 1px;
|
|
||||||
border-radius: $fallback--attachmentRadius;
|
|
||||||
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
|
|
||||||
border-color: $fallback--border;
|
|
||||||
border-color: var(--border, $fallback--border);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.non-gallery.attachment {
|
|
||||||
&.video {
|
|
||||||
flex: 1 0 40%;
|
|
||||||
}
|
|
||||||
.nsfw {
|
|
||||||
height: 260px;
|
|
||||||
}
|
|
||||||
.small {
|
|
||||||
height: 120px;
|
|
||||||
flex-grow: 0;
|
|
||||||
}
|
|
||||||
.video {
|
|
||||||
height: 260px;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
video {
|
|
||||||
max-height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullwidth {
|
|
||||||
flex-basis: 100%;
|
|
||||||
}
|
|
||||||
// fixes small gap below video
|
|
||||||
&.video {
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-container {
|
|
||||||
display: flex;
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-icon {
|
|
||||||
position: absolute;
|
|
||||||
font-size: 64px;
|
|
||||||
top: calc(50% - 32px);
|
|
||||||
left: calc(50% - 32px);
|
|
||||||
color: rgba(255, 255, 255, 0.75);
|
|
||||||
text-shadow: 0 0 2px rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-icon::before {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.html {
|
|
||||||
flex-basis: 90%;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hider {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
margin: 10px;
|
|
||||||
padding: 5px;
|
|
||||||
background: rgba(230,230,230,0.6);
|
|
||||||
font-weight: bold;
|
|
||||||
z-index: 4;
|
|
||||||
line-height: 1;
|
|
||||||
border-radius: $fallback--tooltipRadius;
|
|
||||||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
|
||||||
}
|
|
||||||
|
|
||||||
video {
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
audio {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
img.media-upload {
|
|
||||||
line-height: 0;
|
|
||||||
max-height: 200px;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.oembed {
|
|
||||||
line-height: 1.2em;
|
|
||||||
flex: 1 0 100%;
|
|
||||||
width: 100%;
|
|
||||||
margin-right: 15px;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image {
|
|
||||||
flex: 1;
|
|
||||||
img {
|
|
||||||
border: 0px;
|
|
||||||
border-radius: 5px;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
flex: 2;
|
|
||||||
margin: 8px;
|
|
||||||
word-break: break-all;
|
|
||||||
h1 {
|
|
||||||
font-size: 14px;
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-attachment {
|
|
||||||
&,
|
|
||||||
& .image {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nsfw {
|
|
||||||
object-fit: cover;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
image-orientation: from-image; // NOTE: only FF supports this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { h, resolveComponent } from 'vue'
|
||||||
import LoginForm from '../login_form/login_form.vue'
|
import LoginForm from '../login_form/login_form.vue'
|
||||||
import MFARecoveryForm from '../mfa_form/recovery_form.vue'
|
import MFARecoveryForm from '../mfa_form/recovery_form.vue'
|
||||||
import MFATOTPForm from '../mfa_form/totp_form.vue'
|
import MFATOTPForm from '../mfa_form/totp_form.vue'
|
||||||
|
@ -5,8 +6,8 @@ import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
const AuthForm = {
|
const AuthForm = {
|
||||||
name: 'AuthForm',
|
name: 'AuthForm',
|
||||||
render (createElement) {
|
render () {
|
||||||
return createElement('component', { is: this.authForm })
|
return h(resolveComponent(this.authForm))
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
authForm () {
|
authForm () {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import UserCard from '../user_card/user_card.vue'
|
import UserCard from '../user_card/user_card.vue'
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
|
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
|
||||||
const BasicUserCard = {
|
const BasicUserCard = {
|
||||||
|
@ -13,7 +14,8 @@ const BasicUserCard = {
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
UserCard,
|
UserCard,
|
||||||
UserAvatar
|
UserAvatar,
|
||||||
|
RichContent
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleUserExpanded () {
|
toggleUserExpanded () {
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
class="avatar"
|
class="avatar"
|
||||||
:user="user"
|
:user="user"
|
||||||
@click.prevent.native="toggleUserExpanded"
|
@click.prevent="toggleUserExpanded"
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<div
|
<div
|
||||||
|
@ -25,24 +25,18 @@
|
||||||
:title="user.name"
|
:title="user.name"
|
||||||
class="basic-user-card-user-name"
|
class="basic-user-card-user-name"
|
||||||
>
|
>
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
<RichContent
|
||||||
<span
|
|
||||||
v-if="user.name_html"
|
|
||||||
class="basic-user-card-user-name-value"
|
class="basic-user-card-user-name-value"
|
||||||
v-html="user.name_html"
|
:html="user.name"
|
||||||
|
:emoji="user.emoji"
|
||||||
/>
|
/>
|
||||||
<!-- eslint-enable vue/no-v-html -->
|
|
||||||
<span
|
|
||||||
v-else
|
|
||||||
class="basic-user-card-user-name-value"
|
|
||||||
>{{ user.name }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<router-link
|
<router-link
|
||||||
class="basic-user-card-screen-name"
|
class="basic-user-card-screen-name"
|
||||||
:to="userProfileLink(user)"
|
:to="userProfileLink(user)"
|
||||||
>
|
>
|
||||||
@{{ user.screen_name }}
|
@{{ user.screen_name_ui }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<div class="block-card-content-container">
|
<div class="block-card-content-container">
|
||||||
<button
|
<button
|
||||||
v-if="blocked"
|
v-if="blocked"
|
||||||
class="btn btn-default"
|
class="btn button-default"
|
||||||
:disabled="progress"
|
:disabled="progress"
|
||||||
@click="unblockUser"
|
@click="unblockUser"
|
||||||
>
|
>
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-else
|
v-else
|
||||||
class="btn btn-default"
|
class="btn button-default"
|
||||||
:disabled="progress"
|
:disabled="progress"
|
||||||
@click="blockUser"
|
@click="blockUser"
|
||||||
>
|
>
|
||||||
|
|
|
@ -9,7 +9,7 @@ const Bookmarks = {
|
||||||
components: {
|
components: {
|
||||||
Timeline
|
Timeline
|
||||||
},
|
},
|
||||||
destroyed () {
|
unmounted () {
|
||||||
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
|
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,25 @@ import ChatMessage from '../chat_message/chat_message.vue'
|
||||||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||||
import ChatTitle from '../chat_title/chat_title.vue'
|
import ChatTitle from '../chat_title/chat_title.vue'
|
||||||
import chatService from '../../services/chat_service/chat_service.js'
|
import chatService from '../../services/chat_service/chat_service.js'
|
||||||
import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight } from './chat_layout_utils.js'
|
import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
|
||||||
|
import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight, isScrollable } from './chat_layout_utils.js'
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faChevronDown,
|
||||||
|
faChevronLeft
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faChevronDown,
|
||||||
|
faChevronLeft
|
||||||
|
)
|
||||||
|
|
||||||
const BOTTOMED_OUT_OFFSET = 10
|
const BOTTOMED_OUT_OFFSET = 10
|
||||||
const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 150
|
const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 150
|
||||||
const SAFE_RESIZE_TIME_OFFSET = 100
|
const SAFE_RESIZE_TIME_OFFSET = 100
|
||||||
|
const MARK_AS_READ_DELAY = 1500
|
||||||
|
const MAX_RETRIES = 10
|
||||||
|
|
||||||
const Chat = {
|
const Chat = {
|
||||||
components: {
|
components: {
|
||||||
|
@ -23,7 +37,8 @@ const Chat = {
|
||||||
hoveredMessageChainId: undefined,
|
hoveredMessageChainId: undefined,
|
||||||
lastScrollPosition: {},
|
lastScrollPosition: {},
|
||||||
scrollableContainerHeight: '100%',
|
scrollableContainerHeight: '100%',
|
||||||
errorLoadingChat: false
|
errorLoadingChat: false,
|
||||||
|
messageRetriers: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
@ -42,7 +57,7 @@ const Chat = {
|
||||||
})
|
})
|
||||||
this.setChatLayout()
|
this.setChatLayout()
|
||||||
},
|
},
|
||||||
destroyed () {
|
unmounted () {
|
||||||
window.removeEventListener('scroll', this.handleScroll)
|
window.removeEventListener('scroll', this.handleScroll)
|
||||||
window.removeEventListener('resize', this.handleLayoutChange)
|
window.removeEventListener('resize', this.handleLayoutChange)
|
||||||
this.unsetChatLayout()
|
this.unsetChatLayout()
|
||||||
|
@ -58,7 +73,7 @@ const Chat = {
|
||||||
},
|
},
|
||||||
formPlaceholder () {
|
formPlaceholder () {
|
||||||
if (this.recipient) {
|
if (this.recipient) {
|
||||||
return this.$t('chats.message_user', { nickname: this.recipient.screen_name })
|
return this.$t('chats.message_user', { nickname: this.recipient.screen_name_ui })
|
||||||
} else {
|
} else {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
@ -93,7 +108,7 @@ const Chat = {
|
||||||
const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET)
|
const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET)
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (bottomedOutBeforeUpdate) {
|
if (bottomedOutBeforeUpdate) {
|
||||||
this.scrollDown({ forceRead: !document.hidden })
|
this.scrollDown()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -199,15 +214,18 @@ const Chat = {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
scrollable.scrollTo({ top: scrollable.scrollHeight, left: 0, behavior })
|
scrollable.scrollTo({ top: scrollable.scrollHeight, left: 0, behavior })
|
||||||
})
|
})
|
||||||
if (forceRead || this.newMessageCount > 0) {
|
if (forceRead) {
|
||||||
this.readChat()
|
this.readChat()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
readChat () {
|
readChat () {
|
||||||
if (!(this.currentChatMessageService && this.currentChatMessageService.lastMessage)) { return }
|
if (!(this.currentChatMessageService && this.currentChatMessageService.maxId)) { return }
|
||||||
if (document.hidden) { return }
|
if (document.hidden) { return }
|
||||||
const lastReadId = this.currentChatMessageService.lastMessage.id
|
const lastReadId = this.currentChatMessageService.maxId
|
||||||
this.$store.dispatch('readChat', { id: this.currentChat.id, lastReadId })
|
this.$store.dispatch('readChat', {
|
||||||
|
id: this.currentChat.id,
|
||||||
|
lastReadId
|
||||||
|
})
|
||||||
},
|
},
|
||||||
bottomedOut (offset) {
|
bottomedOut (offset) {
|
||||||
return isBottomedOut(this.$refs.scrollable, offset)
|
return isBottomedOut(this.$refs.scrollable, offset)
|
||||||
|
@ -216,6 +234,13 @@ const Chat = {
|
||||||
const scrollable = this.$refs.scrollable
|
const scrollable = this.$refs.scrollable
|
||||||
return scrollable && scrollable.scrollTop <= 0
|
return scrollable && scrollable.scrollTop <= 0
|
||||||
},
|
},
|
||||||
|
cullOlderCheck () {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
|
||||||
|
this.$store.dispatch('cullOlderMessages', this.currentChatMessageService.chatId)
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
|
},
|
||||||
handleScroll: _.throttle(function () {
|
handleScroll: _.throttle(function () {
|
||||||
if (!this.currentChat) { return }
|
if (!this.currentChat) { return }
|
||||||
|
|
||||||
|
@ -223,13 +248,20 @@ const Chat = {
|
||||||
this.fetchChat({ maxId: this.currentChatMessageService.minId })
|
this.fetchChat({ maxId: this.currentChatMessageService.minId })
|
||||||
} else if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
|
} else if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
|
||||||
this.jumpToBottomButtonVisible = false
|
this.jumpToBottomButtonVisible = false
|
||||||
|
this.cullOlderCheck()
|
||||||
if (this.newMessageCount > 0) {
|
if (this.newMessageCount > 0) {
|
||||||
this.readChat()
|
// Use a delay before marking as read to prevent situation where new messages
|
||||||
|
// arrive just as you're leaving the view and messages that you didn't actually
|
||||||
|
// get to see get marked as read.
|
||||||
|
window.setTimeout(() => {
|
||||||
|
// Don't mark as read if the element doesn't exist, user has left chat view
|
||||||
|
if (this.$el) this.readChat()
|
||||||
|
}, MARK_AS_READ_DELAY)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.jumpToBottomButtonVisible = true
|
this.jumpToBottomButtonVisible = true
|
||||||
}
|
}
|
||||||
}, 100),
|
}, 200),
|
||||||
handleScrollUp (positionBeforeLoading) {
|
handleScrollUp (positionBeforeLoading) {
|
||||||
const positionAfterLoading = getScrollPosition(this.$refs.scrollable)
|
const positionAfterLoading = getScrollPosition(this.$refs.scrollable)
|
||||||
this.$refs.scrollable.scrollTo({
|
this.$refs.scrollable.scrollTo({
|
||||||
|
@ -244,9 +276,9 @@ const Chat = {
|
||||||
|
|
||||||
const chatId = chatMessageService.chatId
|
const chatId = chatMessageService.chatId
|
||||||
const fetchOlderMessages = !!maxId
|
const fetchOlderMessages = !!maxId
|
||||||
const sinceId = fetchLatest && chatMessageService.lastMessage && chatMessageService.lastMessage.id
|
const sinceId = fetchLatest && chatMessageService.maxId
|
||||||
|
|
||||||
this.backendInteractor.chatMessages({ id: chatId, maxId, sinceId })
|
return this.backendInteractor.chatMessages({ id: chatId, maxId, sinceId })
|
||||||
.then((messages) => {
|
.then((messages) => {
|
||||||
// Clear the current chat in case we're recovering from a ws connection loss.
|
// Clear the current chat in case we're recovering from a ws connection loss.
|
||||||
if (isFirstFetch) {
|
if (isFirstFetch) {
|
||||||
|
@ -263,6 +295,14 @@ const Chat = {
|
||||||
if (isFirstFetch) {
|
if (isFirstFetch) {
|
||||||
this.updateScrollableContainerHeight()
|
this.updateScrollableContainerHeight()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In vertical screens, the first batch of fetched messages may not always take the
|
||||||
|
// full height of the scrollable container.
|
||||||
|
// If this is the case, we want to fetch the messages until the scrollable container
|
||||||
|
// is fully populated so that the user has the ability to scroll up and load the history.
|
||||||
|
if (!isScrollable(this.$refs.scrollable) && messages.length > 0) {
|
||||||
|
this.fetchChat({ maxId: this.currentChatMessageService.minId })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -287,42 +327,78 @@ const Chat = {
|
||||||
},
|
},
|
||||||
doStartFetching () {
|
doStartFetching () {
|
||||||
this.$store.dispatch('startFetchingCurrentChat', {
|
this.$store.dispatch('startFetchingCurrentChat', {
|
||||||
fetcher: () => setInterval(() => this.fetchChat({ fetchLatest: true }), 5000)
|
fetcher: () => promiseInterval(() => this.fetchChat({ fetchLatest: true }), 5000)
|
||||||
})
|
})
|
||||||
this.fetchChat({ isFirstFetch: true })
|
this.fetchChat({ isFirstFetch: true })
|
||||||
},
|
},
|
||||||
sendMessage ({ status, media }) {
|
handleAttachmentPosting () {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.handleResize()
|
||||||
|
// When the posting form size changes because of a media attachment, we need an extra resize
|
||||||
|
// to account for the potential delay in the DOM update.
|
||||||
|
setTimeout(() => {
|
||||||
|
this.updateScrollableContainerHeight()
|
||||||
|
}, SAFE_RESIZE_TIME_OFFSET)
|
||||||
|
this.scrollDown({ forceRead: true })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
sendMessage ({ status, media, idempotencyKey }) {
|
||||||
const params = {
|
const params = {
|
||||||
id: this.currentChat.id,
|
id: this.currentChat.id,
|
||||||
content: status
|
content: status,
|
||||||
|
idempotencyKey
|
||||||
}
|
}
|
||||||
|
|
||||||
if (media[0]) {
|
if (media[0]) {
|
||||||
params.mediaId = media[0].id
|
params.mediaId = media[0].id
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.backendInteractor.sendChatMessage(params)
|
const fakeMessage = buildFakeMessage({
|
||||||
|
attachments: media,
|
||||||
|
chatId: this.currentChat.id,
|
||||||
|
content: status,
|
||||||
|
userId: this.currentUser.id,
|
||||||
|
idempotencyKey
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$store.dispatch('addChatMessages', {
|
||||||
|
chatId: this.currentChat.id,
|
||||||
|
messages: [fakeMessage]
|
||||||
|
}).then(() => {
|
||||||
|
this.handleAttachmentPosting()
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.doSendMessage({ params, fakeMessage, retriesLeft: MAX_RETRIES })
|
||||||
|
},
|
||||||
|
doSendMessage ({ params, fakeMessage, retriesLeft = MAX_RETRIES }) {
|
||||||
|
if (retriesLeft <= 0) return
|
||||||
|
|
||||||
|
this.backendInteractor.sendChatMessage(params)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
this.$store.dispatch('addChatMessages', { chatId: this.currentChat.id, messages: [data] }).then(() => {
|
this.$store.dispatch('addChatMessages', {
|
||||||
this.$nextTick(() => {
|
chatId: this.currentChat.id,
|
||||||
this.handleResize()
|
updateMaxId: false,
|
||||||
// When the posting form size changes because of a media attachment, we need an extra resize
|
messages: [{ ...data, fakeId: fakeMessage.id }]
|
||||||
// to account for the potential delay in the DOM update.
|
|
||||||
setTimeout(() => {
|
|
||||||
this.updateScrollableContainerHeight()
|
|
||||||
}, SAFE_RESIZE_TIME_OFFSET)
|
|
||||||
this.scrollDown({ forceRead: true })
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error sending message', error)
|
console.error('Error sending message', error)
|
||||||
return {
|
this.$store.dispatch('handleMessageError', {
|
||||||
error: this.$t('chats.error_sending_message')
|
chatId: this.currentChat.id,
|
||||||
|
fakeId: fakeMessage.id,
|
||||||
|
isRetry: retriesLeft !== MAX_RETRIES
|
||||||
|
})
|
||||||
|
if ((error.statusCode >= 500 && error.statusCode < 600) || error.message === 'Failed to fetch') {
|
||||||
|
this.messageRetriers[fakeMessage.id] = setTimeout(() => {
|
||||||
|
this.doSendMessage({ params, fakeMessage, retriesLeft: retriesLeft - 1 })
|
||||||
|
}, 1000 * (2 ** (MAX_RETRIES - retriesLeft)))
|
||||||
}
|
}
|
||||||
|
return {}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return Promise.resolve(fakeMessage)
|
||||||
},
|
},
|
||||||
goBack () {
|
goBack () {
|
||||||
this.$router.push({ name: 'chats', params: { username: this.currentUser.screen_name } })
|
this.$router.push({ name: 'chats', params: { username: this.currentUser.screen_name } })
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
margin: 0 0 0 0;
|
margin: 0 0 0 0;
|
||||||
border-radius: 10px 10px 0 0;
|
border-radius: 10px 10px 0 0;
|
||||||
border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0 ;
|
border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
@ -58,12 +58,10 @@
|
||||||
|
|
||||||
.go-back-button {
|
.go-back-button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-right: 1.4em;
|
width: 28px;
|
||||||
|
text-align: center;
|
||||||
i {
|
padding: 0.6em;
|
||||||
display: flex;
|
margin: -0.6em 0.6em -0.6em -0.6em;
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.jump-to-bottom-button {
|
.jump-to-bottom-button {
|
||||||
|
@ -78,7 +76,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3), 0px 2px 4px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
transition: 0.35s all;
|
transition: 0.35s all;
|
||||||
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
||||||
|
@ -100,10 +98,10 @@
|
||||||
.unread-message-count {
|
.unread-message-count {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, 0);
|
|
||||||
border-radius: 100%;
|
|
||||||
margin-top: -1rem;
|
margin-top: -1rem;
|
||||||
padding: 0;
|
padding: 0.1em;
|
||||||
|
border-radius: 50px;
|
||||||
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-loading-error {
|
.chat-loading-error {
|
||||||
|
@ -140,11 +138,21 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-view-heading {
|
.chat-view-heading {
|
||||||
|
box-sizing: border-box;
|
||||||
position: static;
|
position: static;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
top: 0;
|
top: 0;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
|
||||||
|
/* This practically overlays the panel heading color over panel background
|
||||||
|
* color. This is needed because we allow transparent panel background and
|
||||||
|
* it doesn't work well in this "disjointed panel header" case
|
||||||
|
*/
|
||||||
|
background:
|
||||||
|
linear-gradient(to top, var(--panel), var(--panel)),
|
||||||
|
linear-gradient(to top, var(--bg), var(--bg));
|
||||||
|
height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollable-message-list {
|
.scrollable-message-list {
|
||||||
|
|
|
@ -14,7 +14,10 @@
|
||||||
class="go-back-button"
|
class="go-back-button"
|
||||||
@click="goBack"
|
@click="goBack"
|
||||||
>
|
>
|
||||||
<i class="button-icon icon-left-open" />
|
<FAIcon
|
||||||
|
size="lg"
|
||||||
|
icon="chevron-left"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
<div class="title text-center">
|
<div class="title text-center">
|
||||||
<ChatTitle
|
<ChatTitle
|
||||||
|
@ -23,71 +26,71 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template>
|
<div
|
||||||
<div
|
ref="scrollable"
|
||||||
ref="scrollable"
|
class="scrollable-message-list"
|
||||||
class="scrollable-message-list"
|
:style="{ height: scrollableContainerHeight }"
|
||||||
:style="{ height: scrollableContainerHeight }"
|
@scroll="handleScroll"
|
||||||
@scroll="handleScroll"
|
>
|
||||||
>
|
<template v-if="!errorLoadingChat">
|
||||||
<template v-if="!errorLoadingChat">
|
<ChatMessage
|
||||||
<ChatMessage
|
v-for="chatViewItem in chatViewItems"
|
||||||
v-for="chatViewItem in chatViewItems"
|
:key="chatViewItem.id"
|
||||||
:key="chatViewItem.id"
|
:author="recipient"
|
||||||
:author="recipient"
|
:chat-view-item="chatViewItem"
|
||||||
:chat-view-item="chatViewItem"
|
:hovered-message-chain="chatViewItem.messageChainId === hoveredMessageChainId"
|
||||||
:hovered-message-chain="chatViewItem.messageChainId === hoveredMessageChainId"
|
@hover="onMessageHover"
|
||||||
@hover="onMessageHover"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="chat-loading-error"
|
|
||||||
>
|
|
||||||
<div class="alert error">
|
|
||||||
{{ $t('chats.error_loading_chat') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
ref="footer"
|
|
||||||
class="panel-body footer"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="jump-to-bottom-button"
|
|
||||||
:class="{ 'visible': jumpToBottomButtonVisible }"
|
|
||||||
@click="scrollDown({ behavior: 'smooth' })"
|
|
||||||
>
|
|
||||||
<i class="icon-down-open">
|
|
||||||
<div
|
|
||||||
v-if="newMessageCount"
|
|
||||||
class="badge badge-notification unread-chat-count unread-message-count"
|
|
||||||
>
|
|
||||||
{{ newMessageCount }}
|
|
||||||
</div>
|
|
||||||
</i>
|
|
||||||
</div>
|
|
||||||
<PostStatusForm
|
|
||||||
:disable-subject="true"
|
|
||||||
:disable-scope-selector="true"
|
|
||||||
:disable-notice="true"
|
|
||||||
:disable-lock-warning="true"
|
|
||||||
:disable-polls="true"
|
|
||||||
:disable-sensitivity-checkbox="true"
|
|
||||||
:disable-submit="errorLoadingChat || !currentChat"
|
|
||||||
:disable-preview="true"
|
|
||||||
:post-handler="sendMessage"
|
|
||||||
:submit-on-enter="!mobileLayout"
|
|
||||||
:preserve-focus="!mobileLayout"
|
|
||||||
:auto-focus="!mobileLayout"
|
|
||||||
:placeholder="formPlaceholder"
|
|
||||||
:file-limit="1"
|
|
||||||
max-height="160"
|
|
||||||
emoji-picker-placement="top"
|
|
||||||
@resize="handleResize"
|
|
||||||
/>
|
/>
|
||||||
|
</template>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="chat-loading-error"
|
||||||
|
>
|
||||||
|
<div class="alert error">
|
||||||
|
{{ $t('chats.error_loading_chat') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
|
<div
|
||||||
|
ref="footer"
|
||||||
|
class="panel-body footer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="jump-to-bottom-button"
|
||||||
|
:class="{ 'visible': jumpToBottomButtonVisible }"
|
||||||
|
@click="scrollDown({ behavior: 'smooth' })"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<FAIcon icon="chevron-down" />
|
||||||
|
<div
|
||||||
|
v-if="newMessageCount"
|
||||||
|
class="badge badge-notification unread-chat-count unread-message-count"
|
||||||
|
>
|
||||||
|
{{ newMessageCount }}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<PostStatusForm
|
||||||
|
:disable-subject="true"
|
||||||
|
:disable-scope-selector="true"
|
||||||
|
:disable-notice="true"
|
||||||
|
:disable-lock-warning="true"
|
||||||
|
:disable-polls="true"
|
||||||
|
:disable-sensitivity-checkbox="true"
|
||||||
|
:disable-submit="errorLoadingChat || !currentChat"
|
||||||
|
:disable-preview="true"
|
||||||
|
:optimistic-posting="true"
|
||||||
|
:post-handler="sendMessage"
|
||||||
|
:submit-on-enter="!mobileLayout"
|
||||||
|
:preserve-focus="!mobileLayout"
|
||||||
|
:auto-focus="!mobileLayout"
|
||||||
|
:placeholder="formPlaceholder"
|
||||||
|
:file-limit="1"
|
||||||
|
max-height="160"
|
||||||
|
emoji-picker-placement="top"
|
||||||
|
@resize="handleResize"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -24,3 +24,10 @@ export const isBottomedOut = (el, offset = 0) => {
|
||||||
export const scrollableContainerHeight = (inner, header, footer) => {
|
export const scrollableContainerHeight = (inner, header, footer) => {
|
||||||
return inner.offsetHeight - header.clientHeight - footer.clientHeight
|
return inner.offsetHeight - header.clientHeight - footer.clientHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns whether or not the scrollbar is visible.
|
||||||
|
export const isScrollable = (el) => {
|
||||||
|
if (!el) return
|
||||||
|
|
||||||
|
return el.scrollHeight > el.clientHeight
|
||||||
|
}
|
||||||
|
|
|
@ -10,7 +10,10 @@
|
||||||
<span class="title">
|
<span class="title">
|
||||||
{{ $t("chats.chats") }}
|
{{ $t("chats.chats") }}
|
||||||
</span>
|
</span>
|
||||||
<button @click="newChat">
|
<button
|
||||||
|
class="button-default"
|
||||||
|
@click="newChat"
|
||||||
|
>
|
||||||
{{ $t("chats.new") }}
|
{{ $t("chats.new") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,10 +23,7 @@
|
||||||
class="timeline"
|
class="timeline"
|
||||||
>
|
>
|
||||||
<List :items="sortedChatList">
|
<List :items="sortedChatList">
|
||||||
<template
|
<template v-slot:item="{item}">
|
||||||
slot="item"
|
|
||||||
slot-scope="{item}"
|
|
||||||
>
|
|
||||||
<ChatListItem
|
<ChatListItem
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:compact="false"
|
:compact="false"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import StatusContent from '../status_content/status_content.vue'
|
import StatusBody from '../status_content/status_content.vue'
|
||||||
import fileType from 'src/services/file_type/file_type.service'
|
import fileType from 'src/services/file_type/file_type.service'
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
import AvatarList from '../avatar_list/avatar_list.vue'
|
import AvatarList from '../avatar_list/avatar_list.vue'
|
||||||
|
@ -16,7 +16,7 @@ const ChatListItem = {
|
||||||
AvatarList,
|
AvatarList,
|
||||||
Timeago,
|
Timeago,
|
||||||
ChatTitle,
|
ChatTitle,
|
||||||
StatusContent
|
StatusBody
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
|
@ -38,12 +38,14 @@ const ChatListItem = {
|
||||||
},
|
},
|
||||||
messageForStatusContent () {
|
messageForStatusContent () {
|
||||||
const message = this.chat.lastMessage
|
const message = this.chat.lastMessage
|
||||||
|
const messageEmojis = message ? message.emojis : []
|
||||||
const isYou = message && message.account_id === this.currentUser.id
|
const isYou = message && message.account_id === this.currentUser.id
|
||||||
const content = message ? (this.attachmentInfo || message.content) : ''
|
const content = message ? (this.attachmentInfo || message.content) : ''
|
||||||
const messagePreview = isYou ? `<i>${this.$t('chats.you')}</i> ${content}` : content
|
const messagePreview = isYou ? `<i>${this.$t('chats.you')}</i> ${content}` : content
|
||||||
return {
|
return {
|
||||||
summary: '',
|
summary: '',
|
||||||
statusnet_html: messagePreview,
|
emojis: messageEmojis,
|
||||||
|
raw_html: messagePreview,
|
||||||
text: messagePreview,
|
text: messagePreview,
|
||||||
attachments: []
|
attachments: []
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,18 +77,15 @@
|
||||||
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.StatusContent {
|
.chat-preview-body {
|
||||||
img.emoji {
|
--emoji-size: 1.4em;
|
||||||
width: 1.4em;
|
|
||||||
height: 1.4em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-wrapper {
|
.time-wrapper {
|
||||||
line-height: 1.4em;
|
line-height: 1.4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.single-line {
|
.chat-preview-body {
|
||||||
padding-right: 1em;
|
padding-right: 1em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,9 +21,16 @@
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span class="heading-right" />
|
<span class="heading-right" />
|
||||||
|
<div class="time-wrapper">
|
||||||
|
<Timeago
|
||||||
|
:time="chat.updated_at"
|
||||||
|
:auto-update="60"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-preview">
|
<div class="chat-preview">
|
||||||
<StatusContent
|
<StatusBody
|
||||||
|
class="chat-preview-body"
|
||||||
:status="messageForStatusContent"
|
:status="messageForStatusContent"
|
||||||
:single-line="true"
|
:single-line="true"
|
||||||
/>
|
/>
|
||||||
|
@ -35,12 +42,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="time-wrapper">
|
|
||||||
<Timeago
|
|
||||||
:time="chat.updated_at"
|
|
||||||
:auto-update="60"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,16 @@ import LinkPreview from '../link-preview/link-preview.vue'
|
||||||
import StatusContent from '../status_content/status_content.vue'
|
import StatusContent from '../status_content/status_content.vue'
|
||||||
import ChatMessageDate from '../chat_message_date/chat_message_date.vue'
|
import ChatMessageDate from '../chat_message_date/chat_message_date.vue'
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faTimes,
|
||||||
|
faEllipsisH
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faTimes,
|
||||||
|
faEllipsisH
|
||||||
|
)
|
||||||
|
|
||||||
const ChatMessage = {
|
const ChatMessage = {
|
||||||
name: 'ChatMessage',
|
name: 'ChatMessage',
|
||||||
|
@ -17,6 +27,7 @@ const ChatMessage = {
|
||||||
'chatViewItem',
|
'chatViewItem',
|
||||||
'hoveredMessageChain'
|
'hoveredMessageChain'
|
||||||
],
|
],
|
||||||
|
emits: ['hover'],
|
||||||
components: {
|
components: {
|
||||||
Popover,
|
Popover,
|
||||||
Attachment,
|
Attachment,
|
||||||
|
@ -47,8 +58,9 @@ const ChatMessage = {
|
||||||
messageForStatusContent () {
|
messageForStatusContent () {
|
||||||
return {
|
return {
|
||||||
summary: '',
|
summary: '',
|
||||||
statusnet_html: this.message.content,
|
emojis: this.message.emojis,
|
||||||
text: this.message.content,
|
raw_html: this.message.content || '',
|
||||||
|
text: this.message.content || '',
|
||||||
attachments: this.message.attachments
|
attachments: this.message.attachments
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.chat-message-wrapper {
|
.chat-message-wrapper {
|
||||||
|
|
||||||
&.hovered-message-chain {
|
&.hovered-message-chain {
|
||||||
.animated.Avatar {
|
.animated.Avatar {
|
||||||
canvas {
|
canvas {
|
||||||
|
@ -24,16 +25,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-ellipsis {
|
.menu-icon {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover, .extra-button-popover.open & {
|
&:hover, .extra-button-popover.open & {
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--text, $fallback--text);
|
color: var(--text, $fallback--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
border-radius: $fallback--chatMessageRadius;
|
|
||||||
border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.popover {
|
.popover {
|
||||||
|
@ -43,6 +41,12 @@
|
||||||
.chat-message {
|
.chat-message {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding-bottom: 0.5em;
|
padding-bottom: 0.5em;
|
||||||
|
|
||||||
|
.status-body:hover {
|
||||||
|
--_still-image-img-visibility: visible;
|
||||||
|
--_still-image-canvas-visibility: hidden;
|
||||||
|
--_still-image-label-visibility: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-wrapper {
|
.avatar-wrapper {
|
||||||
|
@ -65,10 +69,6 @@
|
||||||
&.with-media {
|
&.with-media {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.gallery-row {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -92,8 +92,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.without-attachment {
|
.without-attachment {
|
||||||
.status-content {
|
.message-content {
|
||||||
&::after {
|
// TODO figure out how to do it properly
|
||||||
|
.RichContent::after {
|
||||||
margin-right: 5.4em;
|
margin-right: 5.4em;
|
||||||
content: " ";
|
content: " ";
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -101,6 +102,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pending {
|
||||||
|
.status-content.media-body, .created-at {
|
||||||
|
color: var(--faint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
.status-content.media-body, .created-at {
|
||||||
|
color: $fallback--cRed;
|
||||||
|
color: var(--badgeNotification, $fallback--cRed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.incoming {
|
.incoming {
|
||||||
a {
|
a {
|
||||||
color: var(--chatMessageIncomingLink, $fallback--link);
|
color: var(--chatMessageIncomingLink, $fallback--link);
|
||||||
|
@ -152,6 +166,7 @@
|
||||||
.visible {
|
.visible {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-message-date-separator {
|
.chat-message-date-separator {
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="media status"
|
class="media status"
|
||||||
:class="{ 'without-attachment': !hasAttachment }"
|
:class="{ 'without-attachment': !hasAttachment, 'pending': chatViewItem.data.pending, 'error': chatViewItem.data.error }"
|
||||||
style="position: relative"
|
style="position: relative"
|
||||||
@mouseenter="hovered = true"
|
@mouseenter="hovered = true"
|
||||||
@mouseleave="hovered = false"
|
@mouseleave="hovered = false"
|
||||||
|
@ -50,34 +50,38 @@
|
||||||
@show="menuOpened = true"
|
@show="menuOpened = true"
|
||||||
@close="menuOpened = false"
|
@close="menuOpened = false"
|
||||||
>
|
>
|
||||||
<div slot="content">
|
<template v-slot:content>
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<button
|
<button
|
||||||
class="dropdown-item dropdown-item-icon"
|
class="button-default dropdown-item dropdown-item-icon"
|
||||||
@click="deleteMessage"
|
@click="deleteMessage"
|
||||||
>
|
>
|
||||||
<i class="icon-cancel" /> {{ $t("chats.delete") }}
|
<FAIcon icon="times" /> {{ $t("chats.delete") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<button
|
<template v-slot:trigger>
|
||||||
slot="trigger"
|
<button
|
||||||
:title="$t('chats.more')"
|
class="button-default menu-icon"
|
||||||
>
|
:title="$t('chats.more')"
|
||||||
<i class="icon-ellipsis" />
|
>
|
||||||
</button>
|
<FAIcon icon="ellipsis-h" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
<StatusContent
|
<StatusContent
|
||||||
|
class="message-content"
|
||||||
:status="messageForStatusContent"
|
:status="messageForStatusContent"
|
||||||
:full-content="true"
|
:full-content="true"
|
||||||
>
|
>
|
||||||
<span
|
<template v-slot:footer>
|
||||||
slot="footer"
|
<span
|
||||||
class="created-at"
|
class="created-at"
|
||||||
>
|
>
|
||||||
{{ createdAt }}
|
{{ createdAt }}
|
||||||
</span>
|
</span>
|
||||||
|
</template>
|
||||||
</StatusContent>
|
</StatusContent>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import localeService from 'src/services/locale/locale.service.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Timeago',
|
name: 'Timeago',
|
||||||
props: ['date'],
|
props: ['date'],
|
||||||
|
@ -16,7 +18,7 @@ export default {
|
||||||
if (this.date.getTime() === today.getTime()) {
|
if (this.date.getTime() === today.getTime()) {
|
||||||
return this.$t('display_date.today')
|
return this.$t('display_date.today')
|
||||||
} else {
|
} else {
|
||||||
return this.date.toLocaleDateString('en', { day: 'numeric', month: 'long' })
|
return this.date.toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale), { day: 'numeric', month: 'long' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
import { mapState, mapGetters } from 'vuex'
|
import { mapState, mapGetters } from 'vuex'
|
||||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faSearch,
|
||||||
|
faChevronLeft
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faSearch,
|
||||||
|
faChevronLeft
|
||||||
|
)
|
||||||
|
|
||||||
const chatNew = {
|
const chatNew = {
|
||||||
components: {
|
components: {
|
||||||
|
|
|
@ -8,9 +8,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-search {
|
.search-icon {
|
||||||
font-size: 1.5em;
|
|
||||||
float: right;
|
|
||||||
margin-right: 0.3em;
|
margin-right: 0.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,5 +23,9 @@
|
||||||
|
|
||||||
.go-back-button {
|
.go-back-button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
width: 28px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.6em;
|
||||||
|
margin: -0.6em 0.6em -0.6em -0.6em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,12 +11,18 @@
|
||||||
class="go-back-button"
|
class="go-back-button"
|
||||||
@click="goBack"
|
@click="goBack"
|
||||||
>
|
>
|
||||||
<i class="button-icon icon-left-open" />
|
<FAIcon
|
||||||
|
size="lg"
|
||||||
|
icon="chevron-left"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-wrap">
|
<div class="input-wrap">
|
||||||
<div class="input-search">
|
<div class="input-search">
|
||||||
<i class="button-icon icon-search" />
|
<FAIcon
|
||||||
|
class="search-icon fa-scale-110 fa-old-padding"
|
||||||
|
icon="search"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
ref="search"
|
ref="search"
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
|
||||||
|
|
||||||
const chatPanel = {
|
|
||||||
props: [ 'floating' ],
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
currentMessage: '',
|
|
||||||
channel: null,
|
|
||||||
collapsed: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
messages () {
|
|
||||||
return this.$store.state.chat.messages
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
submit (message) {
|
|
||||||
this.$store.state.chat.channel.push('new_msg', { text: message }, 10000)
|
|
||||||
this.currentMessage = ''
|
|
||||||
},
|
|
||||||
togglePanel () {
|
|
||||||
this.collapsed = !this.collapsed
|
|
||||||
},
|
|
||||||
userProfileLink (user) {
|
|
||||||
return generateProfileLink(user.id, user.username, this.$store.state.instance.restrictedNicknames)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default chatPanel
|
|
|
@ -1,18 +1,19 @@
|
||||||
import Vue from 'vue'
|
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
|
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||||
|
|
||||||
export default Vue.component('chat-title', {
|
export default {
|
||||||
name: 'ChatTitle',
|
name: 'ChatTitle',
|
||||||
components: {
|
components: {
|
||||||
UserAvatar
|
UserAvatar,
|
||||||
|
RichContent
|
||||||
},
|
},
|
||||||
props: [
|
props: [
|
||||||
'user', 'withAvatar'
|
'user', 'withAvatar'
|
||||||
],
|
],
|
||||||
computed: {
|
computed: {
|
||||||
title () {
|
title () {
|
||||||
return this.user ? this.user.screen_name : ''
|
return this.user ? this.user.screen_name_ui : ''
|
||||||
},
|
},
|
||||||
htmlTitle () {
|
htmlTitle () {
|
||||||
return this.user ? this.user.name_html : ''
|
return this.user ? this.user.name_html : ''
|
||||||
|
@ -23,4 +24,4 @@ export default Vue.component('chat-title', {
|
||||||
return generateProfileLink(user.id, user.screen_name)
|
return generateProfileLink(user.id, user.screen_name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<!-- eslint-disable vue/no-v-html -->
|
|
||||||
<div
|
<div
|
||||||
class="chat-title"
|
class="chat-title"
|
||||||
:title="title"
|
:title="title"
|
||||||
|
@ -14,12 +13,14 @@
|
||||||
height="23px"
|
height="23px"
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<span
|
<RichContent
|
||||||
|
v-if="user"
|
||||||
class="username"
|
class="username"
|
||||||
v-html="htmlTitle"
|
:title="'@'+user.screen_name_ui"
|
||||||
|
:html="htmlTitle"
|
||||||
|
:emoji="user.emoji || []"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- eslint-enable vue/no-v-html -->
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./chat_title.js"></script>
|
<script src="./chat_title.js"></script>
|
||||||
|
@ -34,6 +35,8 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
--emoji-size: 14px;
|
||||||
|
|
||||||
.username {
|
.username {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
@ -41,14 +44,6 @@
|
||||||
display: inline;
|
display: inline;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
.emoji {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
vertical-align: middle;
|
|
||||||
object-fit: contain
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.Avatar {
|
.Avatar {
|
||||||
|
|
|
@ -6,9 +6,9 @@
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:checked="checked"
|
:checked="modelValue"
|
||||||
:indeterminate.prop="indeterminate"
|
:indeterminate="indeterminate"
|
||||||
@change="$emit('change', $event.target.checked)"
|
@change="$emit('update:modelValue', $event.target.checked)"
|
||||||
>
|
>
|
||||||
<i class="checkbox-indicator" />
|
<i class="checkbox-indicator" />
|
||||||
<span
|
<span
|
||||||
|
@ -22,12 +22,9 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
model: {
|
emits: ['update:modelValue'],
|
||||||
prop: 'checked',
|
|
||||||
event: 'change'
|
|
||||||
},
|
|
||||||
props: [
|
props: [
|
||||||
'checked',
|
'modelValue',
|
||||||
'indeterminate',
|
'indeterminate',
|
||||||
'disabled'
|
'disabled'
|
||||||
]
|
]
|
||||||
|
|
|
@ -11,28 +11,28 @@
|
||||||
</label>
|
</label>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-if="typeof fallback !== 'undefined' && showOptionalTickbox"
|
v-if="typeof fallback !== 'undefined' && showOptionalTickbox"
|
||||||
:checked="present"
|
:model-value="present"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
class="opt"
|
class="opt"
|
||||||
@change="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
|
@update:modelValue="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
|
||||||
/>
|
/>
|
||||||
<div class="input color-input-field">
|
<div class="input color-input-field">
|
||||||
<input
|
<input
|
||||||
:id="name + '-t'"
|
:id="name + '-t'"
|
||||||
class="textColor unstyled"
|
class="textColor unstyled"
|
||||||
type="text"
|
type="text"
|
||||||
:value="value || fallback"
|
:value="modelValue || fallback"
|
||||||
:disabled="!present || disabled"
|
:disabled="!present || disabled"
|
||||||
@input="$emit('input', $event.target.value)"
|
@input="$emit('update:modelValue', $event.target.value)"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-if="validColor"
|
v-if="validColor"
|
||||||
:id="name"
|
:id="name"
|
||||||
class="nativeColor unstyled"
|
class="nativeColor unstyled"
|
||||||
type="color"
|
type="color"
|
||||||
:value="value || fallback"
|
:value="modelValue || fallback"
|
||||||
:disabled="!present || disabled"
|
:disabled="!present || disabled"
|
||||||
@input="$emit('input', $event.target.value)"
|
@input="$emit('update:modelValue', $event.target.value)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="transparentColor"
|
v-if="transparentColor"
|
||||||
|
@ -67,7 +67,7 @@ export default {
|
||||||
},
|
},
|
||||||
// Color value, should be required but vue cannot tell the difference
|
// Color value, should be required but vue cannot tell the difference
|
||||||
// between "property missing" and "property set to undefined"
|
// between "property missing" and "property set to undefined"
|
||||||
value: {
|
modelValue: {
|
||||||
required: false,
|
required: false,
|
||||||
type: String,
|
type: String,
|
||||||
default: undefined
|
default: undefined
|
||||||
|
@ -91,18 +91,19 @@ export default {
|
||||||
default: true
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
computed: {
|
computed: {
|
||||||
present () {
|
present () {
|
||||||
return typeof this.value !== 'undefined'
|
return typeof this.modelValue !== 'undefined'
|
||||||
},
|
},
|
||||||
validColor () {
|
validColor () {
|
||||||
return hex2rgb(this.value || this.fallback)
|
return hex2rgb(this.modelValue || this.fallback)
|
||||||
},
|
},
|
||||||
transparentColor () {
|
transparentColor () {
|
||||||
return this.value === 'transparent'
|
return this.modelValue === 'transparent'
|
||||||
},
|
},
|
||||||
computedColor () {
|
computedColor () {
|
||||||
return this.value && this.value.startsWith('--')
|
return this.modelValue && this.modelValue.startsWith('--')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,13 +8,13 @@
|
||||||
class="rating"
|
class="rating"
|
||||||
>
|
>
|
||||||
<span v-if="contrast.aaa">
|
<span v-if="contrast.aaa">
|
||||||
<i class="icon-thumbs-up-alt" />
|
<FAIcon icon="thumbs-up" />
|
||||||
</span>
|
</span>
|
||||||
<span v-if="!contrast.aaa && contrast.aa">
|
<span v-if="!contrast.aaa && contrast.aa">
|
||||||
<i class="icon-adjust" />
|
<FAIcon icon="adjust" />
|
||||||
</span>
|
</span>
|
||||||
<span v-if="!contrast.aaa && !contrast.aa">
|
<span v-if="!contrast.aaa && !contrast.aa">
|
||||||
<i class="icon-attention" />
|
<FAIcon icon="exclamation-triangle" />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
|
@ -23,29 +23,45 @@
|
||||||
:title="hint_18pt"
|
:title="hint_18pt"
|
||||||
>
|
>
|
||||||
<span v-if="contrast.laaa">
|
<span v-if="contrast.laaa">
|
||||||
<i class="icon-thumbs-up-alt" />
|
<FAIcon icon="thumbs-up" />
|
||||||
</span>
|
</span>
|
||||||
<span v-if="!contrast.laaa && contrast.laa">
|
<span v-if="!contrast.laaa && contrast.laa">
|
||||||
<i class="icon-adjust" />
|
<FAIcon icon="adjust" />
|
||||||
</span>
|
</span>
|
||||||
<span v-if="!contrast.laaa && !contrast.laa">
|
<span v-if="!contrast.laaa && !contrast.laa">
|
||||||
<i class="icon-attention" />
|
<FAIcon icon="exclamation-triangle" />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faAdjust,
|
||||||
|
faExclamationTriangle,
|
||||||
|
faThumbsUp
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faAdjust,
|
||||||
|
faExclamationTriangle,
|
||||||
|
faThumbsUp
|
||||||
|
)
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
large: {
|
large: {
|
||||||
required: false
|
required: false,
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
},
|
},
|
||||||
// TODO: Make theme switcher compute theme initially so that contrast
|
// TODO: Make theme switcher compute theme initially so that contrast
|
||||||
// component won't be called without contrast data
|
// component won't be called without contrast data
|
||||||
contrast: {
|
contrast: {
|
||||||
required: false,
|
required: false,
|
||||||
type: Object
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -82,6 +98,7 @@ export default {
|
||||||
.rating {
|
.rating {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
margin-left: 0.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,19 @@
|
||||||
import { reduce, filter, findIndex, clone, get } from 'lodash'
|
import { reduce, filter, findIndex, clone, get } from 'lodash'
|
||||||
import Status from '../status/status.vue'
|
import Status from '../status/status.vue'
|
||||||
|
import ThreadTree from '../thread_tree/thread_tree.vue'
|
||||||
|
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faAngleDoubleDown,
|
||||||
|
faAngleDoubleLeft,
|
||||||
|
faChevronLeft
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faAngleDoubleDown,
|
||||||
|
faAngleDoubleLeft,
|
||||||
|
faChevronLeft
|
||||||
|
)
|
||||||
|
|
||||||
const sortById = (a, b) => {
|
const sortById = (a, b) => {
|
||||||
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
|
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
|
||||||
|
@ -35,7 +49,10 @@ const conversation = {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
highlight: null,
|
highlight: null,
|
||||||
expanded: false
|
expanded: false,
|
||||||
|
threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
|
||||||
|
statusContentPropertiesObject: {},
|
||||||
|
inlineDivePosition: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: [
|
props: [
|
||||||
|
@ -44,7 +61,8 @@ const conversation = {
|
||||||
'isPage',
|
'isPage',
|
||||||
'pinnedStatusIdsObject',
|
'pinnedStatusIdsObject',
|
||||||
'inProfile',
|
'inProfile',
|
||||||
'profileUserId'
|
'profileUserId',
|
||||||
|
'virtualHidden'
|
||||||
],
|
],
|
||||||
created () {
|
created () {
|
||||||
if (this.isPage) {
|
if (this.isPage) {
|
||||||
|
@ -52,6 +70,51 @@ const conversation = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
maxDepthToShowByDefault () {
|
||||||
|
// maxDepthInThread = max number of depths that is *visible*
|
||||||
|
// since our depth starts with 0 and "showing" means "showing children"
|
||||||
|
// there is a -2 here
|
||||||
|
const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
|
||||||
|
return maxDepth >= 1 ? maxDepth : 1
|
||||||
|
},
|
||||||
|
displayStyle () {
|
||||||
|
return this.$store.getters.mergedConfig.conversationDisplay
|
||||||
|
},
|
||||||
|
isTreeView () {
|
||||||
|
return !this.isLinearView
|
||||||
|
},
|
||||||
|
treeViewIsSimple () {
|
||||||
|
return !this.$store.getters.mergedConfig.conversationTreeAdvanced
|
||||||
|
},
|
||||||
|
isLinearView () {
|
||||||
|
return this.displayStyle === 'linear'
|
||||||
|
},
|
||||||
|
shouldFadeAncestors () {
|
||||||
|
return this.$store.getters.mergedConfig.conversationTreeFadeAncestors
|
||||||
|
},
|
||||||
|
otherRepliesButtonPosition () {
|
||||||
|
return this.$store.getters.mergedConfig.conversationOtherRepliesButton
|
||||||
|
},
|
||||||
|
showOtherRepliesButtonBelowStatus () {
|
||||||
|
return this.otherRepliesButtonPosition === 'below'
|
||||||
|
},
|
||||||
|
showOtherRepliesButtonInsideStatus () {
|
||||||
|
return this.otherRepliesButtonPosition === 'inside'
|
||||||
|
},
|
||||||
|
suspendable () {
|
||||||
|
if (this.isTreeView) {
|
||||||
|
return Object.entries(this.statusContentProperties)
|
||||||
|
.every(([k, prop]) => !prop.replying && prop.mediaPlaying.length === 0)
|
||||||
|
}
|
||||||
|
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
|
||||||
|
return this.$refs.statusComponent.every(s => s.suspendable)
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hideStatus () {
|
||||||
|
return this.virtualHidden && this.suspendable
|
||||||
|
},
|
||||||
status () {
|
status () {
|
||||||
return this.$store.state.statuses.allStatusesObject[this.statusId]
|
return this.$store.state.statuses.allStatusesObject[this.statusId]
|
||||||
},
|
},
|
||||||
|
@ -82,6 +145,121 @@ const conversation = {
|
||||||
|
|
||||||
return sortAndFilterConversation(conversation, this.status)
|
return sortAndFilterConversation(conversation, this.status)
|
||||||
},
|
},
|
||||||
|
statusMap () {
|
||||||
|
return this.conversation.reduce((res, s) => {
|
||||||
|
res[s.id] = s
|
||||||
|
return res
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
threadTree () {
|
||||||
|
const reverseLookupTable = this.conversation.reduce((table, status, index) => {
|
||||||
|
table[status.id] = index
|
||||||
|
return table
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const threads = this.conversation.reduce((a, cur) => {
|
||||||
|
const id = cur.id
|
||||||
|
a.forest[id] = this.getReplies(id)
|
||||||
|
.map(s => s.id)
|
||||||
|
|
||||||
|
return a
|
||||||
|
}, {
|
||||||
|
forest: {}
|
||||||
|
})
|
||||||
|
|
||||||
|
const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => {
|
||||||
|
if (processed[id]) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
processed[id] = true
|
||||||
|
return [{
|
||||||
|
status: this.conversation[reverseLookupTable[id]],
|
||||||
|
id,
|
||||||
|
depth
|
||||||
|
}, walk(forest, forest[id], depth + 1, processed)].reduce((a, b) => a.concat(b), [])
|
||||||
|
}).reduce((a, b) => a.concat(b), [])
|
||||||
|
|
||||||
|
const linearized = walk(threads.forest, this.topLevel.map(k => k.id))
|
||||||
|
|
||||||
|
return linearized
|
||||||
|
},
|
||||||
|
replyIds () {
|
||||||
|
return this.conversation.map(k => k.id)
|
||||||
|
.reduce((res, id) => {
|
||||||
|
res[id] = (this.replies[id] || []).map(k => k.id)
|
||||||
|
return res
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
totalReplyCount () {
|
||||||
|
const sizes = {}
|
||||||
|
const subTreeSizeFor = (id) => {
|
||||||
|
if (sizes[id]) {
|
||||||
|
return sizes[id]
|
||||||
|
}
|
||||||
|
sizes[id] = 1 + this.replyIds[id].map(cid => subTreeSizeFor(cid)).reduce((a, b) => a + b, 0)
|
||||||
|
return sizes[id]
|
||||||
|
}
|
||||||
|
this.conversation.map(k => k.id).map(subTreeSizeFor)
|
||||||
|
return Object.keys(sizes).reduce((res, id) => {
|
||||||
|
res[id] = sizes[id] - 1 // exclude itself
|
||||||
|
return res
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
totalReplyDepth () {
|
||||||
|
const depths = {}
|
||||||
|
const subTreeDepthFor = (id) => {
|
||||||
|
if (depths[id]) {
|
||||||
|
return depths[id]
|
||||||
|
}
|
||||||
|
depths[id] = 1 + this.replyIds[id].map(cid => subTreeDepthFor(cid)).reduce((a, b) => a > b ? a : b, 0)
|
||||||
|
return depths[id]
|
||||||
|
}
|
||||||
|
this.conversation.map(k => k.id).map(subTreeDepthFor)
|
||||||
|
return Object.keys(depths).reduce((res, id) => {
|
||||||
|
res[id] = depths[id] - 1 // exclude itself
|
||||||
|
return res
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
depths () {
|
||||||
|
return this.threadTree.reduce((a, k) => {
|
||||||
|
a[k.id] = k.depth
|
||||||
|
return a
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
topLevel () {
|
||||||
|
const topLevel = this.conversation.reduce((tl, cur) =>
|
||||||
|
tl.filter(k => this.getReplies(cur.id).map(v => v.id).indexOf(k.id) === -1), this.conversation)
|
||||||
|
return topLevel
|
||||||
|
},
|
||||||
|
otherTopLevelCount () {
|
||||||
|
return this.topLevel.length - 1
|
||||||
|
},
|
||||||
|
showingTopLevel () {
|
||||||
|
if (this.canDive && this.diveRoot) {
|
||||||
|
return [this.statusMap[this.diveRoot]]
|
||||||
|
}
|
||||||
|
return this.topLevel
|
||||||
|
},
|
||||||
|
diveRoot () {
|
||||||
|
const statusId = this.inlineDivePosition || this.statusId
|
||||||
|
const isTopLevel = !this.parentOf(statusId)
|
||||||
|
return isTopLevel ? null : statusId
|
||||||
|
},
|
||||||
|
diveDepth () {
|
||||||
|
return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0
|
||||||
|
},
|
||||||
|
diveMode () {
|
||||||
|
return this.canDive && !!this.diveRoot
|
||||||
|
},
|
||||||
|
shouldShowAllConversationButton () {
|
||||||
|
// The "show all conversation" button tells the user that there exist
|
||||||
|
// other toplevel statuses, so do not show it if there is only a single root
|
||||||
|
return this.isTreeView && this.isExpanded && this.diveMode && this.topLevel.length > 1
|
||||||
|
},
|
||||||
|
shouldShowAncestors () {
|
||||||
|
return this.isTreeView && this.isExpanded && this.ancestorsOf(this.diveRoot).length
|
||||||
|
},
|
||||||
replies () {
|
replies () {
|
||||||
let i = 1
|
let i = 1
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
|
@ -101,11 +279,71 @@ const conversation = {
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
isExpanded () {
|
isExpanded () {
|
||||||
return this.expanded || this.isPage
|
return !!(this.expanded || this.isPage)
|
||||||
|
},
|
||||||
|
hiddenStyle () {
|
||||||
|
const height = (this.status && this.status.virtualHeight) || '120px'
|
||||||
|
return this.virtualHidden ? { height } : {}
|
||||||
|
},
|
||||||
|
threadDisplayStatus () {
|
||||||
|
return this.conversation.reduce((a, k) => {
|
||||||
|
const id = k.id
|
||||||
|
const depth = this.depths[id]
|
||||||
|
const status = (() => {
|
||||||
|
if (this.threadDisplayStatusObject[id]) {
|
||||||
|
return this.threadDisplayStatusObject[id]
|
||||||
|
}
|
||||||
|
if ((depth - this.diveDepth) <= this.maxDepthToShowByDefault) {
|
||||||
|
return 'showing'
|
||||||
|
} else {
|
||||||
|
return 'hidden'
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
a[id] = status
|
||||||
|
return a
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
statusContentProperties () {
|
||||||
|
return this.conversation.reduce((a, k) => {
|
||||||
|
const id = k.id
|
||||||
|
const props = (() => {
|
||||||
|
const def = {
|
||||||
|
showingTall: false,
|
||||||
|
expandingSubject: false,
|
||||||
|
showingLongSubject: false,
|
||||||
|
isReplying: false,
|
||||||
|
mediaPlaying: []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.statusContentPropertiesObject[id]) {
|
||||||
|
return {
|
||||||
|
...def,
|
||||||
|
...this.statusContentPropertiesObject[id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
})()
|
||||||
|
|
||||||
|
a[id] = props
|
||||||
|
return a
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
canDive () {
|
||||||
|
return this.isTreeView && this.isExpanded
|
||||||
|
},
|
||||||
|
focused () {
|
||||||
|
return (id) => {
|
||||||
|
return (this.isExpanded) && id === this.highlight
|
||||||
|
}
|
||||||
|
},
|
||||||
|
maybeHighlight () {
|
||||||
|
return this.isExpanded ? this.highlight : null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Status
|
Status,
|
||||||
|
ThreadTree
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
statusId (newVal, oldVal) {
|
statusId (newVal, oldVal) {
|
||||||
|
@ -120,7 +358,15 @@ const conversation = {
|
||||||
expanded (value) {
|
expanded (value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
this.fetchConversation()
|
this.fetchConversation()
|
||||||
|
} else {
|
||||||
|
this.resetDisplayState()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
virtualHidden (value) {
|
||||||
|
this.$store.dispatch(
|
||||||
|
'setVirtualHeight',
|
||||||
|
{ statusId: this.statusId, height: `${this.$el.clientHeight}px` }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -143,8 +389,8 @@ const conversation = {
|
||||||
getReplies (id) {
|
getReplies (id) {
|
||||||
return this.replies[id] || []
|
return this.replies[id] || []
|
||||||
},
|
},
|
||||||
focused (id) {
|
getHighlight () {
|
||||||
return (this.isExpanded) && id === this.statusId
|
return this.isExpanded ? this.highlight : null
|
||||||
},
|
},
|
||||||
setHighlight (id) {
|
setHighlight (id) {
|
||||||
if (!id) return
|
if (!id) return
|
||||||
|
@ -152,15 +398,139 @@ const conversation = {
|
||||||
this.$store.dispatch('fetchFavsAndRepeats', id)
|
this.$store.dispatch('fetchFavsAndRepeats', id)
|
||||||
this.$store.dispatch('fetchEmojiReactionsBy', id)
|
this.$store.dispatch('fetchEmojiReactionsBy', id)
|
||||||
},
|
},
|
||||||
getHighlight () {
|
|
||||||
return this.isExpanded ? this.highlight : null
|
|
||||||
},
|
|
||||||
toggleExpanded () {
|
toggleExpanded () {
|
||||||
this.expanded = !this.expanded
|
this.expanded = !this.expanded
|
||||||
},
|
},
|
||||||
getConversationId (statusId) {
|
getConversationId (statusId) {
|
||||||
const status = this.$store.state.statuses.allStatusesObject[statusId]
|
const status = this.$store.state.statuses.allStatusesObject[statusId]
|
||||||
return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id'))
|
return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id'))
|
||||||
|
},
|
||||||
|
setThreadDisplay (id, nextStatus) {
|
||||||
|
this.threadDisplayStatusObject = {
|
||||||
|
...this.threadDisplayStatusObject,
|
||||||
|
[id]: nextStatus
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleThreadDisplay (id) {
|
||||||
|
const curStatus = this.threadDisplayStatus[id]
|
||||||
|
const nextStatus = curStatus === 'showing' ? 'hidden' : 'showing'
|
||||||
|
this.setThreadDisplay(id, nextStatus)
|
||||||
|
},
|
||||||
|
setThreadDisplayRecursively (id, nextStatus) {
|
||||||
|
this.setThreadDisplay(id, nextStatus)
|
||||||
|
this.getReplies(id).map(k => k.id).map(id => this.setThreadDisplayRecursively(id, nextStatus))
|
||||||
|
},
|
||||||
|
showThreadRecursively (id) {
|
||||||
|
this.setThreadDisplayRecursively(id, 'showing')
|
||||||
|
},
|
||||||
|
setStatusContentProperty (id, name, value) {
|
||||||
|
this.statusContentPropertiesObject = {
|
||||||
|
...this.statusContentPropertiesObject,
|
||||||
|
[id]: {
|
||||||
|
...this.statusContentPropertiesObject[id],
|
||||||
|
[name]: value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleStatusContentProperty (id, name) {
|
||||||
|
this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name])
|
||||||
|
},
|
||||||
|
leastVisibleAncestor (id) {
|
||||||
|
let cur = id
|
||||||
|
let parent = this.parentOf(cur)
|
||||||
|
while (cur) {
|
||||||
|
// if the parent is showing it means cur is visible
|
||||||
|
if (this.threadDisplayStatus[parent] === 'showing') {
|
||||||
|
return cur
|
||||||
|
}
|
||||||
|
parent = this.parentOf(parent)
|
||||||
|
cur = this.parentOf(cur)
|
||||||
|
}
|
||||||
|
// nothing found, fall back to toplevel
|
||||||
|
return this.topLevel[0] ? this.topLevel[0].id : undefined
|
||||||
|
},
|
||||||
|
diveIntoStatus (id, preventScroll) {
|
||||||
|
this.tryScrollTo(id)
|
||||||
|
},
|
||||||
|
diveToTopLevel () {
|
||||||
|
this.tryScrollTo(this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id)
|
||||||
|
},
|
||||||
|
// only used when we are not on a page
|
||||||
|
undive () {
|
||||||
|
this.inlineDivePosition = null
|
||||||
|
this.setHighlight(this.statusId)
|
||||||
|
},
|
||||||
|
tryScrollTo (id) {
|
||||||
|
if (!id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.isPage) {
|
||||||
|
// set statusId
|
||||||
|
this.$router.push({ name: 'conversation', params: { id } })
|
||||||
|
} else {
|
||||||
|
this.inlineDivePosition = id
|
||||||
|
}
|
||||||
|
// Because the conversation can be unmounted when out of sight
|
||||||
|
// and mounted again when it comes into sight,
|
||||||
|
// the `mounted` or `created` function in `status` should not
|
||||||
|
// contain scrolling calls, as we do not want the page to jump
|
||||||
|
// when we scroll with an expanded conversation.
|
||||||
|
//
|
||||||
|
// Now the method is to rely solely on the `highlight` watcher
|
||||||
|
// in `status` components.
|
||||||
|
// In linear views, all statuses are rendered at all times, but
|
||||||
|
// in tree views, it is possible that a change in active status
|
||||||
|
// removes and adds status components (e.g. an originally child
|
||||||
|
// status becomes an ancestor status, and thus they will be
|
||||||
|
// different).
|
||||||
|
// Here, let the components be rendered first, in order to trigger
|
||||||
|
// the `highlight` watcher.
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.setHighlight(id)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goToCurrent () {
|
||||||
|
this.tryScrollTo(this.diveRoot || this.topLevel[0].id)
|
||||||
|
},
|
||||||
|
statusById (id) {
|
||||||
|
return this.statusMap[id]
|
||||||
|
},
|
||||||
|
parentOf (id) {
|
||||||
|
const status = this.statusById(id)
|
||||||
|
if (!status) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const { in_reply_to_status_id: parentId } = status
|
||||||
|
if (!this.statusMap[parentId]) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return parentId
|
||||||
|
},
|
||||||
|
parentOrSelf (id) {
|
||||||
|
return this.parentOf(id) || id
|
||||||
|
},
|
||||||
|
// Ancestors of some status, from top to bottom
|
||||||
|
ancestorsOf (id) {
|
||||||
|
const ancestors = []
|
||||||
|
let cur = this.parentOf(id)
|
||||||
|
while (cur) {
|
||||||
|
ancestors.unshift(this.statusMap[cur])
|
||||||
|
cur = this.parentOf(cur)
|
||||||
|
}
|
||||||
|
return ancestors
|
||||||
|
},
|
||||||
|
topLevelAncestorOrSelfId (id) {
|
||||||
|
let cur = id
|
||||||
|
let parent = this.parentOf(id)
|
||||||
|
while (parent) {
|
||||||
|
cur = this.parentOf(cur)
|
||||||
|
parent = this.parentOf(parent)
|
||||||
|
}
|
||||||
|
return cur
|
||||||
|
},
|
||||||
|
resetDisplayState () {
|
||||||
|
this.undive()
|
||||||
|
this.threadDisplayStatusObject = {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
|
v-if="!hideStatus"
|
||||||
|
:style="hiddenStyle"
|
||||||
class="Conversation"
|
class="Conversation"
|
||||||
:class="{ '-expanded' : isExpanded, 'panel' : isExpanded }"
|
:class="{ '-expanded' : isExpanded, 'panel' : isExpanded }"
|
||||||
>
|
>
|
||||||
|
@ -8,31 +10,189 @@
|
||||||
class="panel-heading conversation-heading"
|
class="panel-heading conversation-heading"
|
||||||
>
|
>
|
||||||
<span class="title"> {{ $t('timeline.conversation') }} </span>
|
<span class="title"> {{ $t('timeline.conversation') }} </span>
|
||||||
<span v-if="collapsable">
|
<button
|
||||||
<a
|
v-if="collapsable"
|
||||||
href="#"
|
class="button-unstyled -link"
|
||||||
@click.prevent="toggleExpanded"
|
@click.prevent="toggleExpanded"
|
||||||
>{{ $t('timeline.collapse') }}</a>
|
>
|
||||||
</span>
|
{{ $t('timeline.collapse') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="conversation-body panel-body">
|
||||||
|
<div
|
||||||
|
v-if="isTreeView"
|
||||||
|
class="thread-body"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="shouldShowAllConversationButton"
|
||||||
|
class="conversation-dive-to-top-level-box"
|
||||||
|
>
|
||||||
|
<i18n-t
|
||||||
|
keypath="status.show_all_conversation_with_icon"
|
||||||
|
tag="button"
|
||||||
|
class="button-unstyled -link"
|
||||||
|
@click.prevent="diveToTopLevel"
|
||||||
|
scope="global"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<FAIcon
|
||||||
|
icon="angle-double-left"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #text>
|
||||||
|
<span>
|
||||||
|
{{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="shouldShowAncestors"
|
||||||
|
class="thread-ancestors"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="status in ancestorsOf(diveRoot)"
|
||||||
|
:key="status.id"
|
||||||
|
class="thread-ancestor"
|
||||||
|
:class="{'thread-ancestor-has-other-replies': getReplies(status.id).length > 1, '-faded': shouldFadeAncestors}"
|
||||||
|
>
|
||||||
|
<status
|
||||||
|
ref="statusComponent"
|
||||||
|
:inline-expanded="collapsable && isExpanded"
|
||||||
|
:statusoid="status"
|
||||||
|
:expandable="!isExpanded"
|
||||||
|
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||||
|
:focused="focused(status.id)"
|
||||||
|
:in-conversation="isExpanded"
|
||||||
|
:highlight="getHighlight()"
|
||||||
|
:replies="getReplies(status.id)"
|
||||||
|
:in-profile="inProfile"
|
||||||
|
:profile-user-id="profileUserId"
|
||||||
|
class="conversation-status status-fadein panel-body"
|
||||||
|
|
||||||
|
:simple-tree="treeViewIsSimple"
|
||||||
|
:toggle-thread-display="toggleThreadDisplay"
|
||||||
|
:thread-display-status="threadDisplayStatus"
|
||||||
|
:show-thread-recursively="showThreadRecursively"
|
||||||
|
:total-reply-count="totalReplyCount"
|
||||||
|
:total-reply-depth="totalReplyDepth"
|
||||||
|
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
|
||||||
|
:dive="() => diveIntoStatus(status.id)"
|
||||||
|
|
||||||
|
:controlled-showing-tall="statusContentProperties[status.id].showingTall"
|
||||||
|
:controlled-expanding-subject="statusContentProperties[status.id].expandingSubject"
|
||||||
|
:controlled-showing-long-subject="statusContentProperties[status.id].showingLongSubject"
|
||||||
|
:controlled-replying="statusContentProperties[status.id].replying"
|
||||||
|
:controlled-media-playing="statusContentProperties[status.id].mediaPlaying"
|
||||||
|
:controlled-toggle-showing-tall="() => toggleStatusContentProperty(status.id, 'showingTall')"
|
||||||
|
:controlled-toggle-expanding-subject="() => toggleStatusContentProperty(status.id, 'expandingSubject')"
|
||||||
|
:controlled-toggle-showing-long-subject="() => toggleStatusContentProperty(status.id, 'showingLongSubject')"
|
||||||
|
:controlled-toggle-replying="() => toggleStatusContentProperty(status.id, 'replying')"
|
||||||
|
:controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)"
|
||||||
|
|
||||||
|
@goto="setHighlight"
|
||||||
|
@toggleExpanded="toggleExpanded"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
|
||||||
|
class="thread-ancestor-dive-box"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="thread-ancestor-dive-box-inner"
|
||||||
|
>
|
||||||
|
<i18n-t
|
||||||
|
tag="button"
|
||||||
|
scope="global"
|
||||||
|
keypath="status.ancestor_follow_with_icon"
|
||||||
|
class="button-unstyled -link thread-tree-show-replies-button"
|
||||||
|
@click.prevent="diveIntoStatus(status.id)"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<FAIcon
|
||||||
|
icon="angle-double-right"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #text>
|
||||||
|
<span>
|
||||||
|
{{ $tc('status.ancestor_follow', getReplies(status.id).length - 1, { numReplies: getReplies(status.id).length - 1 }) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<thread-tree
|
||||||
|
v-for="status in showingTopLevel"
|
||||||
|
:key="status.id"
|
||||||
|
ref="statusComponent"
|
||||||
|
:depth="0"
|
||||||
|
|
||||||
|
:status="status"
|
||||||
|
:in-profile="inProfile"
|
||||||
|
:conversation="conversation"
|
||||||
|
:collapsable="collapsable"
|
||||||
|
:is-expanded="isExpanded"
|
||||||
|
:pinned-status-ids-object="pinnedStatusIdsObject"
|
||||||
|
:profile-user-id="profileUserId"
|
||||||
|
|
||||||
|
:focused="focused"
|
||||||
|
:get-replies="getReplies"
|
||||||
|
:highlight="maybeHighlight"
|
||||||
|
:set-highlight="setHighlight"
|
||||||
|
:toggle-expanded="toggleExpanded"
|
||||||
|
|
||||||
|
:simple="treeViewIsSimple"
|
||||||
|
:toggle-thread-display="toggleThreadDisplay"
|
||||||
|
:thread-display-status="threadDisplayStatus"
|
||||||
|
:show-thread-recursively="showThreadRecursively"
|
||||||
|
:total-reply-count="totalReplyCount"
|
||||||
|
:total-reply-depth="totalReplyDepth"
|
||||||
|
:status-content-properties="statusContentProperties"
|
||||||
|
:set-status-content-property="setStatusContentProperty"
|
||||||
|
:toggle-status-content-property="toggleStatusContentProperty"
|
||||||
|
:dive="canDive ? diveIntoStatus : undefined"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="isLinearView"
|
||||||
|
class="thread-body"
|
||||||
|
>
|
||||||
|
<status
|
||||||
|
v-for="status in conversation"
|
||||||
|
:key="status.id"
|
||||||
|
ref="statusComponent"
|
||||||
|
:inline-expanded="collapsable && isExpanded"
|
||||||
|
:statusoid="status"
|
||||||
|
:expandable="!isExpanded"
|
||||||
|
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||||
|
:focused="focused(status.id)"
|
||||||
|
:in-conversation="isExpanded"
|
||||||
|
:highlight="getHighlight()"
|
||||||
|
:replies="getReplies(status.id)"
|
||||||
|
:in-profile="inProfile"
|
||||||
|
:profile-user-id="profileUserId"
|
||||||
|
class="conversation-status status-fadein panel-body"
|
||||||
|
|
||||||
|
:toggle-thread-display="toggleThreadDisplay"
|
||||||
|
:thread-display-status="threadDisplayStatus"
|
||||||
|
:show-thread-recursively="showThreadRecursively"
|
||||||
|
:total-reply-count="totalReplyCount"
|
||||||
|
:total-reply-depth="totalReplyDepth"
|
||||||
|
:status-content-properties="statusContentProperties"
|
||||||
|
:set-status-content-property="setStatusContentProperty"
|
||||||
|
:toggle-status-content-property="toggleStatusContentProperty"
|
||||||
|
|
||||||
|
@goto="setHighlight"
|
||||||
|
@toggleExpanded="toggleExpanded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<status
|
|
||||||
v-for="status in conversation"
|
|
||||||
:key="status.id"
|
|
||||||
:inline-expanded="collapsable && isExpanded"
|
|
||||||
:statusoid="status"
|
|
||||||
:expandable="!isExpanded"
|
|
||||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
|
||||||
:focused="focused(status.id)"
|
|
||||||
:in-conversation="isExpanded"
|
|
||||||
:highlight="getHighlight()"
|
|
||||||
:replies="getReplies(status.id)"
|
|
||||||
:in-profile="inProfile"
|
|
||||||
:profile-user-id="profileUserId"
|
|
||||||
class="conversation-status status-fadein panel-body"
|
|
||||||
@goto="setHighlight"
|
|
||||||
@toggleExpanded="toggleExpanded"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
:style="hiddenStyle"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./conversation.js"></script>
|
<script src="./conversation.js"></script>
|
||||||
|
@ -41,27 +201,74 @@
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.Conversation {
|
.Conversation {
|
||||||
|
.conversation-dive-to-top-level-box {
|
||||||
|
padding: var(--status-margin, $status-margin);
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-bottom-color: var(--border, $fallback--border);
|
||||||
|
border-radius: 0;
|
||||||
|
/* Make the button stretch along the whole row */
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thread-ancestors {
|
||||||
|
margin-left: var(--status-margin, $status-margin);
|
||||||
|
border-left: 2px solid var(--border, $fallback--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thread-ancestor.-faded .StatusContent {
|
||||||
|
--link: var(--faintLink);
|
||||||
|
--text: var(--faint);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.thread-ancestor-dive-box {
|
||||||
|
padding-left: var(--status-margin, $status-margin);
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-bottom-color: var(--border, $fallback--border);
|
||||||
|
border-radius: 0;
|
||||||
|
/* Make the button stretch along the whole row */
|
||||||
|
&, &-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.thread-ancestor-dive-box-inner {
|
||||||
|
padding: var(--status-margin, $status-margin);
|
||||||
|
}
|
||||||
|
|
||||||
.conversation-status {
|
.conversation-status {
|
||||||
border-left: none;
|
|
||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
border-bottom-style: solid;
|
border-bottom-style: solid;
|
||||||
border-bottom-color: var(--border, $fallback--border);
|
border-bottom-color: var(--border, $fallback--border);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.-expanded {
|
.thread-ancestor-has-other-replies .conversation-status,
|
||||||
.conversation-status {
|
.thread-ancestor:last-child .conversation-status,
|
||||||
border-color: $fallback--border;
|
.thread-ancestor:last-child .thread-ancestor-dive-box,
|
||||||
border-color: var(--border, $fallback--border);
|
&.-expanded .thread-tree .conversation-status {
|
||||||
border-left: 4px solid $fallback--cRed;
|
border-bottom: none;
|
||||||
border-left: 4px solid var(--cRed, $fallback--cRed);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.conversation-status:last-child {
|
.thread-ancestors + .thread-tree > .conversation-status {
|
||||||
border-bottom: none;
|
border-top-width: 1px;
|
||||||
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
|
border-top-style: solid;
|
||||||
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
border-top-color: var(--border, $fallback--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* expanded conversation in timeline */
|
||||||
|
&.status-fadein.-expanded .thread-body {
|
||||||
|
border-left-width: 4px;
|
||||||
|
border-left-style: solid;
|
||||||
|
border-left-color: $fallback--cRed;
|
||||||
|
border-left-color: var(--cRed, $fallback--cRed);
|
||||||
|
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
|
||||||
|
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
||||||
|
border-bottom: 1px solid var(--border, $fallback--border);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
89
src/components/desktop_nav/desktop_nav.js
Normal file
89
src/components/desktop_nav/desktop_nav.js
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import SearchBar from 'components/search_bar/search_bar.vue'
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faSignInAlt,
|
||||||
|
faSignOutAlt,
|
||||||
|
faHome,
|
||||||
|
faComments,
|
||||||
|
faBell,
|
||||||
|
faUserPlus,
|
||||||
|
faBullhorn,
|
||||||
|
faSearch,
|
||||||
|
faTachometerAlt,
|
||||||
|
faCog,
|
||||||
|
faInfoCircle
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faSignInAlt,
|
||||||
|
faSignOutAlt,
|
||||||
|
faHome,
|
||||||
|
faComments,
|
||||||
|
faBell,
|
||||||
|
faUserPlus,
|
||||||
|
faBullhorn,
|
||||||
|
faSearch,
|
||||||
|
faTachometerAlt,
|
||||||
|
faCog,
|
||||||
|
faInfoCircle
|
||||||
|
)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
SearchBar
|
||||||
|
},
|
||||||
|
data: () => ({
|
||||||
|
searchBarHidden: true,
|
||||||
|
supportsMask: window.CSS && window.CSS.supports && (
|
||||||
|
window.CSS.supports('mask-size', 'contain') ||
|
||||||
|
window.CSS.supports('-webkit-mask-size', 'contain') ||
|
||||||
|
window.CSS.supports('-moz-mask-size', 'contain') ||
|
||||||
|
window.CSS.supports('-ms-mask-size', 'contain') ||
|
||||||
|
window.CSS.supports('-o-mask-size', 'contain')
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
enableMask () { return this.supportsMask && this.$store.state.instance.logoMask },
|
||||||
|
logoStyle () {
|
||||||
|
return {
|
||||||
|
'visibility': this.enableMask ? 'hidden' : 'visible'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
logoMaskStyle () {
|
||||||
|
return this.enableMask ? {
|
||||||
|
'mask-image': `url(${this.$store.state.instance.logo})`
|
||||||
|
} : {
|
||||||
|
'background-color': this.enableMask ? '' : 'transparent'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
logoBgStyle () {
|
||||||
|
return Object.assign({
|
||||||
|
'margin': `${this.$store.state.instance.logoMargin} 0`,
|
||||||
|
opacity: this.searchBarHidden ? 1 : 0
|
||||||
|
}, this.enableMask ? {} : {
|
||||||
|
'background-color': this.enableMask ? '' : 'transparent'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
logo () { return this.$store.state.instance.logo },
|
||||||
|
sitename () { return this.$store.state.instance.name },
|
||||||
|
hideSitename () { return this.$store.state.instance.hideSitename },
|
||||||
|
logoLeft () { return this.$store.state.instance.logoLeft },
|
||||||
|
currentUser () { return this.$store.state.users.currentUser },
|
||||||
|
privateMode () { return this.$store.state.instance.private }
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
scrollToTop () {
|
||||||
|
window.scrollTo(0, 0)
|
||||||
|
},
|
||||||
|
logout () {
|
||||||
|
this.$router.replace('/main/public')
|
||||||
|
this.$store.dispatch('logout')
|
||||||
|
},
|
||||||
|
onSearchBarToggled (hidden) {
|
||||||
|
this.searchBarHidden = hidden
|
||||||
|
},
|
||||||
|
openSettingsModal () {
|
||||||
|
this.$store.dispatch('openSettingsModal')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
117
src/components/desktop_nav/desktop_nav.scss
Normal file
117
src/components/desktop_nav/desktop_nav.scss
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.DesktopNav {
|
||||||
|
height: 50px;
|
||||||
|
width: 100%;
|
||||||
|
position: fixed;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--topBarLink, $fallback--link);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-nav {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 50px;
|
||||||
|
grid-template-columns: 2fr auto 2fr;
|
||||||
|
grid-template-areas: "sitename logo actions";
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0 1.2em;
|
||||||
|
margin: auto;
|
||||||
|
max-width: 980px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-logoLeft {
|
||||||
|
grid-template-columns: auto 2fr 2fr;
|
||||||
|
grid-template-areas: "logo sitename actions";
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-default {
|
||||||
|
&, svg {
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--btnTopBarText, $fallback--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: $fallback--fg;
|
||||||
|
background-color: var(--btnPressedTopBar, $fallback--fg);
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--btnPressedTopBarText, $fallback--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--btnDisabledTopBarText, $fallback--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.toggled {
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--btnToggledTopBarText, $fallback--text);
|
||||||
|
background-color: $fallback--fg;
|
||||||
|
background-color: var(--btnToggledTopBar, $fallback--fg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
grid-area: logo;
|
||||||
|
position: relative;
|
||||||
|
transition: opacity;
|
||||||
|
transition-timing-function: ease-out;
|
||||||
|
transition-duration: 100ms;
|
||||||
|
|
||||||
|
@media all and (min-width: 800px) {
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mask {
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
background-color: $fallback--fg;
|
||||||
|
background-color: var(--topBarText, $fallback--fg);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: inline-block;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
margin-left: 0.2em;
|
||||||
|
width: 2em;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.svg-inline--fa {
|
||||||
|
color: $fallback--link;
|
||||||
|
color: var(--topBarLink, $fallback--link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sitename {
|
||||||
|
grid-area: sitename;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
grid-area: actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
flex: 1;
|
||||||
|
line-height: 50px;
|
||||||
|
height: 50px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
&.right {
|
||||||
|
justify-content: flex-end;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
82
src/components/desktop_nav/desktop_nav.vue
Normal file
82
src/components/desktop_nav/desktop_nav.vue
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
<template>
|
||||||
|
<nav
|
||||||
|
id="nav"
|
||||||
|
class="DesktopNav"
|
||||||
|
:class="{ '-logoLeft': logoLeft }"
|
||||||
|
@click="scrollToTop()"
|
||||||
|
>
|
||||||
|
<div class="inner-nav">
|
||||||
|
<div class="item sitename">
|
||||||
|
<router-link
|
||||||
|
v-if="!hideSitename"
|
||||||
|
class="site-name"
|
||||||
|
:to="{ name: 'root' }"
|
||||||
|
active-class="home"
|
||||||
|
>
|
||||||
|
{{ sitename }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<router-link
|
||||||
|
class="logo"
|
||||||
|
:to="{ name: 'root' }"
|
||||||
|
:style="logoBgStyle"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mask"
|
||||||
|
:style="logoMaskStyle"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
:src="logo"
|
||||||
|
:style="logoStyle"
|
||||||
|
>
|
||||||
|
</router-link>
|
||||||
|
<div class="item right actions">
|
||||||
|
<search-bar
|
||||||
|
v-if="currentUser || !privateMode"
|
||||||
|
@toggled="onSearchBarToggled"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="button-unstyled nav-icon"
|
||||||
|
@click.stop="openSettingsModal"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
icon="cog"
|
||||||
|
:title="$t('nav.preferences')"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
v-if="currentUser && currentUser.role === 'admin'"
|
||||||
|
href="/pleroma/admin/#/login-pleroma"
|
||||||
|
class="nav-icon"
|
||||||
|
target="_blank"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
icon="tachometer-alt"
|
||||||
|
:title="$t('nav.administration')"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
v-if="currentUser"
|
||||||
|
class="button-unstyled nav-icon"
|
||||||
|
@click.prevent="logout"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
icon="sign-out-alt"
|
||||||
|
:title="$t('login.logout')"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
<script src="./desktop_nav.js"></script>
|
||||||
|
|
||||||
|
<style src="./desktop_nav.scss" lang="scss"></style>
|
|
@ -6,20 +6,20 @@
|
||||||
<ProgressButton
|
<ProgressButton
|
||||||
v-if="muted"
|
v-if="muted"
|
||||||
:click="unmuteDomain"
|
:click="unmuteDomain"
|
||||||
class="btn btn-default"
|
class="btn button-default"
|
||||||
>
|
>
|
||||||
{{ $t('domain_mute_card.unmute') }}
|
{{ $t('domain_mute_card.unmute') }}
|
||||||
<template slot="progress">
|
<template v-slot:progress>
|
||||||
{{ $t('domain_mute_card.unmute_progress') }}
|
{{ $t('domain_mute_card.unmute_progress') }}
|
||||||
</template>
|
</template>
|
||||||
</ProgressButton>
|
</ProgressButton>
|
||||||
<ProgressButton
|
<ProgressButton
|
||||||
v-else
|
v-else
|
||||||
:click="muteDomain"
|
:click="muteDomain"
|
||||||
class="btn btn-default"
|
class="btn button-default"
|
||||||
>
|
>
|
||||||
{{ $t('domain_mute_card.mute') }}
|
{{ $t('domain_mute_card.mute') }}
|
||||||
<template slot="progress">
|
<template v-slot:progress>
|
||||||
{{ $t('domain_mute_card.mute_progress') }}
|
{{ $t('domain_mute_card.mute_progress') }}
|
||||||
</template>
|
</template>
|
||||||
</ProgressButton>
|
</ProgressButton>
|
||||||
|
|
|
@ -3,6 +3,15 @@ import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
||||||
import { take } from 'lodash'
|
import { take } from 'lodash'
|
||||||
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||||
|
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faSmileBeam
|
||||||
|
} from '@fortawesome/free-regular-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faSmileBeam
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EmojiInput - augmented inputs for emoji and autocomplete support in inputs
|
* EmojiInput - augmented inputs for emoji and autocomplete support in inputs
|
||||||
* without having to give up the comfort of <input/> and <textarea/> elements
|
* without having to give up the comfort of <input/> and <textarea/> elements
|
||||||
|
@ -22,6 +31,7 @@ import { findOffset } from '../../services/offset_finder/offset_finder.service.j
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const EmojiInput = {
|
const EmojiInput = {
|
||||||
|
emits: ['update:modelValue', 'shown'],
|
||||||
props: {
|
props: {
|
||||||
suggest: {
|
suggest: {
|
||||||
/**
|
/**
|
||||||
|
@ -48,7 +58,7 @@ const EmojiInput = {
|
||||||
required: true,
|
required: true,
|
||||||
type: Function
|
type: Function
|
||||||
},
|
},
|
||||||
value: {
|
modelValue: {
|
||||||
/**
|
/**
|
||||||
* Used for v-model
|
* Used for v-model
|
||||||
*/
|
*/
|
||||||
|
@ -105,7 +115,8 @@ const EmojiInput = {
|
||||||
showPicker: false,
|
showPicker: false,
|
||||||
temporarilyHideSuggestions: false,
|
temporarilyHideSuggestions: false,
|
||||||
keepOpen: false,
|
keepOpen: false,
|
||||||
disableClickOutside: false
|
disableClickOutside: false,
|
||||||
|
suggestions: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
@ -115,21 +126,6 @@ const EmojiInput = {
|
||||||
padEmoji () {
|
padEmoji () {
|
||||||
return this.$store.getters.mergedConfig.padEmoji
|
return this.$store.getters.mergedConfig.padEmoji
|
||||||
},
|
},
|
||||||
suggestions () {
|
|
||||||
const firstchar = this.textAtCaret.charAt(0)
|
|
||||||
if (this.textAtCaret === firstchar) { return [] }
|
|
||||||
const matchedSuggestions = this.suggest(this.textAtCaret)
|
|
||||||
if (matchedSuggestions.length <= 0) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
return take(matchedSuggestions, 5)
|
|
||||||
.map(({ imageUrl, ...rest }, index) => ({
|
|
||||||
...rest,
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
img: imageUrl || '',
|
|
||||||
highlighted: index === this.highlighted
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
showSuggestions () {
|
showSuggestions () {
|
||||||
return this.focused &&
|
return this.focused &&
|
||||||
this.suggestions &&
|
this.suggestions &&
|
||||||
|
@ -141,52 +137,78 @@ const EmojiInput = {
|
||||||
return (this.wordAtCaret || {}).word || ''
|
return (this.wordAtCaret || {}).word || ''
|
||||||
},
|
},
|
||||||
wordAtCaret () {
|
wordAtCaret () {
|
||||||
if (this.value && this.caret) {
|
if (this.modelValue && this.caret) {
|
||||||
const word = Completion.wordAtPosition(this.value, this.caret - 1) || {}
|
const word = Completion.wordAtPosition(this.modelValue, this.caret - 1) || {}
|
||||||
return word
|
return word
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
const slots = this.$slots.default
|
const { root } = this.$refs
|
||||||
if (!slots || slots.length === 0) return
|
const input = root.querySelector('.emoji-input > input') || root.querySelector('.emoji-input > textarea')
|
||||||
const input = slots.find(slot => ['input', 'textarea'].includes(slot.tag))
|
|
||||||
if (!input) return
|
if (!input) return
|
||||||
this.input = input
|
this.input = input
|
||||||
this.resize()
|
this.resize()
|
||||||
input.elm.addEventListener('blur', this.onBlur)
|
input.addEventListener('blur', this.onBlur)
|
||||||
input.elm.addEventListener('focus', this.onFocus)
|
input.addEventListener('focus', this.onFocus)
|
||||||
input.elm.addEventListener('paste', this.onPaste)
|
input.addEventListener('paste', this.onPaste)
|
||||||
input.elm.addEventListener('keyup', this.onKeyUp)
|
input.addEventListener('keyup', this.onKeyUp)
|
||||||
input.elm.addEventListener('keydown', this.onKeyDown)
|
input.addEventListener('keydown', this.onKeyDown)
|
||||||
input.elm.addEventListener('click', this.onClickInput)
|
input.addEventListener('click', this.onClickInput)
|
||||||
input.elm.addEventListener('transitionend', this.onTransition)
|
input.addEventListener('transitionend', this.onTransition)
|
||||||
input.elm.addEventListener('input', this.onInput)
|
input.addEventListener('input', this.onInput)
|
||||||
},
|
},
|
||||||
unmounted () {
|
unmounted () {
|
||||||
const { input } = this
|
const { input } = this
|
||||||
if (input) {
|
if (input) {
|
||||||
input.elm.removeEventListener('blur', this.onBlur)
|
input.removeEventListener('blur', this.onBlur)
|
||||||
input.elm.removeEventListener('focus', this.onFocus)
|
input.removeEventListener('focus', this.onFocus)
|
||||||
input.elm.removeEventListener('paste', this.onPaste)
|
input.removeEventListener('paste', this.onPaste)
|
||||||
input.elm.removeEventListener('keyup', this.onKeyUp)
|
input.removeEventListener('keyup', this.onKeyUp)
|
||||||
input.elm.removeEventListener('keydown', this.onKeyDown)
|
input.removeEventListener('keydown', this.onKeyDown)
|
||||||
input.elm.removeEventListener('click', this.onClickInput)
|
input.removeEventListener('click', this.onClickInput)
|
||||||
input.elm.removeEventListener('transitionend', this.onTransition)
|
input.removeEventListener('transitionend', this.onTransition)
|
||||||
input.elm.removeEventListener('input', this.onInput)
|
input.removeEventListener('input', this.onInput)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
showSuggestions: function (newValue) {
|
showSuggestions: function (newValue) {
|
||||||
this.$emit('shown', newValue)
|
this.$emit('shown', newValue)
|
||||||
|
},
|
||||||
|
textAtCaret: async function (newWord) {
|
||||||
|
const firstchar = newWord.charAt(0)
|
||||||
|
this.suggestions = []
|
||||||
|
if (newWord === firstchar) return
|
||||||
|
const matchedSuggestions = await this.suggest(newWord)
|
||||||
|
// Async: cancel if textAtCaret has changed during wait
|
||||||
|
if (this.textAtCaret !== newWord) return
|
||||||
|
if (matchedSuggestions.length <= 0) return
|
||||||
|
this.suggestions = take(matchedSuggestions, 5)
|
||||||
|
.map(({ imageUrl, ...rest }) => ({
|
||||||
|
...rest,
|
||||||
|
img: imageUrl || ''
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
suggestions: {
|
||||||
|
handler (newValue) {
|
||||||
|
this.$nextTick(this.resize)
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
focusPickerInput () {
|
||||||
|
const pickerEl = this.$refs.picker.$el
|
||||||
|
if (!pickerEl) return
|
||||||
|
const pickerInput = pickerEl.querySelector('input')
|
||||||
|
if (pickerInput) pickerInput.focus()
|
||||||
|
},
|
||||||
triggerShowPicker () {
|
triggerShowPicker () {
|
||||||
this.showPicker = true
|
this.showPicker = true
|
||||||
this.$refs.picker.startEmojiLoad()
|
this.$refs.picker.startEmojiLoad()
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.scrollIntoView()
|
this.scrollIntoView()
|
||||||
|
this.focusPickerInput()
|
||||||
})
|
})
|
||||||
// This temporarily disables "click outside" handler
|
// This temporarily disables "click outside" handler
|
||||||
// since external trigger also means click originates
|
// since external trigger also means click originates
|
||||||
|
@ -197,21 +219,22 @@ const EmojiInput = {
|
||||||
}, 0)
|
}, 0)
|
||||||
},
|
},
|
||||||
togglePicker () {
|
togglePicker () {
|
||||||
this.input.elm.focus()
|
this.input.focus()
|
||||||
this.showPicker = !this.showPicker
|
this.showPicker = !this.showPicker
|
||||||
if (this.showPicker) {
|
if (this.showPicker) {
|
||||||
this.scrollIntoView()
|
this.scrollIntoView()
|
||||||
this.$refs.picker.startEmojiLoad()
|
this.$refs.picker.startEmojiLoad()
|
||||||
|
this.$nextTick(this.focusPickerInput)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
replace (replacement) {
|
replace (replacement) {
|
||||||
const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement)
|
const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement)
|
||||||
this.$emit('input', newValue)
|
this.$emit('update:modelValue', newValue)
|
||||||
this.caret = 0
|
this.caret = 0
|
||||||
},
|
},
|
||||||
insert ({ insertion, keepOpen, surroundingSpace = true }) {
|
insert ({ insertion, keepOpen, surroundingSpace = true }) {
|
||||||
const before = this.value.substring(0, this.caret) || ''
|
const before = this.modelValue.substring(0, this.caret) || ''
|
||||||
const after = this.value.substring(this.caret) || ''
|
const after = this.modelValue.substring(this.caret) || ''
|
||||||
|
|
||||||
/* Using a bit more smart approach to padding emojis with spaces:
|
/* Using a bit more smart approach to padding emojis with spaces:
|
||||||
* - put a space before cursor if there isn't one already, unless we
|
* - put a space before cursor if there isn't one already, unless we
|
||||||
|
@ -239,16 +262,16 @@ const EmojiInput = {
|
||||||
after
|
after
|
||||||
].join('')
|
].join('')
|
||||||
this.keepOpen = keepOpen
|
this.keepOpen = keepOpen
|
||||||
this.$emit('input', newValue)
|
this.$emit('update:modelValue', newValue)
|
||||||
const position = this.caret + (insertion + spaceAfter + spaceBefore).length
|
const position = this.caret + (insertion + spaceAfter + spaceBefore).length
|
||||||
if (!keepOpen) {
|
if (!keepOpen) {
|
||||||
this.input.elm.focus()
|
this.input.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$nextTick(function () {
|
this.$nextTick(function () {
|
||||||
// Re-focus inputbox after clicking suggestion
|
// Re-focus inputbox after clicking suggestion
|
||||||
// Set selection right after the replacement instead of the very end
|
// Set selection right after the replacement instead of the very end
|
||||||
this.input.elm.setSelectionRange(position, position)
|
this.input.setSelectionRange(position, position)
|
||||||
this.caret = position
|
this.caret = position
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
@ -258,16 +281,16 @@ const EmojiInput = {
|
||||||
if (len > 0 || suggestion) {
|
if (len > 0 || suggestion) {
|
||||||
const chosenSuggestion = suggestion || this.suggestions[this.highlighted]
|
const chosenSuggestion = suggestion || this.suggestions[this.highlighted]
|
||||||
const replacement = chosenSuggestion.replacement
|
const replacement = chosenSuggestion.replacement
|
||||||
const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement)
|
const newValue = Completion.replaceWord(this.modelValue, this.wordAtCaret, replacement)
|
||||||
this.$emit('input', newValue)
|
this.$emit('update:modelValue', newValue)
|
||||||
this.highlighted = 0
|
this.highlighted = 0
|
||||||
const position = this.wordAtCaret.start + replacement.length
|
const position = this.wordAtCaret.start + replacement.length
|
||||||
|
|
||||||
this.$nextTick(function () {
|
this.$nextTick(function () {
|
||||||
// Re-focus inputbox after clicking suggestion
|
// Re-focus inputbox after clicking suggestion
|
||||||
this.input.elm.focus()
|
this.input.focus()
|
||||||
// Set selection right after the replacement instead of the very end
|
// Set selection right after the replacement instead of the very end
|
||||||
this.input.elm.setSelectionRange(position, position)
|
this.input.setSelectionRange(position, position)
|
||||||
this.caret = position
|
this.caret = position
|
||||||
})
|
})
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
@ -329,7 +352,7 @@ const EmojiInput = {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
const { offsetHeight } = this.input.elm
|
const { offsetHeight } = this.input
|
||||||
const { picker } = this.$refs
|
const { picker } = this.$refs
|
||||||
const pickerBottom = picker.$el.getBoundingClientRect().bottom
|
const pickerBottom = picker.$el.getBoundingClientRect().bottom
|
||||||
if (pickerBottom > window.innerHeight) {
|
if (pickerBottom > window.innerHeight) {
|
||||||
|
@ -394,8 +417,8 @@ const EmojiInput = {
|
||||||
|
|
||||||
// Scroll the input element to the position of the cursor
|
// Scroll the input element to the position of the cursor
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.input.elm.blur()
|
this.input.blur()
|
||||||
this.input.elm.focus()
|
this.input.focus()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Disable suggestions hotkeys if suggestions are hidden
|
// Disable suggestions hotkeys if suggestions are hidden
|
||||||
|
@ -424,7 +447,7 @@ const EmojiInput = {
|
||||||
// de-focuses the element (i.e. default browser behavior)
|
// de-focuses the element (i.e. default browser behavior)
|
||||||
if (key === 'Escape') {
|
if (key === 'Escape') {
|
||||||
if (!this.temporarilyHideSuggestions) {
|
if (!this.temporarilyHideSuggestions) {
|
||||||
this.input.elm.focus()
|
this.input.focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -435,7 +458,7 @@ const EmojiInput = {
|
||||||
this.showPicker = false
|
this.showPicker = false
|
||||||
this.setCaret(e)
|
this.setCaret(e)
|
||||||
this.resize()
|
this.resize()
|
||||||
this.$emit('input', e.target.value)
|
this.$emit('update:modelValue', e.target.value)
|
||||||
},
|
},
|
||||||
onClickInput (e) {
|
onClickInput (e) {
|
||||||
this.showPicker = false
|
this.showPicker = false
|
||||||
|
@ -460,7 +483,7 @@ const EmojiInput = {
|
||||||
if (!panel) return
|
if (!panel) return
|
||||||
const picker = this.$refs.picker.$el
|
const picker = this.$refs.picker.$el
|
||||||
const panelBody = this.$refs['panel-body']
|
const panelBody = this.$refs['panel-body']
|
||||||
const { offsetHeight, offsetTop } = this.input.elm
|
const { offsetHeight, offsetTop } = this.input
|
||||||
const offsetBottom = offsetTop + offsetHeight
|
const offsetBottom = offsetTop + offsetHeight
|
||||||
|
|
||||||
this.setPlacement(panelBody, panel, offsetBottom)
|
this.setPlacement(panelBody, panel, offsetBottom)
|
||||||
|
@ -474,7 +497,7 @@ const EmojiInput = {
|
||||||
|
|
||||||
if (this.placement === 'top' || (this.placement === 'auto' && this.overflowsBottom(container))) {
|
if (this.placement === 'top' || (this.placement === 'auto' && this.overflowsBottom(container))) {
|
||||||
target.style.top = 'auto'
|
target.style.top = 'auto'
|
||||||
target.style.bottom = this.input.elm.offsetHeight + 'px'
|
target.style.bottom = this.input.offsetHeight + 'px'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
overflowsBottom (el) {
|
overflowsBottom (el) {
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
|
ref="root"
|
||||||
v-click-outside="onClickOutside"
|
v-click-outside="onClickOutside"
|
||||||
class="emoji-input"
|
class="emoji-input"
|
||||||
:class="{ 'with-picker': !hideEmojiButton }"
|
:class="{ 'with-picker': !hideEmojiButton }"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
<template v-if="enableEmojiPicker">
|
<template v-if="enableEmojiPicker">
|
||||||
<div
|
<button
|
||||||
v-if="!hideEmojiButton"
|
v-if="!hideEmojiButton"
|
||||||
class="emoji-picker-icon"
|
class="button-unstyled emoji-picker-icon"
|
||||||
|
type="button"
|
||||||
@click.prevent="togglePicker"
|
@click.prevent="togglePicker"
|
||||||
>
|
>
|
||||||
<i class="icon-smile" />
|
<FAIcon :icon="['far', 'smile-beam']" />
|
||||||
</div>
|
</button>
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
v-if="enableEmojiPicker"
|
v-if="enableEmojiPicker"
|
||||||
ref="picker"
|
ref="picker"
|
||||||
|
@ -37,7 +39,7 @@
|
||||||
v-for="(suggestion, index) in suggestions"
|
v-for="(suggestion, index) in suggestions"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="autocomplete-item"
|
class="autocomplete-item"
|
||||||
:class="{ highlighted: suggestion.highlighted }"
|
:class="{ highlighted: index === highlighted }"
|
||||||
@click.stop.prevent="onClick($event, suggestion)"
|
@click.stop.prevent="onClick($event, suggestion)"
|
||||||
>
|
>
|
||||||
<span class="image">
|
<span class="image">
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { debounce } from 'lodash'
|
|
||||||
/**
|
/**
|
||||||
* suggest - generates a suggestor function to be used by emoji-input
|
* suggest - generates a suggestor function to be used by emoji-input
|
||||||
* data: object providing source information for specific types of suggestions:
|
* data: object providing source information for specific types of suggestions:
|
||||||
|
@ -11,19 +10,19 @@ import { debounce } from 'lodash'
|
||||||
* doesn't support user linking you can just provide only emoji.
|
* doesn't support user linking you can just provide only emoji.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const debounceUserSearch = debounce((data, input) => {
|
export default data => {
|
||||||
data.updateUsersList(input)
|
const emojiCurry = suggestEmoji(data.emoji)
|
||||||
}, 500)
|
const usersCurry = data.store && suggestUsers(data.store)
|
||||||
|
return input => {
|
||||||
export default data => input => {
|
const firstChar = input[0]
|
||||||
const firstChar = input[0]
|
if (firstChar === ':' && data.emoji) {
|
||||||
if (firstChar === ':' && data.emoji) {
|
return emojiCurry(input)
|
||||||
return suggestEmoji(data.emoji)(input)
|
}
|
||||||
|
if (firstChar === '@' && usersCurry) {
|
||||||
|
return usersCurry(input)
|
||||||
|
}
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
if (firstChar === '@' && data.users) {
|
|
||||||
return suggestUsers(data)(input)
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const suggestEmoji = emojis => input => {
|
export const suggestEmoji = emojis => input => {
|
||||||
|
@ -57,50 +56,75 @@ export const suggestEmoji = emojis => input => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const suggestUsers = data => input => {
|
export const suggestUsers = ({ dispatch, state }) => {
|
||||||
const noPrefix = input.toLowerCase().substr(1)
|
// Keep some persistent values in closure, most importantly for the
|
||||||
const users = data.users
|
// custom debounce to work. Lodash debounce does not return a promise.
|
||||||
|
let suggestions = []
|
||||||
|
let previousQuery = ''
|
||||||
|
let timeout = null
|
||||||
|
let cancelUserSearch = null
|
||||||
|
|
||||||
const newUsers = users.filter(
|
const userSearch = (query) => dispatch('searchUsers', { query })
|
||||||
user =>
|
const debounceUserSearch = (query) => {
|
||||||
user.screen_name.toLowerCase().startsWith(noPrefix) ||
|
cancelUserSearch && cancelUserSearch()
|
||||||
user.name.toLowerCase().startsWith(noPrefix)
|
return new Promise((resolve, reject) => {
|
||||||
|
timeout = setTimeout(() => {
|
||||||
/* taking only 20 results so that sorting is a bit cheaper, we display
|
userSearch(query).then(resolve).catch(reject)
|
||||||
* only 5 anyway. could be inaccurate, but we ideally we should query
|
}, 300)
|
||||||
* backend anyway
|
cancelUserSearch = () => {
|
||||||
*/
|
clearTimeout(timeout)
|
||||||
).slice(0, 20).sort((a, b) => {
|
resolve([])
|
||||||
let aScore = 0
|
}
|
||||||
let bScore = 0
|
})
|
||||||
|
}
|
||||||
// Matches on screen name (i.e. user@instance) makes a priority
|
|
||||||
aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
return async input => {
|
||||||
bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
const noPrefix = input.toLowerCase().substr(1)
|
||||||
|
if (previousQuery === noPrefix) return suggestions
|
||||||
// Matches on name takes second priority
|
|
||||||
aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
suggestions = []
|
||||||
bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
previousQuery = noPrefix
|
||||||
|
// Fetch more and wait, don't fetch if there's the 2nd @ because
|
||||||
const diff = (bScore - aScore) * 10
|
// the backend user search can't deal with it.
|
||||||
|
// Reference semantics make it so that we get the updated data after
|
||||||
// Then sort alphabetically
|
// the await.
|
||||||
const nameAlphabetically = a.name > b.name ? 1 : -1
|
if (!noPrefix.includes('@')) {
|
||||||
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
|
await debounceUserSearch(noPrefix)
|
||||||
|
}
|
||||||
return diff + nameAlphabetically + screenNameAlphabetically
|
|
||||||
/* eslint-disable camelcase */
|
const newSuggestions = state.users.users.filter(
|
||||||
}).map(({ screen_name, name, profile_image_url_original }) => ({
|
user =>
|
||||||
displayText: screen_name,
|
user.screen_name.toLowerCase().startsWith(noPrefix) ||
|
||||||
detailText: name,
|
user.name.toLowerCase().startsWith(noPrefix)
|
||||||
imageUrl: profile_image_url_original,
|
).slice(0, 20).sort((a, b) => {
|
||||||
replacement: '@' + screen_name + ' '
|
let aScore = 0
|
||||||
}))
|
let bScore = 0
|
||||||
|
|
||||||
// BE search users to get more comprehensive results
|
// Matches on screen name (i.e. user@instance) makes a priority
|
||||||
if (data.updateUsersList) {
|
aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
||||||
debounceUserSearch(data, noPrefix)
|
bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
||||||
|
|
||||||
|
// Matches on name takes second priority
|
||||||
|
aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
||||||
|
bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
||||||
|
|
||||||
|
const diff = (bScore - aScore) * 10
|
||||||
|
|
||||||
|
// Then sort alphabetically
|
||||||
|
const nameAlphabetically = a.name > b.name ? 1 : -1
|
||||||
|
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
|
||||||
|
|
||||||
|
return diff + nameAlphabetically + screenNameAlphabetically
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
}).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({
|
||||||
|
displayText: screen_name_ui,
|
||||||
|
detailText: name,
|
||||||
|
imageUrl: profile_image_url_original,
|
||||||
|
replacement: '@' + screen_name + ' '
|
||||||
|
}))
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
suggestions = newSuggestions || []
|
||||||
|
return suggestions
|
||||||
}
|
}
|
||||||
return newUsers
|
|
||||||
/* eslint-enable camelcase */
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,17 @@
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
import Checkbox from '../checkbox/checkbox.vue'
|
import Checkbox from '../checkbox/checkbox.vue'
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faBoxOpen,
|
||||||
|
faStickyNote,
|
||||||
|
faSmileBeam
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faBoxOpen,
|
||||||
|
faStickyNote,
|
||||||
|
faSmileBeam
|
||||||
|
)
|
||||||
|
|
||||||
// At widest, approximately 20 emoji are visible in a row,
|
// At widest, approximately 20 emoji are visible in a row,
|
||||||
// loading 3 rows, could be overkill for narrow picker
|
// loading 3 rows, could be overkill for narrow picker
|
||||||
|
@ -8,7 +21,20 @@ const LOAD_EMOJI_BY = 60
|
||||||
const LOAD_EMOJI_MARGIN = 64
|
const LOAD_EMOJI_MARGIN = 64
|
||||||
|
|
||||||
const filterByKeyword = (list, keyword = '') => {
|
const filterByKeyword = (list, keyword = '') => {
|
||||||
return list.filter(x => x.displayText.includes(keyword))
|
if (keyword === '') return list
|
||||||
|
|
||||||
|
const keywordLowercase = keyword.toLowerCase()
|
||||||
|
let orderedEmojiList = []
|
||||||
|
for (const emoji of list) {
|
||||||
|
const indexOfKeyword = emoji.displayText.toLowerCase().indexOf(keywordLowercase)
|
||||||
|
if (indexOfKeyword > -1) {
|
||||||
|
if (!Array.isArray(orderedEmojiList[indexOfKeyword])) {
|
||||||
|
orderedEmojiList[indexOfKeyword] = []
|
||||||
|
}
|
||||||
|
orderedEmojiList[indexOfKeyword].push(emoji)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return orderedEmojiList.flat()
|
||||||
}
|
}
|
||||||
|
|
||||||
const EmojiPicker = {
|
const EmojiPicker = {
|
||||||
|
@ -32,7 +58,7 @@ const EmojiPicker = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
StickerPicker: () => import('../sticker_picker/sticker_picker.vue'),
|
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
|
||||||
Checkbox
|
Checkbox
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -54,7 +80,7 @@ const EmojiPicker = {
|
||||||
},
|
},
|
||||||
highlight (key) {
|
highlight (key) {
|
||||||
const ref = this.$refs['group-' + key]
|
const ref = this.$refs['group-' + key]
|
||||||
const top = ref[0].offsetTop
|
const top = ref.offsetTop
|
||||||
this.setShowStickers(false)
|
this.setShowStickers(false)
|
||||||
this.activeGroup = key
|
this.activeGroup = key
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
@ -71,7 +97,7 @@ const EmojiPicker = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
triggerLoadMore (target) {
|
triggerLoadMore (target) {
|
||||||
const ref = this.$refs['group-end-custom'][0]
|
const ref = this.$refs['group-end-custom']
|
||||||
if (!ref) return
|
if (!ref) return
|
||||||
const bottom = ref.offsetTop + ref.offsetHeight
|
const bottom = ref.offsetTop + ref.offsetHeight
|
||||||
|
|
||||||
|
@ -94,7 +120,7 @@ const EmojiPicker = {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.emojisView.forEach(group => {
|
this.emojisView.forEach(group => {
|
||||||
const ref = this.$refs['group-' + group.id]
|
const ref = this.$refs['group-' + group.id]
|
||||||
if (ref[0].offsetTop <= top) {
|
if (ref.offsetTop <= top) {
|
||||||
this.activeGroup = group.id
|
this.activeGroup = group.id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -164,13 +190,13 @@ const EmojiPicker = {
|
||||||
{
|
{
|
||||||
id: 'custom',
|
id: 'custom',
|
||||||
text: this.$t('emoji.custom'),
|
text: this.$t('emoji.custom'),
|
||||||
icon: 'icon-smile',
|
icon: 'smile-beam',
|
||||||
emojis: customEmojis
|
emojis: customEmojis
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'standard',
|
id: 'standard',
|
||||||
text: this.$t('emoji.unicode'),
|
text: this.$t('emoji.unicode'),
|
||||||
icon: 'icon-picture',
|
icon: 'box-open',
|
||||||
emojis: filterByKeyword(standardEmojis, this.keyword)
|
emojis: filterByKeyword(standardEmojis, this.keyword)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -82,7 +82,7 @@
|
||||||
&.active {
|
&.active {
|
||||||
border-bottom: 4px solid;
|
border-bottom: 4px solid;
|
||||||
|
|
||||||
i {
|
svg {
|
||||||
color: $fallback--lightText;
|
color: $fallback--lightText;
|
||||||
color: var(--lightText, $fallback--lightText);
|
color: var(--lightText, $fallback--lightText);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,10 @@
|
||||||
:title="group.text"
|
:title="group.text"
|
||||||
@click.prevent="highlight(group.id)"
|
@click.prevent="highlight(group.id)"
|
||||||
>
|
>
|
||||||
<i :class="group.icon" />
|
<FAIcon
|
||||||
|
:icon="group.icon"
|
||||||
|
fixed-width
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
|
@ -26,7 +29,10 @@
|
||||||
:title="$t('emoji.stickers')"
|
:title="$t('emoji.stickers')"
|
||||||
@click.prevent="toggleStickers"
|
@click.prevent="toggleStickers"
|
||||||
>
|
>
|
||||||
<i class="icon-star" />
|
<FAIcon
|
||||||
|
icon="sticky-note"
|
||||||
|
fixed-width
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
:users="accountsForEmoji[reaction.name]"
|
:users="accountsForEmoji[reaction.name]"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="emoji-reaction btn btn-default"
|
class="emoji-reaction btn button-default"
|
||||||
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
|
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
|
||||||
@click="emojiOnClick(reaction.name, $event)"
|
@click="emojiOnClick(reaction.name, $event)"
|
||||||
@mouseenter="fetchEmojiReactionsByIfMissing()"
|
@mouseenter="fetchEmojiReactionsByIfMissing()"
|
||||||
|
|
|
@ -1,102 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="import-export-container">
|
|
||||||
<slot name="before" />
|
|
||||||
<button
|
|
||||||
class="btn"
|
|
||||||
@click="exportData"
|
|
||||||
>
|
|
||||||
{{ exportLabel }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn"
|
|
||||||
@click="importData"
|
|
||||||
>
|
|
||||||
{{ importLabel }}
|
|
||||||
</button>
|
|
||||||
<slot name="afterButtons" />
|
|
||||||
<p
|
|
||||||
v-if="importFailed"
|
|
||||||
class="alert error"
|
|
||||||
>
|
|
||||||
{{ importFailedText }}
|
|
||||||
</p>
|
|
||||||
<slot name="afterError" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: [
|
|
||||||
'exportObject',
|
|
||||||
'importLabel',
|
|
||||||
'exportLabel',
|
|
||||||
'importFailedText',
|
|
||||||
'validator',
|
|
||||||
'onImport',
|
|
||||||
'onImportFailure'
|
|
||||||
],
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
importFailed: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
exportData () {
|
|
||||||
const stringified = JSON.stringify(this.exportObject, null, 2) // Pretty-print and indent with 2 spaces
|
|
||||||
|
|
||||||
// Create an invisible link with a data url and simulate a click
|
|
||||||
const e = document.createElement('a')
|
|
||||||
e.setAttribute('download', 'pleroma_theme.json')
|
|
||||||
e.setAttribute('href', 'data:application/json;base64,' + window.btoa(stringified))
|
|
||||||
e.style.display = 'none'
|
|
||||||
|
|
||||||
document.body.appendChild(e)
|
|
||||||
e.click()
|
|
||||||
document.body.removeChild(e)
|
|
||||||
},
|
|
||||||
importData () {
|
|
||||||
this.importFailed = false
|
|
||||||
const filePicker = document.createElement('input')
|
|
||||||
filePicker.setAttribute('type', 'file')
|
|
||||||
filePicker.setAttribute('accept', '.json')
|
|
||||||
|
|
||||||
filePicker.addEventListener('change', event => {
|
|
||||||
if (event.target.files[0]) {
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = ({ target }) => {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(target.result)
|
|
||||||
const valid = this.validator(parsed)
|
|
||||||
if (valid) {
|
|
||||||
this.onImport(parsed)
|
|
||||||
} else {
|
|
||||||
this.importFailed = true
|
|
||||||
// this.onImportFailure(valid)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// This will happen both if there is a JSON syntax error or the theme is missing components
|
|
||||||
this.importFailed = true
|
|
||||||
// this.onImportFailure(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reader.readAsText(event.target.files[0])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
document.body.appendChild(filePicker)
|
|
||||||
filePicker.click()
|
|
||||||
document.body.removeChild(filePicker)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.import-export-container {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: baseline;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,3 +1,10 @@
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faCircleNotch
|
||||||
|
)
|
||||||
|
|
||||||
const Exporter = {
|
const Exporter = {
|
||||||
props: {
|
props: {
|
||||||
getContent: {
|
getContent: {
|
||||||
|
@ -8,18 +15,8 @@ const Exporter = {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'export.csv'
|
default: 'export.csv'
|
||||||
},
|
},
|
||||||
exportButtonLabel: {
|
exportButtonLabel: { type: String },
|
||||||
type: String,
|
processingMessage: { type: String }
|
||||||
default () {
|
|
||||||
return this.$t('exporter.export')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
processingMessage: {
|
|
||||||
type: String,
|
|
||||||
default () {
|
|
||||||
return this.$t('exporter.processing')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,15 +1,20 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="exporter">
|
<div class="exporter">
|
||||||
<div v-if="processing">
|
<div v-if="processing">
|
||||||
<i class="icon-spin4 animate-spin exporter-processing" />
|
<FAIcon
|
||||||
<span>{{ processingMessage }}</span>
|
icon="circle-notch"
|
||||||
|
size="lg"
|
||||||
|
spin
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span>{{ processingMessage || $t('exporter.processing') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-else
|
v-else
|
||||||
class="btn btn-default"
|
class="btn button-default"
|
||||||
@click="process"
|
@click="process"
|
||||||
>
|
>
|
||||||
{{ exportButtonLabel }}
|
{{ exportButtonLabel || $t('exporter.export') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -19,7 +24,6 @@
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.exporter {
|
.exporter {
|
||||||
&-processing {
|
&-processing {
|
||||||
font-size: 1.5em;
|
|
||||||
margin: 0.25em;
|
margin: 0.25em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,28 @@
|
||||||
import Popover from '../popover/popover.vue'
|
import Popover from '../popover/popover.vue'
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faEllipsisH,
|
||||||
|
faBookmark,
|
||||||
|
faEyeSlash,
|
||||||
|
faThumbtack,
|
||||||
|
faShareAlt,
|
||||||
|
faExternalLinkAlt
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import {
|
||||||
|
faBookmark as faBookmarkReg,
|
||||||
|
faFlag
|
||||||
|
} from '@fortawesome/free-regular-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faEllipsisH,
|
||||||
|
faBookmark,
|
||||||
|
faBookmarkReg,
|
||||||
|
faEyeSlash,
|
||||||
|
faThumbtack,
|
||||||
|
faShareAlt,
|
||||||
|
faExternalLinkAlt,
|
||||||
|
faFlag
|
||||||
|
)
|
||||||
|
|
||||||
const ExtraButtons = {
|
const ExtraButtons = {
|
||||||
props: [ 'status' ],
|
props: [ 'status' ],
|
||||||
|
@ -44,6 +68,9 @@ const ExtraButtons = {
|
||||||
this.$store.dispatch('unbookmark', { id: this.status.id })
|
this.$store.dispatch('unbookmark', { id: this.status.id })
|
||||||
.then(() => this.$emit('onSuccess'))
|
.then(() => this.$emit('onSuccess'))
|
||||||
.catch(err => this.$emit('onError', err.error.error))
|
.catch(err => this.$emit('onError', err.error.error))
|
||||||
|
},
|
||||||
|
reportStatus () {
|
||||||
|
this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
@ -1,82 +1,131 @@
|
||||||
<template>
|
<template>
|
||||||
<Popover
|
<Popover
|
||||||
|
class="ExtraButtons"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
placement="top"
|
placement="top"
|
||||||
class="extra-button-popover"
|
:offset="{ y: 5 }"
|
||||||
:bound-to="{ x: 'container' }"
|
:bound-to="{ x: 'container' }"
|
||||||
|
remove-padding
|
||||||
>
|
>
|
||||||
<div
|
<template v-slot:content="{close}">
|
||||||
slot="content"
|
|
||||||
slot-scope="{close}"
|
|
||||||
>
|
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<button
|
<button
|
||||||
v-if="canMute && !status.thread_muted"
|
v-if="canMute && !status.thread_muted"
|
||||||
class="dropdown-item dropdown-item-icon"
|
class="button-default dropdown-item dropdown-item-icon"
|
||||||
@click.prevent="muteConversation"
|
@click.prevent="muteConversation"
|
||||||
>
|
>
|
||||||
<i class="icon-eye-off" /><span>{{ $t("status.mute_conversation") }}</span>
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
icon="eye-slash"
|
||||||
|
/><span>{{ $t("status.mute_conversation") }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canMute && status.thread_muted"
|
v-if="canMute && status.thread_muted"
|
||||||
class="dropdown-item dropdown-item-icon"
|
class="button-default dropdown-item dropdown-item-icon"
|
||||||
@click.prevent="unmuteConversation"
|
@click.prevent="unmuteConversation"
|
||||||
>
|
>
|
||||||
<i class="icon-eye-off" /><span>{{ $t("status.unmute_conversation") }}</span>
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
icon="eye-slash"
|
||||||
|
/><span>{{ $t("status.unmute_conversation") }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="!status.pinned && canPin"
|
v-if="!status.pinned && canPin"
|
||||||
class="dropdown-item dropdown-item-icon"
|
class="button-default dropdown-item dropdown-item-icon"
|
||||||
@click.prevent="pinStatus"
|
@click.prevent="pinStatus"
|
||||||
@click="close"
|
@click="close"
|
||||||
>
|
>
|
||||||
<i class="icon-pin" /><span>{{ $t("status.pin") }}</span>
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
icon="thumbtack"
|
||||||
|
/><span>{{ $t("status.pin") }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="status.pinned && canPin"
|
v-if="status.pinned && canPin"
|
||||||
class="dropdown-item dropdown-item-icon"
|
class="button-default dropdown-item dropdown-item-icon"
|
||||||
@click.prevent="unpinStatus"
|
@click.prevent="unpinStatus"
|
||||||
@click="close"
|
@click="close"
|
||||||
>
|
>
|
||||||
<i class="icon-pin" /><span>{{ $t("status.unpin") }}</span>
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
icon="thumbtack"
|
||||||
|
/><span>{{ $t("status.unpin") }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="!status.bookmarked"
|
v-if="!status.bookmarked"
|
||||||
class="dropdown-item dropdown-item-icon"
|
class="button-default dropdown-item dropdown-item-icon"
|
||||||
@click.prevent="bookmarkStatus"
|
@click.prevent="bookmarkStatus"
|
||||||
@click="close"
|
@click="close"
|
||||||
>
|
>
|
||||||
<i class="icon-bookmark-empty" /><span>{{ $t("status.bookmark") }}</span>
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
:icon="['far', 'bookmark']"
|
||||||
|
/><span>{{ $t("status.bookmark") }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="status.bookmarked"
|
v-if="status.bookmarked"
|
||||||
class="dropdown-item dropdown-item-icon"
|
class="button-default dropdown-item dropdown-item-icon"
|
||||||
@click.prevent="unbookmarkStatus"
|
@click.prevent="unbookmarkStatus"
|
||||||
@click="close"
|
@click="close"
|
||||||
>
|
>
|
||||||
<i class="icon-bookmark" /><span>{{ $t("status.unbookmark") }}</span>
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
icon="bookmark"
|
||||||
|
/><span>{{ $t("status.unbookmark") }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canDelete"
|
v-if="canDelete"
|
||||||
class="dropdown-item dropdown-item-icon"
|
class="button-default dropdown-item dropdown-item-icon"
|
||||||
@click.prevent="deleteStatus"
|
@click.prevent="deleteStatus"
|
||||||
@click="close"
|
@click="close"
|
||||||
>
|
>
|
||||||
<i class="icon-cancel" /><span>{{ $t("status.delete") }}</span>
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
icon="times"
|
||||||
|
/><span>{{ $t("status.delete") }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="dropdown-item dropdown-item-icon"
|
class="button-default dropdown-item dropdown-item-icon"
|
||||||
@click.prevent="copyLink"
|
@click.prevent="copyLink"
|
||||||
@click="close"
|
@click="close"
|
||||||
>
|
>
|
||||||
<i class="icon-share" /><span>{{ $t("status.copy_link") }}</span>
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
icon="share-alt"
|
||||||
|
/><span>{{ $t("status.copy_link") }}</span>
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
v-if="!status.is_local"
|
||||||
|
class="button-default dropdown-item dropdown-item-icon"
|
||||||
|
title="Source"
|
||||||
|
:href="status.external_url"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
icon="external-link-alt"
|
||||||
|
/><span>{{ $t("status.external_source") }}</span>
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item dropdown-item-icon"
|
||||||
|
@click.prevent="reportStatus"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
:icon="['far', 'flag']"
|
||||||
|
/><span>{{ $t("user_card.report") }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<i
|
<template v-slot:trigger>
|
||||||
slot="trigger"
|
<button class="button-unstyled popover-trigger">
|
||||||
class="icon-ellipsis button-icon"
|
<FAIcon
|
||||||
/>
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
icon="ellipsis-h"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
</Popover>
|
</Popover>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -85,13 +134,21 @@
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.icon-ellipsis {
|
.ExtraButtons {
|
||||||
cursor: pointer;
|
/* override of popover internal stuff */
|
||||||
|
.popover-trigger-button {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover,
|
.popover-trigger {
|
||||||
.extra-button-popover.open & {
|
position: static;
|
||||||
color: $fallback--text;
|
padding: 10px;
|
||||||
color: var(--text, $fallback--text);
|
margin: -10px;
|
||||||
|
|
||||||
|
&:hover .svg-inline--fa {
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--text, $fallback--text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,4 +1,14 @@
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import { faStar } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import {
|
||||||
|
faStar as faStarRegular
|
||||||
|
} from '@fortawesome/free-regular-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faStar,
|
||||||
|
faStarRegular
|
||||||
|
)
|
||||||
|
|
||||||
const FavoriteButton = {
|
const FavoriteButton = {
|
||||||
props: ['status', 'loggedIn'],
|
props: ['status', 'loggedIn'],
|
||||||
|
@ -21,13 +31,6 @@ const FavoriteButton = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
classes () {
|
|
||||||
return {
|
|
||||||
'icon-star-empty': !this.status.favorited,
|
|
||||||
'icon-star': this.status.favorited,
|
|
||||||
'animate-spin': this.animated
|
|
||||||
}
|
|
||||||
},
|
|
||||||
...mapGetters(['mergedConfig'])
|
...mapGetters(['mergedConfig'])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,31 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="loggedIn">
|
<div class="FavoriteButton">
|
||||||
<i
|
<button
|
||||||
:class="classes"
|
v-if="loggedIn"
|
||||||
class="button-icon favorite-button fav-active"
|
class="button-unstyled interactive"
|
||||||
|
:class="status.favorited && '-favorited'"
|
||||||
:title="$t('tool_tip.favorite')"
|
:title="$t('tool_tip.favorite')"
|
||||||
@click.prevent="favorite()"
|
@click.prevent="favorite()"
|
||||||
/>
|
>
|
||||||
<span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span>
|
<FAIcon
|
||||||
</div>
|
class="fa-scale-110 fa-old-padding"
|
||||||
<div v-else>
|
:icon="[status.favorited ? 'fas' : 'far', 'star']"
|
||||||
<i
|
:spin="animated"
|
||||||
:class="classes"
|
/>
|
||||||
class="button-icon favorite-button"
|
</button>
|
||||||
:title="$t('tool_tip.favorite')"
|
<span v-else>
|
||||||
/>
|
<FAIcon
|
||||||
<span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span>
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
:title="$t('tool_tip.favorite')"
|
||||||
|
:icon="['far', 'star']"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="!mergedConfig.hidePostStats && status.fave_num > 0"
|
||||||
|
class="action-counter"
|
||||||
|
>
|
||||||
|
{{ status.fave_num }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -23,18 +34,29 @@
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.fav-active {
|
.FavoriteButton {
|
||||||
cursor: pointer;
|
display: flex;
|
||||||
animation-duration: 0.6s;
|
|
||||||
|
|
||||||
&:hover {
|
> :first-child {
|
||||||
color: $fallback--cOrange;
|
padding: 10px;
|
||||||
color: var(--cOrange, $fallback--cOrange);
|
margin: -10px -8px -10px -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-counter {
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interactive {
|
||||||
|
.svg-inline--fa {
|
||||||
|
animation-duration: 0.6s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .svg-inline--fa,
|
||||||
|
&.-favorited .svg-inline--fa {
|
||||||
|
color: $fallback--cOrange;
|
||||||
|
color: var(--cOrange, $fallback--cOrange);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.favorite-button.icon-star {
|
|
||||||
color: $fallback--cOrange;
|
|
||||||
color: var(--cOrange, $fallback--cOrange);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
|
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
|
||||||
|
|
||||||
const FeaturesPanel = {
|
const FeaturesPanel = {
|
||||||
computed: {
|
computed: {
|
||||||
chat: function () { return this.$store.state.instance.chatAvailable },
|
shout: function () { return this.$store.state.instance.shoutAvailable },
|
||||||
pleromaChatMessages: function () { return this.$store.state.instance.pleromaChatMessagesAvailable },
|
pleromaChatMessages: function () { return this.$store.state.instance.pleromaChatMessagesAvailable },
|
||||||
gopher: function () { return this.$store.state.instance.gopherAvailable },
|
gopher: function () { return this.$store.state.instance.gopherAvailable },
|
||||||
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled },
|
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled },
|
||||||
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable },
|
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable },
|
||||||
minimalScopesMode: function () { return this.$store.state.instance.minimalScopesMode },
|
minimalScopesMode: function () { return this.$store.state.instance.minimalScopesMode },
|
||||||
textlimit: function () { return this.$store.state.instance.textlimit }
|
textlimit: function () { return this.$store.state.instance.textlimit },
|
||||||
|
uploadlimit: function () { return fileSizeFormatService.fileSizeFormat(this.$store.state.instance.uploadlimit) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body features-panel">
|
<div class="panel-body features-panel">
|
||||||
<ul>
|
<ul>
|
||||||
<li v-if="chat">
|
<li v-if="shout">
|
||||||
{{ $t('features_panel.chat') }}
|
{{ $t('features_panel.shout') }}
|
||||||
</li>
|
</li>
|
||||||
<li v-if="pleromaChatMessages">
|
<li v-if="pleromaChatMessages">
|
||||||
{{ $t('features_panel.pleroma_chat_messages') }}
|
{{ $t('features_panel.pleroma_chat_messages') }}
|
||||||
|
@ -25,6 +25,7 @@
|
||||||
</li>
|
</li>
|
||||||
<li>{{ $t('features_panel.scope_options') }}</li>
|
<li>{{ $t('features_panel.scope_options') }}</li>
|
||||||
<li>{{ $t('features_panel.text_limit') }} = {{ textlimit }}</li>
|
<li>{{ $t('features_panel.text_limit') }} = {{ textlimit }}</li>
|
||||||
|
<li>{{ $t('features_panel.upload_limit') }} = {{ uploadlimit.num }} {{ $t('upload.file_size_units.' + uploadlimit.unit) }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
53
src/components/flash/flash.js
Normal file
53
src/components/flash/flash.js
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import RuffleService from '../../services/ruffle_service/ruffle_service.js'
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faStop,
|
||||||
|
faExclamationTriangle
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faStop,
|
||||||
|
faExclamationTriangle
|
||||||
|
)
|
||||||
|
|
||||||
|
const Flash = {
|
||||||
|
props: [ 'src' ],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
player: false, // can be true, "hidden", false. hidden = element exists
|
||||||
|
loaded: false,
|
||||||
|
ruffleInstance: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openPlayer () {
|
||||||
|
if (this.player) return // prevent double-loading, or re-loading on failure
|
||||||
|
this.player = 'hidden'
|
||||||
|
RuffleService.getRuffle().then((ruffle) => {
|
||||||
|
const player = ruffle.newest().createPlayer()
|
||||||
|
player.config = {
|
||||||
|
letterbox: 'on'
|
||||||
|
}
|
||||||
|
const container = this.$refs.container
|
||||||
|
container.appendChild(player)
|
||||||
|
player.style.width = '100%'
|
||||||
|
player.style.height = '100%'
|
||||||
|
player.load(this.src).then(() => {
|
||||||
|
this.player = true
|
||||||
|
}).catch((e) => {
|
||||||
|
console.error('Error loading ruffle', e)
|
||||||
|
this.player = 'error'
|
||||||
|
})
|
||||||
|
this.ruffleInstance = player
|
||||||
|
this.$emit('playerOpened')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
closePlayer () {
|
||||||
|
this.ruffleInstance && this.ruffleInstance.remove()
|
||||||
|
this.player = false
|
||||||
|
this.$emit('playerClosed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Flash
|
84
src/components/flash/flash.vue
Normal file
84
src/components/flash/flash.vue
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
<template>
|
||||||
|
<div class="Flash">
|
||||||
|
<div
|
||||||
|
v-if="player === true || player === 'hidden'"
|
||||||
|
ref="container"
|
||||||
|
class="player"
|
||||||
|
:class="{ hidden: player === 'hidden' }"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="player !== true"
|
||||||
|
class="button-unstyled placeholder"
|
||||||
|
@click="openPlayer"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="player === 'hidden'"
|
||||||
|
class="label"
|
||||||
|
>
|
||||||
|
{{ $t('general.loading') }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="player === 'error'"
|
||||||
|
class="label"
|
||||||
|
>
|
||||||
|
{{ $t('general.flash_fail') }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="label"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{{ $t('general.flash_content') }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<FAIcon icon="exclamation-triangle" />
|
||||||
|
{{ $t('general.flash_security') }}
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./flash.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
.Flash {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.player {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--link);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hider {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
text-align: center;
|
||||||
|
flex: 1 1 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: normal;
|
||||||
|
word-wrap: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
visibility: 'hidden';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,6 +1,6 @@
|
||||||
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
|
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
|
||||||
export default {
|
export default {
|
||||||
props: ['relationship', 'labelFollowing', 'buttonClass'],
|
props: ['relationship', 'user', 'labelFollowing', 'buttonClass'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
inProgress: false
|
inProgress: false
|
||||||
|
@ -14,7 +14,7 @@ export default {
|
||||||
if (this.inProgress || this.relationship.following) {
|
if (this.inProgress || this.relationship.following) {
|
||||||
return this.$t('user_card.follow_unfollow')
|
return this.$t('user_card.follow_unfollow')
|
||||||
} else if (this.relationship.requested) {
|
} else if (this.relationship.requested) {
|
||||||
return this.$t('user_card.follow_again')
|
return this.$t('user_card.follow_cancel')
|
||||||
} else {
|
} else {
|
||||||
return this.$t('user_card.follow')
|
return this.$t('user_card.follow')
|
||||||
}
|
}
|
||||||
|
@ -29,11 +29,14 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
return this.$t('user_card.follow')
|
return this.$t('user_card.follow')
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
disabled () {
|
||||||
|
return this.inProgress || this.user.deactivated
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onClick () {
|
onClick () {
|
||||||
this.relationship.following ? this.unfollow() : this.follow()
|
this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow()
|
||||||
},
|
},
|
||||||
follow () {
|
follow () {
|
||||||
this.inProgress = true
|
this.inProgress = true
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
class="btn btn-default follow-button"
|
class="btn button-default follow-button"
|
||||||
:class="{ toggled: isPressed }"
|
:class="{ toggled: isPressed }"
|
||||||
:disabled="inProgress"
|
:disabled="disabled"
|
||||||
:title="title"
|
:title="title"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
>
|
>
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
:relationship="relationship"
|
:relationship="relationship"
|
||||||
:label-following="$t('user_card.follow_unfollow')"
|
:label-following="$t('user_card.follow_unfollow')"
|
||||||
class="follow-card-follow-button"
|
class="follow-card-follow-button"
|
||||||
|
:user="user"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,13 +2,13 @@
|
||||||
<basic-user-card :user="user">
|
<basic-user-card :user="user">
|
||||||
<div class="follow-request-card-content-container">
|
<div class="follow-request-card-content-container">
|
||||||
<button
|
<button
|
||||||
class="btn btn-default"
|
class="btn button-default"
|
||||||
@click="approveUser"
|
@click="approveUser"
|
||||||
>
|
>
|
||||||
{{ $t('user_card.approve') }}
|
{{ $t('user_card.approve') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-default"
|
class="btn button-default"
|
||||||
@click="denyUser"
|
@click="denyUser"
|
||||||
>
|
>
|
||||||
{{ $t('user_card.deny') }}
|
{{ $t('user_card.deny') }}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="settings panel panel-default">
|
<div class="settings panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
{{ $t('nav.friend_requests') }}
|
<div class="title">
|
||||||
|
{{ $t('nav.friend_requests') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<FollowRequestCard
|
<FollowRequestCard
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
import { set } from 'vue'
|
import { set } from 'lodash'
|
||||||
|
import Select from '../select/select.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
Select
|
||||||
|
},
|
||||||
props: [
|
props: [
|
||||||
'name', 'label', 'value', 'fallback', 'options', 'no-inherit'
|
'name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit'
|
||||||
],
|
],
|
||||||
|
emits: ['update:modelValue'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
lValue: this.value,
|
lValue: this.modelValue,
|
||||||
availableOptions: [
|
availableOptions: [
|
||||||
this.noInherit ? '' : 'inherit',
|
this.noInherit ? '' : 'inherit',
|
||||||
'custom',
|
'custom',
|
||||||
|
@ -18,7 +23,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeUpdate () {
|
beforeUpdate () {
|
||||||
this.lValue = this.value
|
this.lValue = this.modelValue
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
present () {
|
present () {
|
||||||
|
@ -33,7 +38,7 @@ export default {
|
||||||
},
|
},
|
||||||
set (v) {
|
set (v) {
|
||||||
set(this.lValue, 'family', v)
|
set(this.lValue, 'family', v)
|
||||||
this.$emit('input', this.lValue)
|
this.$emit('update:modelValue', this.lValue)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isCustom () {
|
isCustom () {
|
||||||
|
|
|
@ -15,34 +15,28 @@
|
||||||
class="opt exlcude-disabled"
|
class="opt exlcude-disabled"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="present"
|
:checked="present"
|
||||||
@input="$emit('input', typeof value === 'undefined' ? fallback : undefined)"
|
@change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
v-if="typeof fallback !== 'undefined'"
|
v-if="typeof fallback !== 'undefined'"
|
||||||
class="opt-l"
|
class="opt-l"
|
||||||
:for="name + '-o'"
|
:for="name + '-o'"
|
||||||
/>
|
/>
|
||||||
<label
|
{{ ' ' }}
|
||||||
:for="name + '-font-switcher'"
|
<Select
|
||||||
class="select"
|
:id="name + '-font-switcher'"
|
||||||
|
v-model="preset"
|
||||||
:disabled="!present"
|
:disabled="!present"
|
||||||
|
class="font-switcher"
|
||||||
>
|
>
|
||||||
<select
|
<option
|
||||||
:id="name + '-font-switcher'"
|
v-for="option in availableOptions"
|
||||||
v-model="preset"
|
:key="option"
|
||||||
:disabled="!present"
|
:value="option"
|
||||||
class="font-switcher"
|
|
||||||
>
|
>
|
||||||
<option
|
{{ option === 'custom' ? $t('settings.style.fonts.custom') : option }}
|
||||||
v-for="option in availableOptions"
|
</option>
|
||||||
:key="option"
|
</Select>
|
||||||
:value="option"
|
|
||||||
>
|
|
||||||
{{ option === 'custom' ? $t('settings.style.fonts.custom') : option }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<i class="icon-down-open" />
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
v-if="isCustom"
|
v-if="isCustom"
|
||||||
:id="name"
|
:id="name"
|
||||||
|
@ -62,7 +56,8 @@
|
||||||
min-width: 10em;
|
min-width: 10em;
|
||||||
}
|
}
|
||||||
&.custom {
|
&.custom {
|
||||||
.select {
|
/* TODO Should make proper joiners... */
|
||||||
|
.font-switcher {
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,26 @@
|
||||||
import Attachment from '../attachment/attachment.vue'
|
import Attachment from '../attachment/attachment.vue'
|
||||||
import { chunk, last, dropRight, sumBy } from 'lodash'
|
import { sumBy, set } from 'lodash'
|
||||||
|
|
||||||
const Gallery = {
|
const Gallery = {
|
||||||
props: [
|
props: [
|
||||||
'attachments',
|
'attachments',
|
||||||
|
'limitRows',
|
||||||
|
'descriptions',
|
||||||
|
'limit',
|
||||||
'nsfw',
|
'nsfw',
|
||||||
'setMedia'
|
'setMedia',
|
||||||
|
'size',
|
||||||
|
'editable',
|
||||||
|
'removeAttachment',
|
||||||
|
'shiftUpAttachment',
|
||||||
|
'shiftDnAttachment',
|
||||||
|
'editAttachment',
|
||||||
|
'grid'
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
sizes: {}
|
sizes: {},
|
||||||
|
hidingLong: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: { Attachment },
|
components: { Attachment },
|
||||||
|
@ -18,26 +29,70 @@ const Gallery = {
|
||||||
if (!this.attachments) {
|
if (!this.attachments) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
const rows = chunk(this.attachments, 3)
|
const attachments = this.limit > 0
|
||||||
if (last(rows).length === 1 && rows.length > 1) {
|
? this.attachments.slice(0, this.limit)
|
||||||
// if 1 attachment on last row -> add it to the previous row instead
|
: this.attachments
|
||||||
const lastAttachment = last(rows)[0]
|
if (this.size === 'hide') {
|
||||||
const allButLastRow = dropRight(rows)
|
return attachments.map(item => ({ minimal: true, items: [item] }))
|
||||||
last(allButLastRow).push(lastAttachment)
|
|
||||||
return allButLastRow
|
|
||||||
}
|
}
|
||||||
|
const rows = this.grid
|
||||||
|
? [{ grid: true, items: attachments }]
|
||||||
|
: attachments.reduce((acc, attachment, i) => {
|
||||||
|
if (attachment.mimetype.includes('audio')) {
|
||||||
|
return [...acc, { audio: true, items: [attachment] }, { items: [] }]
|
||||||
|
}
|
||||||
|
if (!(
|
||||||
|
attachment.mimetype.includes('image') ||
|
||||||
|
attachment.mimetype.includes('video') ||
|
||||||
|
attachment.mimetype.includes('flash')
|
||||||
|
)) {
|
||||||
|
return [...acc, { minimal: true, items: [attachment] }, { items: [] }]
|
||||||
|
}
|
||||||
|
const maxPerRow = 3
|
||||||
|
const attachmentsRemaining = this.attachments.length - i + 1
|
||||||
|
const currentRow = acc[acc.length - 1].items
|
||||||
|
currentRow.push(attachment)
|
||||||
|
if (currentRow.length >= maxPerRow && attachmentsRemaining > maxPerRow) {
|
||||||
|
return [...acc, { items: [] }]
|
||||||
|
} else {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
}, [{ items: [] }]).filter(_ => _.items.length > 0)
|
||||||
return rows
|
return rows
|
||||||
},
|
},
|
||||||
useContainFit () {
|
attachmentsDimensionalScore () {
|
||||||
return this.$store.getters.mergedConfig.useContainFit
|
return this.rows.reduce((acc, row) => {
|
||||||
|
let size = 0
|
||||||
|
if (row.minimal) {
|
||||||
|
size += 1 / 8
|
||||||
|
} else if (row.audio) {
|
||||||
|
size += 1 / 4
|
||||||
|
} else {
|
||||||
|
size += 1 / (row.items.length + 0.6)
|
||||||
|
}
|
||||||
|
return acc + size
|
||||||
|
}, 0)
|
||||||
|
},
|
||||||
|
tooManyAttachments () {
|
||||||
|
if (this.editable || this.size === 'small') {
|
||||||
|
return false
|
||||||
|
} else if (this.size === 'hide') {
|
||||||
|
return this.attachments.length > 8
|
||||||
|
} else {
|
||||||
|
return this.attachmentsDimensionalScore > 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onNaturalSizeLoad (id, size) {
|
onNaturalSizeLoad ({ id, width, height }) {
|
||||||
this.$set(this.sizes, id, size)
|
set(this.sizes, id, { width, height })
|
||||||
},
|
},
|
||||||
rowStyle (itemsPerRow) {
|
rowStyle (row) {
|
||||||
return { 'padding-bottom': `${(100 / (itemsPerRow + 0.6))}%` }
|
if (row.audio) {
|
||||||
|
return { 'padding-bottom': '25%' } // fixed reduced height for audio
|
||||||
|
} else if (!row.minimal && !row.grid) {
|
||||||
|
return { 'padding-bottom': `${(100 / (row.items.length + 0.6))}%` }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
itemStyle (id, row) {
|
itemStyle (id, row) {
|
||||||
const total = sumBy(row, item => this.getAspectRatio(item.id))
|
const total = sumBy(row, item => this.getAspectRatio(item.id))
|
||||||
|
@ -46,6 +101,16 @@ const Gallery = {
|
||||||
getAspectRatio (id) {
|
getAspectRatio (id) {
|
||||||
const size = this.sizes[id]
|
const size = this.sizes[id]
|
||||||
return size ? size.width / size.height : 1
|
return size ? size.width / size.height : 1
|
||||||
|
},
|
||||||
|
toggleHidingLong (event) {
|
||||||
|
this.hidingLong = event
|
||||||
|
},
|
||||||
|
openGallery () {
|
||||||
|
this.$store.dispatch('setMedia', this.attachments)
|
||||||
|
this.$store.dispatch('setCurrentMedia', this.attachments[0])
|
||||||
|
},
|
||||||
|
onMedia () {
|
||||||
|
this.$store.dispatch('setMedia', this.attachments)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,83 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="galleryContainer"
|
ref="galleryContainer"
|
||||||
style="width: 100%;"
|
class="Gallery"
|
||||||
|
:class="{ '-long': tooManyAttachments && hidingLong }"
|
||||||
>
|
>
|
||||||
|
<div class="gallery-rows">
|
||||||
|
<div
|
||||||
|
v-for="(row, rowIndex) in rows"
|
||||||
|
:key="rowIndex"
|
||||||
|
class="gallery-row"
|
||||||
|
:style="rowStyle(row)"
|
||||||
|
:class="{ '-audio': row.audio, '-minimal': row.minimal, '-grid': grid }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="gallery-row-inner"
|
||||||
|
:class="{ '-grid': grid }"
|
||||||
|
>
|
||||||
|
<Attachment
|
||||||
|
v-for="(attachment, attachmentIndex) in row.items"
|
||||||
|
:key="attachment.id"
|
||||||
|
class="gallery-item"
|
||||||
|
:nsfw="nsfw"
|
||||||
|
:attachment="attachment"
|
||||||
|
:size="size"
|
||||||
|
:editable="editable"
|
||||||
|
:remove="removeAttachment"
|
||||||
|
:shift-up="!(attachmentIndex === 0 && rowIndex === 0) && shiftUpAttachment"
|
||||||
|
:shift-dn="!(attachmentIndex === row.items.length - 1 && rowIndex === rows.length - 1) && shiftDnAttachment"
|
||||||
|
:edit="editAttachment"
|
||||||
|
:description="descriptions && descriptions[attachment.id]"
|
||||||
|
:hide-description="size === 'small' || tooManyAttachments && hidingLong"
|
||||||
|
:style="itemStyle(attachment.id, row.items)"
|
||||||
|
@setMedia="onMedia"
|
||||||
|
@naturalSizeLoad="onNaturalSizeLoad"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="(row, index) in rows"
|
v-if="tooManyAttachments"
|
||||||
:key="index"
|
class="many-attachments"
|
||||||
class="gallery-row"
|
|
||||||
:style="rowStyle(row.length)"
|
|
||||||
:class="{ 'contain-fit': useContainFit, 'cover-fit': !useContainFit }"
|
|
||||||
>
|
>
|
||||||
<div class="gallery-row-inner">
|
<div class="many-attachments-text">
|
||||||
<attachment
|
{{ $t("status.many_attachments", { number: attachments.length }) }}
|
||||||
v-for="attachment in row"
|
</div>
|
||||||
:key="attachment.id"
|
<div class="many-attachments-buttons">
|
||||||
:set-media="setMedia"
|
<span
|
||||||
:nsfw="nsfw"
|
v-if="!hidingLong"
|
||||||
:attachment="attachment"
|
class="many-attachments-button"
|
||||||
:allow-play="false"
|
>
|
||||||
:natural-size-load="onNaturalSizeLoad.bind(null, attachment.id)"
|
<button
|
||||||
:style="itemStyle(attachment.id, row)"
|
class="button-unstyled -link"
|
||||||
/>
|
@click="toggleHidingLong(true)"
|
||||||
|
>
|
||||||
|
{{ $t("status.collapse_attachments") }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="hidingLong"
|
||||||
|
class="many-attachments-button"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button-unstyled -link"
|
||||||
|
@click="toggleHidingLong(false)"
|
||||||
|
>
|
||||||
|
{{ $t("status.show_all_attachments") }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="hidingLong"
|
||||||
|
class="many-attachments-button"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button-unstyled -link"
|
||||||
|
@click="openGallery"
|
||||||
|
>
|
||||||
|
{{ $t("status.open_gallery") }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,12 +88,66 @@
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.gallery-row {
|
.Gallery {
|
||||||
position: relative;
|
.gallery-rows {
|
||||||
height: 0;
|
display: flex;
|
||||||
width: 100%;
|
flex-direction: column;
|
||||||
flex-grow: 1;
|
}
|
||||||
margin-top: 0.5em;
|
|
||||||
|
.gallery-row {
|
||||||
|
position: relative;
|
||||||
|
height: 0;
|
||||||
|
width: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-long {
|
||||||
|
.gallery-rows {
|
||||||
|
max-height: 25em;
|
||||||
|
overflow: hidden;
|
||||||
|
mask:
|
||||||
|
linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
|
||||||
|
linear-gradient(to top, white, white);
|
||||||
|
|
||||||
|
/* Autoprefixed seem to ignore this one, and also syntax is different */
|
||||||
|
-webkit-mask-composite: xor;
|
||||||
|
mask-composite: exclude;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.many-attachments-text {
|
||||||
|
text-align: center;
|
||||||
|
line-height: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.many-attachments-buttons {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.many-attachments-button {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 2;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-row {
|
||||||
|
&.-grid,
|
||||||
|
&.-minimal {
|
||||||
|
height: auto;
|
||||||
|
.gallery-row-inner {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.gallery-row-inner {
|
.gallery-row-inner {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -48,9 +159,24 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
align-content: stretch;
|
align-content: stretch;
|
||||||
|
|
||||||
|
&.-grid {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-column-gap: 0.5em;
|
||||||
|
grid-row-gap: 0.5em;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(15em, 1fr));
|
||||||
|
|
||||||
|
.gallery-item {
|
||||||
|
margin: 0;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.gallery-row-inner .attachment {
|
.gallery-item {
|
||||||
margin: 0 0.5em 0 0;
|
margin: 0 0.5em 0 0;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -61,32 +187,5 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-attachment {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-container {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.contain-fit {
|
|
||||||
img,
|
|
||||||
video,
|
|
||||||
canvas {
|
|
||||||
object-fit: contain;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.cover-fit {
|
|
||||||
img,
|
|
||||||
video,
|
|
||||||
canvas {
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,3 +1,11 @@
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faTimes
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faTimes
|
||||||
|
)
|
||||||
|
|
||||||
const GlobalNoticeList = {
|
const GlobalNoticeList = {
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
@ -9,10 +9,15 @@
|
||||||
<div class="notice-message">
|
<div class="notice-message">
|
||||||
{{ $t(notice.messageKey, notice.messageArgs) }}
|
{{ $t(notice.messageKey, notice.messageArgs) }}
|
||||||
</div>
|
</div>
|
||||||
<i
|
<button
|
||||||
class="button-icon icon-cancel"
|
class="button-unstyled close-notice"
|
||||||
@click="closeNotice(notice)"
|
@click="closeNotice(notice)"
|
||||||
/>
|
>
|
||||||
|
<FAIcon
|
||||||
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
icon="times"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -53,7 +58,7 @@
|
||||||
.global-error {
|
.global-error {
|
||||||
background-color: var(--alertPopupError, $fallback--cRed);
|
background-color: var(--alertPopupError, $fallback--cRed);
|
||||||
color: var(--alertPopupErrorText, $fallback--text);
|
color: var(--alertPopupErrorText, $fallback--text);
|
||||||
i {
|
.svg-inline--fa {
|
||||||
color: var(--alertPopupErrorText, $fallback--text);
|
color: var(--alertPopupErrorText, $fallback--text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,17 +66,32 @@
|
||||||
.global-warning {
|
.global-warning {
|
||||||
background-color: var(--alertPopupWarning, $fallback--cOrange);
|
background-color: var(--alertPopupWarning, $fallback--cOrange);
|
||||||
color: var(--alertPopupWarningText, $fallback--text);
|
color: var(--alertPopupWarningText, $fallback--text);
|
||||||
i {
|
.svg-inline--fa {
|
||||||
color: var(--alertPopupWarningText, $fallback--text);
|
color: var(--alertPopupWarningText, $fallback--text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.global-success {
|
||||||
|
background-color: var(--alertPopupSuccess, $fallback--cGreen);
|
||||||
|
color: var(--alertPopupSuccessText, $fallback--text);
|
||||||
|
.svg-inline--fa {
|
||||||
|
color: var(--alertPopupSuccessText, $fallback--text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.global-info {
|
.global-info {
|
||||||
background-color: var(--alertPopupNeutral, $fallback--fg);
|
background-color: var(--alertPopupNeutral, $fallback--fg);
|
||||||
color: var(--alertPopupNeutralText, $fallback--text);
|
color: var(--alertPopupNeutralText, $fallback--text);
|
||||||
i {
|
.svg-inline--fa {
|
||||||
color: var(--alertPopupNeutralText, $fallback--text);
|
color: var(--alertPopupNeutralText, $fallback--text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.close-notice {
|
||||||
|
padding-right: 0.2em;
|
||||||
|
.svg-inline--fa:hover {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
36
src/components/hashtag_link/hashtag_link.js
Normal file
36
src/components/hashtag_link/hashtag_link.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
|
||||||
|
|
||||||
|
const HashtagLink = {
|
||||||
|
name: 'HashtagLink',
|
||||||
|
props: {
|
||||||
|
url: {
|
||||||
|
required: true,
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
required: true,
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
tag: {
|
||||||
|
required: false,
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onClick () {
|
||||||
|
const tag = this.tag || extractTagFromUrl(this.url)
|
||||||
|
if (tag) {
|
||||||
|
const link = this.generateTagLink(tag)
|
||||||
|
this.$router.push(link)
|
||||||
|
} else {
|
||||||
|
window.open(this.url, '_blank')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
generateTagLink (tag) {
|
||||||
|
return `/tag/${tag}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HashtagLink
|
6
src/components/hashtag_link/hashtag_link.scss
Normal file
6
src/components/hashtag_link/hashtag_link.scss
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
.HashtagLink {
|
||||||
|
position: relative;
|
||||||
|
white-space: normal;
|
||||||
|
display: inline-block;
|
||||||
|
color: var(--link);
|
||||||
|
}
|
19
src/components/hashtag_link/hashtag_link.vue
Normal file
19
src/components/hashtag_link/hashtag_link.vue
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
class="HashtagLink"
|
||||||
|
>
|
||||||
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
|
<a
|
||||||
|
:href="url"
|
||||||
|
class="original"
|
||||||
|
target="_blank"
|
||||||
|
@click.prevent="onClick"
|
||||||
|
v-html="content"
|
||||||
|
/>
|
||||||
|
<!-- eslint-enable vue/no-v-html -->
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./hashtag_link.js"/>
|
||||||
|
|
||||||
|
<style lang="scss" src="./hashtag_link.scss"/>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue