Compare commits
762 commits
shigusegub
...
shigusegub
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a47252d33 | ||
|
|
0264090953 | ||
|
|
8dbe6ad1ad | ||
|
|
b21685cb0c | ||
|
|
ce99dca909 | ||
|
|
ff621d9d80 | ||
|
|
d03ca88eba | ||
|
|
54298927da | ||
|
|
5e574dca77 | ||
|
|
e202f18720 | ||
|
|
2ab1d86d92 | ||
|
|
a597d947bd | ||
|
|
77cc2e5201 | ||
|
|
739c4420f3 | ||
|
|
6cdb921e56 | ||
|
|
94570af748 | ||
|
|
4e235562aa | ||
|
|
1be0debc63 | ||
|
|
784ae2edb6 | ||
|
|
a461068e40 | ||
|
|
bb0656c505 | ||
|
|
694a1f0103 | ||
|
|
f57c24cf6d | ||
|
|
0e2a94bf34 | ||
|
|
a1cde6ce0f | ||
|
|
2520935676 | ||
|
|
00cd46d05a | ||
|
|
4e21031fde | ||
|
|
ea69bf5fd9 | ||
|
|
b8557bb678 | ||
|
|
b08d5dd8df | ||
|
|
a2eb4fd202 | ||
|
|
8ee71cdfff | ||
|
|
1aedd8f16d | ||
|
|
45771001e6 | ||
|
|
bf8a4bdbe4 | ||
|
|
e775098da8 | ||
|
|
5785bb850d | ||
|
|
6592955a28 | ||
|
|
c905219e8c | ||
|
|
e6649c7c25 | ||
|
|
b2ec9cb890 | ||
|
|
63b4586c0f | ||
|
|
9a6f3bfbe4 | ||
|
|
b57f681f85 | ||
|
|
c50181f7d0 | ||
|
|
dbededdea4 | ||
|
|
b1f3e097f4 | ||
|
|
c2be818504 | ||
|
|
12b4b00e49 | ||
|
|
f29710d0af | ||
|
|
c4c4f3bae7 | ||
|
|
f6fd5ed8c4 | ||
|
|
55fdb8687a | ||
|
|
d881b92f86 | ||
|
|
195e353b3a | ||
|
|
650785bb5b | ||
|
|
392b595dc9 | ||
|
|
1fb7b6b48b | ||
|
|
18b10ea042 | ||
|
|
6a8abf1b32 | ||
|
|
10b4259de6 | ||
|
|
7528a72b2e | ||
|
|
7b517a85bf | ||
|
|
47cf88426d | ||
|
|
cac39ed49d | ||
|
|
e6a6a06465 | ||
|
|
f31a9a42da | ||
|
|
d0fb8bcf85 | ||
|
|
5c42661ca7 | ||
|
|
6659308ace | ||
|
|
3121eaad01 | ||
|
|
de7844cbaa | ||
|
|
702e6b231c | ||
|
|
c7501f21a9 | ||
|
|
fb10ee54ed | ||
|
|
e77e8431c6 | ||
|
|
6e55622963 | ||
|
|
e610361217 | ||
|
|
71c7bb86f9 | ||
|
|
1785546120 | ||
|
|
b564fc2d66 | ||
|
|
b29b573b9d | ||
|
|
2290816e8b | ||
|
|
756a931ea5 | ||
|
|
314dc3888c | ||
|
|
9ddbd625f6 | ||
|
|
3b850ae223 | ||
|
|
3a136f6c11 | ||
|
|
fda58058a3 | ||
|
|
826e726391 | ||
|
|
13d9a281c9 | ||
|
|
ca4604602d | ||
|
|
da49b0d009 | ||
|
|
4e44af04b7 | ||
|
|
a7b2a9a823 | ||
|
|
914df47a35 | ||
|
|
7f42d33f67 | ||
|
|
d358b8370f | ||
|
|
3a9ff0797b | ||
|
|
884b47845d | ||
|
|
d4f8f60475 | ||
|
|
53a3c8bcd6 | ||
|
|
aa8c9fec3b | ||
|
|
a1f4dd2142 | ||
|
|
6eceac4723 | ||
|
|
11045fd81c | ||
|
|
773c56f69d | ||
|
|
f0aa25059b | ||
|
|
35a1dec8a5 | ||
|
|
ab7fb1ba51 | ||
|
|
e582b23ad7 | ||
|
|
b09efcb5f3 | ||
|
|
7505c94986 | ||
|
|
bb24349c68 | ||
|
|
bcb13f3a49 | ||
|
|
98b2e1e829 | ||
|
|
97a50cba7e | ||
|
|
183bd73af0 | ||
|
|
65cb78eef8 | ||
|
|
9bb9192f0f | ||
|
|
adfe233250 | ||
|
|
8edee4bd9d | ||
|
|
18a3bbbd49 | ||
|
|
e84fb2acad | ||
|
|
bc97016ea3 | ||
|
|
fbfd62d98a | ||
|
|
eb7cff467e | ||
|
|
44ae68b025 | ||
|
|
e3234226a3 | ||
|
|
8a43624f2e | ||
|
|
98e2a60948 | ||
|
|
a605e4c337 | ||
|
|
bc70905d96 | ||
|
|
42cd240347 | ||
|
|
9967f453e7 | ||
|
|
f40fdf1240 |
||
|
|
fa53bd2ebe | ||
|
|
a9172acd35 | ||
|
|
edaff65ce6 | ||
|
|
a9a86bdbdc | ||
|
|
4faf6d8b0d |
||
|
|
1727a7e266 | ||
|
|
e804a6aa06 | ||
|
|
f1b6e8417d | ||
|
|
0ed54185b4 | ||
|
|
85ac9a21fe | ||
|
|
68eab91db8 |
||
|
|
0c3d81d0a0 |
||
|
|
5655875e16 | ||
|
|
d5222ff212 | ||
|
|
e127895568 | ||
|
|
463cc2ef5c | ||
|
|
449c244d11 | ||
|
|
24be3da17e | ||
|
|
dbdec60110 | ||
|
|
38444f3165 |
||
|
|
033b227edb | ||
|
|
a0d4e107a9 | ||
|
|
9d83b8ecc2 | ||
|
|
ee73a0746b | ||
|
|
f3308f463f | ||
|
|
bac19670f7 | ||
|
|
406df8c27a | ||
|
|
5b1536eb3e | ||
|
|
42e5deb133 | ||
|
|
ca0da60bcd | ||
|
|
7738ce87e6 | ||
|
|
f77e1225b5 | ||
|
|
f70972d011 | ||
|
|
30aae4a346 | ||
|
|
d2c4bc3b2b | ||
|
|
70177deb49 | ||
|
|
dae4e5b2ac | ||
|
|
209637ee8a | ||
|
|
9d657395ff | ||
|
|
6d6c627c3e | ||
|
|
20162e5358 | ||
|
|
2e53707324 | ||
|
|
208600bd16 | ||
|
|
6124d9c04c | ||
|
|
f62e0c5718 | ||
|
|
3fbf21a757 | ||
|
|
a4845bf275 | ||
|
|
f7c67130f5 | ||
|
|
63bffe73db | ||
|
|
2ce11e56d4 | ||
|
|
dbdf81d8b3 | ||
|
|
496099bb00 | ||
|
|
6967151275 | ||
|
|
ea020cefdb | ||
|
|
6f3f75d9b4 | ||
|
|
2c4cb8a67a | ||
|
|
0e84cffa41 | ||
|
|
d2a870ac96 | ||
|
|
76d3ec1b39 | ||
|
|
85976a61b8 | ||
|
|
682ad334c1 | ||
|
|
2c673f439f | ||
|
|
71172ec93a | ||
|
|
d3d4b899d2 | ||
|
|
b08df84282 | ||
|
|
dbc9bd9c46 | ||
|
|
c9dede920e | ||
|
|
8fabbe9525 | ||
|
|
0688cdab86 | ||
|
|
5dc6ca6d58 | ||
|
|
dc96e5ac53 | ||
|
|
fc66830138 | ||
|
|
cd66dabf94 | ||
|
|
69656e0181 | ||
|
|
29e71c8a26 | ||
|
|
2881b31ff2 | ||
|
|
0f5c9ace4f | ||
|
|
9ae56174c9 | ||
|
|
db2f48a1f9 | ||
|
|
23fae89413 |
||
|
|
3b9187694c |
||
|
|
8d06ccb04e |
||
|
|
89ae2e6f46 |
||
|
|
4cd6c6e9f0 | ||
|
|
fd2986ea85 | ||
|
|
b2011429c6 | ||
|
|
8dce2d75aa | ||
|
|
fbf01fce09 | ||
|
|
c87abc9eb7 | ||
|
|
9060977790 | ||
|
|
6750397ef7 | ||
|
|
58f06dea94 | ||
|
|
8985094644 | ||
|
|
13cd50886b | ||
|
|
c5aee1c012 | ||
|
|
92d0916f40 | ||
|
|
1fcc7c6ae8 | ||
|
|
c2f5840a02 | ||
|
|
1e93e0a9c3 | ||
|
|
848d48e404 | ||
|
|
20e781c71d | ||
|
|
e3a441310f | ||
|
|
3cdcfe19f4 | ||
|
|
74d83a996c | ||
|
|
3e71833cab | ||
|
|
d028a86013 | ||
|
|
18d8ea6b63 | ||
|
|
e554eeeef6 | ||
|
|
5e21134d9b | ||
|
|
573a980512 | ||
|
|
912aa228d1 | ||
|
|
095abb2914 | ||
|
|
617613dfb4 | ||
|
|
4156b1597a | ||
|
|
24ce2dc0a5 | ||
|
|
7e67b5274d | ||
|
|
cf77127335 |
||
|
|
02f952047d | ||
|
|
b9f1c33b50 | ||
|
|
98110c0596 | ||
|
|
127396d2af | ||
|
|
320899c9a2 | ||
|
|
69edded5b0 | ||
|
|
1e08616b1f | ||
|
|
dcb7ed1b8c | ||
|
|
aa25cd04b1 | ||
|
|
42930252b1 | ||
|
|
6f5eb6c442 | ||
|
|
e13e84e26d | ||
|
|
3d78a34daa | ||
|
|
ca10ffca8d | ||
|
|
4b87fa083e | ||
|
|
851c100a24 | ||
|
|
9e980dcd87 | ||
|
|
6fc8cc32ca | ||
|
|
b8bfd6b1a9 | ||
|
|
0dc8305e95 | ||
|
|
949aa90faa | ||
|
|
03b6178d17 | ||
|
|
d2f528bb15 | ||
|
|
0492a8d6a0 | ||
|
|
33c7876a8a | ||
|
|
7f54706821 | ||
|
|
99d2efac6c | ||
|
|
7ffec2c324 | ||
|
|
ae600da287 | ||
|
|
2197c030de | ||
|
|
85c2947714 | ||
|
|
f30537f25e | ||
|
|
d15642d93d | ||
|
|
b2964612ae | ||
|
|
e956d55219 | ||
|
|
04797f8bd2 | ||
|
|
7f54d11834 | ||
|
|
1c53ac84cc | ||
|
|
1654234e32 | ||
|
|
b05a501236 | ||
|
|
9262e803ec | ||
|
|
8372348148 | ||
|
|
b9a77cc61d | ||
|
|
1fe112c7f3 | ||
|
|
59b7da77c9 | ||
|
|
b33ded3a45 | ||
|
|
46cbef8253 | ||
|
|
308c5cd222 | ||
|
|
bbee089d64 | ||
|
|
fd03a5ade3 | ||
|
|
d7b1aaa616 | ||
|
|
971161a441 | ||
|
|
29fe616c1c | ||
|
|
50105a8f92 | ||
|
|
fb77d084d4 | ||
|
|
6ae05ab956 | ||
|
|
963c55cf71 | ||
|
|
fd086febfa | ||
|
|
202d1618c2 | ||
|
|
657de70153 | ||
|
|
c2732e3f40 | ||
|
|
c9b47a0ca9 | ||
|
|
3aae2f33d2 | ||
|
|
aa426b3d14 | ||
|
|
c71a36de30 | ||
|
|
7d88140bb4 | ||
|
|
db7e4a3434 | ||
|
|
4edf6b03ce | ||
|
|
25a9033b6b | ||
|
|
d26dca92e2 | ||
|
|
f97658b6d6 | ||
|
|
b9e6b9b1f0 | ||
|
|
d2d8b0167c | ||
|
|
3355f1d797 | ||
|
|
06608adec4 | ||
|
|
68aadcdd08 | ||
|
|
0222d493f8 | ||
|
|
bb802ed756 | ||
|
|
db3bfb6fc3 | ||
|
|
3ec21cb442 | ||
|
|
5b143b2aea | ||
|
|
96fc30a1b7 | ||
|
|
edbf5f3276 | ||
|
|
189b092d2c | ||
|
|
bf75c7af85 | ||
|
|
952800410e | ||
|
|
c085acd2dd | ||
|
|
e4a33bf6d7 | ||
|
|
625954721e | ||
|
|
76dd3540e4 | ||
|
|
494f6b471e | ||
|
|
1642d62b82 | ||
|
|
5a6f4fb466 | ||
|
|
13ace1d24e | ||
|
|
57dfbd8a53 | ||
|
|
576774540f | ||
|
|
caa0213ac6 | ||
|
|
a7fa7558b3 | ||
|
|
bdb992a8e5 | ||
|
|
bc47bef80d | ||
|
|
c4f83808b0 | ||
|
|
5aed9a20b8 | ||
|
|
e961b6e14c | ||
|
|
df8df5a0bf | ||
|
|
7d0f03fdba | ||
|
|
0bc5442eb9 | ||
|
|
4329502422 | ||
|
|
2fe5efc69d | ||
|
|
ace8295c03 | ||
|
|
d5c75915e6 | ||
|
|
d7453c09b2 | ||
|
|
9c043533f2 | ||
|
|
b0bce1bf18 | ||
|
|
48ba3892c3 | ||
|
|
ac751320f4 | ||
|
|
3fca18e248 | ||
|
|
56a6a25112 | ||
|
|
cdbf3f42b8 | ||
|
|
0a9a3648d6 | ||
|
|
5042db43ab | ||
|
|
cae19d8a9a | ||
|
|
311e9d255d | ||
|
|
3a321ca756 | ||
|
|
42a5da93ea | ||
|
|
b7a97b8603 | ||
|
|
c93f55e8f7 | ||
|
|
4a98ec9611 | ||
|
|
388ecd9a5e | ||
|
|
0252d39c75 | ||
|
|
235e6bd233 | ||
|
|
672bedaf6d | ||
|
|
55b5d2c5d7 | ||
|
|
b73c9ae4e8 | ||
|
|
5718483558 | ||
|
|
114d49b6d6 | ||
|
|
bc92f535de | ||
|
|
db535ae057 | ||
|
|
eae09226b5 | ||
|
|
16f456eaea | ||
|
|
489fb17070 | ||
|
|
0d04b1c8ce | ||
|
|
b38343705c | ||
|
|
3bc8800c35 | ||
|
|
e4c5a88913 | ||
|
|
ad13f2417f | ||
|
|
951dc87c09 | ||
|
|
7b02072133 | ||
|
|
2af0c83389 | ||
|
|
04a21e4698 | ||
|
|
41b6e80171 | ||
|
|
b519c0f3aa | ||
|
|
355a5955b3 | ||
|
|
dba63e6825 | ||
|
|
f24f164995 | ||
|
|
e6bda9638b | ||
|
|
e01753830d | ||
|
|
1e73c7e8cb | ||
|
|
94864276c1 | ||
|
|
2a3999bf42 | ||
|
|
171bb3b7ca | ||
|
|
23c5a6fab7 | ||
|
|
f7fc678c23 | ||
|
|
38a99b2f39 | ||
|
|
9fae1d086b | ||
|
|
6f9cd347df | ||
|
|
402cbf6593 | ||
|
|
452a522fa4 | ||
|
|
1fd6584374 | ||
|
|
ce04595e36 | ||
|
|
2d0bd043cb | ||
|
|
db73631459 | ||
|
|
b3bf4fca75 | ||
|
|
3716797e04 | ||
|
|
7c57be22e4 | ||
|
|
50ede338e7 | ||
|
|
b0f725671a | ||
|
|
fba7d15a2c | ||
|
|
9572b9704c | ||
|
|
63535b1494 | ||
|
|
8b8af2889b | ||
|
|
3f4ad34377 | ||
|
|
7d1799e929 | ||
|
|
8e6800fd1e | ||
|
|
e6f025bf6e | ||
|
|
5958c32acf | ||
|
|
a96f533777 | ||
|
|
a3a35e76a8 | ||
|
|
1683d98699 | ||
|
|
3601012cc2 | ||
|
|
848f2c7ddb | ||
|
|
043c02ff40 | ||
|
|
661ab34889 | ||
|
|
debd3a3e7b | ||
|
|
2f8ea4f3b3 | ||
|
|
30f9b84f08 | ||
|
|
dd910ff8a8 |
||
|
|
90f7dee343 | ||
|
|
f616c583f2 | ||
|
|
f0cf1da920 | ||
|
|
004bdd6b79 | ||
|
|
3286725510 | ||
|
|
ce048667f5 | ||
|
|
bc23d46615 | ||
|
|
aa7911665b | ||
|
|
7ba0b1d622 | ||
|
|
be0bf5e119 | ||
|
|
e9f1b29e1c | ||
|
|
b4cd8d8fab | ||
|
|
d5723bbf34 | ||
|
|
d67967dc78 | ||
|
|
59b65891af | ||
|
|
ff9127973e | ||
|
|
8cd50f6df3 | ||
|
|
b3b71fcf18 | ||
|
|
ce0921e208 | ||
|
|
e40e56a988 | ||
|
|
41bcc4c93e | ||
|
|
d2266303d1 | ||
|
|
6c965789d8 | ||
|
|
3196eb79de | ||
|
|
f3e897588b | ||
|
|
abe1b9c565 | ||
|
|
e0b76eeda6 | ||
|
|
811eb3d361 | ||
|
|
320a53835a | ||
|
|
00ba6b7c5d | ||
|
|
7a919e7c76 | ||
|
|
326d4976a3 | ||
|
|
df05a7a8a7 | ||
|
|
fb828b07f9 | ||
|
|
ed10af15e2 | ||
|
|
6341a48d05 | ||
|
|
53afb86da1 | ||
|
|
aa7d5c0efd | ||
|
|
8353db33ad | ||
|
|
40eb565f2a | ||
|
|
0ecbae9675 | ||
|
|
6e1c65a574 | ||
|
|
b7c75dcba2 | ||
|
|
205e03ea15 | ||
|
|
f9848daf48 | ||
|
|
30917ff017 | ||
|
|
f9570b132f | ||
|
|
473133aa02 | ||
|
|
a62ffbfbab | ||
|
|
2551947ee6 | ||
|
|
e3bfbcf0d2 | ||
|
|
683b2d7fea | ||
|
|
0fe823aeec | ||
|
|
b46c8358ae | ||
|
|
65b40f8f72 | ||
|
|
6b4057c6ab | ||
|
|
2a69abf374 | ||
|
|
0560110868 | ||
|
|
3df779f02f | ||
|
|
0b78c64928 | ||
|
|
9b8bccd27d | ||
|
|
2854b012c9 | ||
|
|
82b560ccf2 | ||
|
|
288ad9dd06 | ||
|
|
fdaaf333c5 | ||
|
|
2601c09660 | ||
|
|
38fc7f784e | ||
|
|
4c4144f1eb | ||
|
|
088be118c3 | ||
|
|
58ce121010 | ||
|
|
7e04269b7e | ||
|
|
3bacacd214 | ||
|
|
b3f5edb913 | ||
|
|
785c8d518a | ||
|
|
a5af76603f | ||
|
|
589737dced | ||
|
|
542db846a6 | ||
|
|
798178a86f | ||
|
|
8f320faa2d | ||
|
|
528cc9f388 | ||
|
|
17c04d4d83 | ||
|
|
0b9547b289 | ||
|
|
0ad18d3e5e | ||
|
|
0424992a35 | ||
|
|
4ce9a011da | ||
|
|
91ab21b37b | ||
|
|
b3ba505e51 | ||
|
|
7678b7f597 | ||
|
|
d148f36474 | ||
|
|
f252585b7f | ||
|
|
4aa27c5be0 | ||
|
|
bebb3fcfa6 | ||
|
|
8780e0191e | ||
|
|
26a2232e18 | ||
|
|
0d6453baec | ||
|
|
80b0117ba2 | ||
|
|
7b3ef62b96 | ||
|
|
15239cf92d | ||
|
|
3ba9c01e15 | ||
|
|
7ff72e5ae2 | ||
|
|
48651518df | ||
|
|
202f5d87b3 | ||
|
|
7b2f3b648a | ||
|
|
16ada7ec6c | ||
|
|
028556f8ab | ||
|
|
9ed2fd3c4b | ||
|
|
c47bfe53ff | ||
|
|
35fea49bd0 | ||
|
|
866b416dbe | ||
|
|
1c0aa025a3 | ||
|
|
bfd1809a34 | ||
|
|
96d886d213 | ||
|
|
1a0f4c8c34 | ||
|
|
9b58b8c290 | ||
|
|
9f481052d0 | ||
|
|
69726895b1 | ||
|
|
e560b1d6c7 | ||
|
|
c65c0afce3 | ||
|
|
c7ad009496 | ||
|
|
f0250ffc52 | ||
|
|
267692f56a | ||
|
|
49a98a6fe2 | ||
|
|
4c17458e58 | ||
|
|
6399de114c | ||
|
|
bfb9f6c5e2 | ||
|
|
e076403fcb | ||
|
|
97adff267f | ||
|
|
5bb0424397 | ||
|
|
00c3719618 | ||
|
|
aa5688a43b | ||
|
|
f35633c855 | ||
|
|
6e479d246b | ||
|
|
0b9b7a51a6 | ||
|
|
a6c844e522 | ||
|
|
2869ab787d | ||
|
|
22a14d8985 | ||
|
|
ce5638d223 | ||
|
|
2a2ffff992 | ||
|
|
0a4c867519 | ||
|
|
4886b34c2f | ||
|
|
f6cdf06ba1 | ||
|
|
adbeb9d0dd | ||
|
|
8958f70a48 | ||
|
|
f7fca3a1ba | ||
|
|
918fbecf3e | ||
|
|
25d54cbeea | ||
|
|
be3170b50b | ||
|
|
6f29f9e735 | ||
|
|
1e4544b2ca | ||
|
|
286bb6aa97 | ||
|
|
9d16aa5ace | ||
|
|
72cd6b40ea | ||
|
|
e3a7d3dca8 | ||
|
|
4ba1e55f05 | ||
|
|
24c0e0e497 | ||
|
|
bf4dbaf077 | ||
|
|
a6a2dbd2f7 | ||
|
|
c3872147c0 | ||
|
|
e04d7d2c97 | ||
|
|
0ccff2019f | ||
|
|
04c180e0d9 | ||
|
|
ec635426c3 | ||
|
|
a0159f1e18 | ||
|
|
d62393bf6b | ||
|
|
1d3b271e7c | ||
|
|
6e5da62233 | ||
|
|
b80035cbb0 | ||
|
|
047dda5525 | ||
|
|
e82de98892 | ||
|
|
cf4aa692e3 | ||
|
|
8f16da2f6f | ||
|
|
1f53c8bb07 | ||
|
|
2830b55d41 | ||
|
|
f86cc5d8b5 | ||
|
|
8b8975adb2 | ||
|
|
2aabaeb5c6 | ||
|
|
67f606a3b0 | ||
|
|
9440d35266 | ||
|
|
6341747ec9 | ||
|
|
9ec2ff409d | ||
|
|
4ff257be57 | ||
|
|
5e77a0a23d | ||
|
|
370a7f8291 | ||
|
|
0f51550802 | ||
|
|
700e096dd4 | ||
|
|
d879b6f6eb | ||
|
|
8d141cbeab | ||
|
|
81bb4f133b | ||
|
|
a4de299c58 | ||
|
|
11e6349e8d | ||
|
|
409816748e | ||
|
|
cfb4868c55 | ||
|
|
f2783260f1 | ||
|
|
7fd46f2d6c | ||
|
|
f27099a4b3 | ||
|
|
18110d6821 | ||
|
|
202bfbad02 | ||
|
|
7b643b5486 | ||
|
|
1c4f19e56f | ||
|
|
d66dd17f7b | ||
|
|
76e67a08c6 | ||
|
|
ebe727b378 | ||
|
|
99886ac28c | ||
|
|
d4c0ccf659 | ||
|
|
6e44a3afa9 | ||
|
|
ee01395071 | ||
|
|
fa67b2330f | ||
|
|
7e9fd4d1dd | ||
|
|
bc2964c327 | ||
|
|
3311c676ad | ||
|
|
184f1cdc24 | ||
|
|
8268d0d349 | ||
|
|
a4e6a72ca2 | ||
|
|
49422705a8 | ||
|
|
d34ce95d17 | ||
|
|
fc90a92ecf | ||
|
|
ea3e054c21 | ||
|
|
d89f564b5e | ||
|
|
a4802030be | ||
|
|
86f8f46b95 | ||
|
|
41267a5d43 | ||
|
|
c8fa72c791 | ||
|
|
800ab90cf9 | ||
|
|
59de80639f | ||
|
|
f79c61c4e7 | ||
|
|
b305748a92 | ||
|
|
7d985bd475 | ||
|
|
60363e66fb | ||
|
|
50314fe253 | ||
|
|
20beb30fc3 | ||
|
|
2df895ab02 | ||
|
|
51eb61180d | ||
|
|
38b9b04385 | ||
|
|
28422adc8c | ||
|
|
3c2c572661 | ||
|
|
39602f36bb | ||
|
|
68c88677a0 | ||
|
|
d82ab81e4e | ||
|
|
66b83558bb | ||
|
|
9a85c3f8fb | ||
|
|
34c29e4fdc | ||
|
|
6e7ad5d554 | ||
|
|
07a6d58660 | ||
|
|
ddcd9007dc | ||
|
|
f8a5918fc8 | ||
|
|
bb044315cd | ||
|
|
426fb90522 | ||
|
|
2008fcce22 | ||
|
|
ba5efe4a67 | ||
|
|
83ad4078bb | ||
|
|
41d794d3ab | ||
|
|
eb68857293 | ||
|
|
add9535b1a | ||
|
|
1fdb676eca | ||
|
|
9e77071e77 | ||
|
|
f5d167950d | ||
|
|
4ffd8499a7 | ||
|
|
b05ebeaed5 | ||
|
|
518144f308 | ||
|
|
731e9eed6e | ||
|
|
0b1e7dbcea | ||
|
|
ef0a4f4023 | ||
|
|
2c1bea0e0b | ||
|
|
d299e864bf | ||
|
|
79868aeeb9 | ||
|
|
38130fce90 | ||
|
|
3294df0325 | ||
|
|
0e56f8f103 | ||
|
|
e747ee896e | ||
|
|
e1cbb1ccd2 | ||
|
|
04ee1a892c | ||
|
|
23975b506e | ||
|
|
c12e1a69cf | ||
|
|
6aae8a8705 | ||
|
|
6ae52e192b | ||
|
|
eb77c5dbb9 | ||
|
|
b014489295 | ||
|
|
35c2e87131 | ||
|
|
9f0b65654e | ||
|
|
2441e6508d | ||
|
|
71d1baffcc | ||
|
|
8436f39eff | ||
|
|
385f921c41 | ||
|
|
74b410da2b | ||
|
|
d9639c543c | ||
|
|
c433aa38fb | ||
|
|
a626b37354 | ||
|
|
2e9f23542c | ||
|
|
1d729cb0c5 | ||
|
|
0f573637a7 | ||
|
|
b2f7309e1e | ||
|
|
c179daaf80 | ||
|
|
558251ce74 | ||
|
|
7bc5dd440b | ||
|
|
a635f025be | ||
|
|
1bc53262d6 | ||
|
|
d6ebc5049e | ||
|
|
3081504c64 | ||
|
|
dc531d4ef3 | ||
|
|
5a6a77bd75 | ||
|
|
bc96d16e11 | ||
|
|
82d67a634e | ||
|
|
3822aaf137 | ||
|
|
0d32a7ddac | ||
|
|
8483268cb3 | ||
|
|
230e61235d | ||
|
|
e4b44f8c7b | ||
|
|
046b959bd7 | ||
|
|
4146c071ce | ||
|
|
537031f0cb | ||
|
|
c9a4aee954 | ||
|
|
b9161ef697 |
609 changed files with 71590 additions and 25219 deletions
12
.dockerignore
Normal file
12
.dockerignore
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
node_modules/
|
||||
dist/
|
||||
logs/
|
||||
.DS_Store
|
||||
.git/
|
||||
config/local.json
|
||||
pleroma-backend/
|
||||
test/e2e/reports/
|
||||
test/e2e-playwright/test-results/
|
||||
test/e2e-playwright/playwright-report/
|
||||
__screenshots__/
|
||||
|
||||
87
.forgejo/issue_template/bug.yaml
Normal file
87
.forgejo/issue_template/bug.yaml
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
name: 'Bug report'
|
||||
about: 'Bug report for Pleroma FE'
|
||||
labels:
|
||||
- Bug
|
||||
body:
|
||||
- type: input
|
||||
id: env-browser
|
||||
attributes:
|
||||
label: Browser and OS
|
||||
description: What browser are you using, including version, and what OS are you running?
|
||||
placeholder: Firefox 140, Arch Linux
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: env-instance
|
||||
attributes:
|
||||
label: Instance URL
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: env-backend
|
||||
attributes:
|
||||
label: Backend version information
|
||||
description: Backend version being used. (See Settings->Show advanced->Developer)
|
||||
placeholder: Pleroma BE 2.10
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: env-frontend
|
||||
attributes:
|
||||
label: Frontend version information
|
||||
description: Frontend version being used. (See Settings->Show advanced->Developer)
|
||||
placeholder: Pleroma FE 2.10
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: env-extensions
|
||||
attributes:
|
||||
label: Browser extensions
|
||||
description: List of browser extensions you are using, like uBlock, rikaichamp etc. If none leave empty.
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: env-modifications
|
||||
attributes:
|
||||
label: Known instance/user customizations
|
||||
description: Whether you are using a Pleroma FE fork, any mods mods or instance level styles among others.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: bug-text
|
||||
attributes:
|
||||
label: Bug description
|
||||
description: A short description of the bug. Images can be helpful.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: bug-reproducer
|
||||
attributes:
|
||||
label: Reproduction steps
|
||||
description: Ordered list of reproduction steps needed to make the bug happen. If you don't have reproduction steps, leave this empty.
|
||||
placeholder: |
|
||||
1. Log in with a fresh browser session
|
||||
2. Open timeline X
|
||||
3. Click on button Y
|
||||
4. Z broke
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: bug-seriousness
|
||||
attributes:
|
||||
label: Bug seriousness
|
||||
value: |
|
||||
* How annoying it is:
|
||||
* How often does it happen:
|
||||
* How many people does it affect:
|
||||
* Is there a workaround for it:
|
||||
- type: checkboxes
|
||||
id: duplicate-issues
|
||||
attributes:
|
||||
label: Duplicate issues
|
||||
hide_label: true
|
||||
description: Before submitting this issue, search for same or similar issues on the [Pleroma FE bug tracker](https://git.pleroma.social/pleroma/pleroma-fe/issues).
|
||||
options:
|
||||
- label: I've searched for same or similar issues before submitting this issue.
|
||||
required: true
|
||||
visible: [form]
|
||||
22
.forgejo/issue_template/suggestion.yaml
Normal file
22
.forgejo/issue_template/suggestion.yaml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
name: 'Feature request / Suggestion / Improvement'
|
||||
about: 'Feature requests, suggestions and improvements for Pleroma FE'
|
||||
labels:
|
||||
- Feature Request / Enhancement
|
||||
body:
|
||||
- type: textarea
|
||||
id: issue-text
|
||||
attributes:
|
||||
label: Proposal
|
||||
placeholder: Make groups happen!
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: duplicate-issues
|
||||
attributes:
|
||||
label: Duplicate issues
|
||||
hide_label: true
|
||||
description: Before submitting this issue, search for same or similar requests on the [Pleroma FE bug tracker](https://git.pleroma.social/pleroma/pleroma-fe/issues).
|
||||
options:
|
||||
- label: I've searched for same or similar requests before submitting this issue.
|
||||
required: true
|
||||
visible: [form]
|
||||
12
.forgejo/pull_request_template.md
Normal file
12
.forgejo/pull_request_template.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
### Checklist
|
||||
- [ ] Adding a changelog: In the `changelog.d` directory, create a file named `<code>.<type>`.
|
||||
|
||||
<!--
|
||||
`<code>` can be anything, but we recommend using a more or less unique identifier to avoid collisions, such as the branch name.
|
||||
|
||||
`<type>` can be `add`, `change`, `remove`, `fix`, `security` or `skip`. `skip` is only used if there is no user-visible change in the MR (for example, only editing comments in the code). Otherwise, choose a type that corresponds to your change.
|
||||
|
||||
In the file, write the changelog entry. For example, if an MR adds group functionality, we can create a file named `group.add` and write `Add group functionality` in it.
|
||||
|
||||
If one changelog entry is not enough, you may add more. But that might mean you can split it into two MRs. Only use more than one changelog entry if you really need to (for example, when one change in the code fix two different bugs, or when refactoring).
|
||||
-->
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -4,8 +4,11 @@ dist/
|
|||
npm-debug.log
|
||||
test/unit/coverage
|
||||
test/e2e/reports
|
||||
test/e2e-playwright/test-results
|
||||
test/e2e-playwright/playwright-report
|
||||
selenium-debug.log
|
||||
.idea/
|
||||
.gitlab-ci-local/
|
||||
config/local.json
|
||||
src/assets/emoji.json
|
||||
logs/
|
||||
|
|
|
|||
146
.gitlab-ci.yml
146
.gitlab-ci.yml
|
|
@ -34,12 +34,23 @@ check-changelog:
|
|||
- apk add git
|
||||
- sh ./tools/check-changelog
|
||||
|
||||
lint:
|
||||
lint-eslint:
|
||||
stage: lint
|
||||
script:
|
||||
- yarn
|
||||
- yarn lint
|
||||
- yarn stylelint
|
||||
- yarn ci-eslint
|
||||
|
||||
lint-biome:
|
||||
stage: lint
|
||||
script:
|
||||
- yarn
|
||||
- yarn ci-biome
|
||||
|
||||
lint-stylelint:
|
||||
stage: lint
|
||||
script:
|
||||
- yarn
|
||||
- yarn ci-stylelint
|
||||
|
||||
test:
|
||||
stage: test
|
||||
|
|
@ -60,6 +71,135 @@ test:
|
|||
- test/**/__screenshots__
|
||||
when: on_failure
|
||||
|
||||
e2e-pleroma:
|
||||
stage: test
|
||||
image: mcr.microsoft.com/playwright:v1.57.0-jammy
|
||||
services:
|
||||
- name: postgres:15-alpine
|
||||
alias: db
|
||||
- name: $PLEROMA_IMAGE
|
||||
alias: pleroma
|
||||
entrypoint: ["/bin/ash", "-c"]
|
||||
command:
|
||||
- |
|
||||
set -eu
|
||||
|
||||
SEED_SENTINEL_PATH=/var/lib/pleroma/.e2e_seeded
|
||||
CONFIG_OVERRIDE_PATH=/var/lib/pleroma/config.exs
|
||||
|
||||
echo '-- Waiting for database...'
|
||||
while ! pg_isready -U ${DB_USER:-pleroma} -d postgres://${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-pleroma} -t 1; do
|
||||
sleep 1s
|
||||
done
|
||||
|
||||
echo '-- Writing E2E config overrides...'
|
||||
cat > $CONFIG_OVERRIDE_PATH <<EOF
|
||||
import Config
|
||||
|
||||
config :pleroma, Pleroma.Captcha,
|
||||
enabled: false
|
||||
|
||||
config :pleroma, :instance,
|
||||
registrations_open: true,
|
||||
account_activation_required: false,
|
||||
approval_required: false
|
||||
EOF
|
||||
|
||||
echo '-- Running migrations...'
|
||||
/opt/pleroma/bin/pleroma_ctl migrate
|
||||
|
||||
echo '-- Starting!'
|
||||
/opt/pleroma/bin/pleroma start &
|
||||
PLEROMA_PID=$!
|
||||
|
||||
cleanup() {
|
||||
if kill -0 $PLEROMA_PID 2>/dev/null; then
|
||||
kill -TERM $PLEROMA_PID
|
||||
wait $PLEROMA_PID || true
|
||||
fi
|
||||
}
|
||||
|
||||
trap cleanup INT TERM
|
||||
|
||||
echo '-- Waiting for API...'
|
||||
api_ok=false
|
||||
for _i in $(seq 1 120); do
|
||||
if wget -qO- http://127.0.0.1:4000/api/v1/instance >/dev/null 2>&1; then
|
||||
api_ok=true
|
||||
break
|
||||
fi
|
||||
sleep 1s
|
||||
done
|
||||
|
||||
if [ $api_ok != true ]; then
|
||||
echo 'Timed out waiting for Pleroma API to become available'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f $SEED_SENTINEL_PATH ]; then
|
||||
if [ -n ${E2E_ADMIN_USERNAME:-} ] && [ -n ${E2E_ADMIN_PASSWORD:-} ] && [ -n ${E2E_ADMIN_EMAIL:-} ]; then
|
||||
echo '-- Seeding admin user' $E2E_ADMIN_USERNAME '...'
|
||||
if ! /opt/pleroma/bin/pleroma_ctl user new $E2E_ADMIN_USERNAME $E2E_ADMIN_EMAIL --admin --password $E2E_ADMIN_PASSWORD -y; then
|
||||
echo '-- User already exists or creation failed, ensuring admin + confirmed...'
|
||||
/opt/pleroma/bin/pleroma_ctl user set $E2E_ADMIN_USERNAME --admin --confirmed
|
||||
fi
|
||||
else
|
||||
echo '-- Skipping admin seeding (missing E2E_ADMIN_* env)'
|
||||
fi
|
||||
|
||||
touch $SEED_SENTINEL_PATH
|
||||
fi
|
||||
|
||||
wait $PLEROMA_PID
|
||||
tags:
|
||||
- amd64
|
||||
- himem
|
||||
variables:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
|
||||
FF_NETWORK_PER_BUILD: "true"
|
||||
PLEROMA_IMAGE: git.pleroma.social:5050/pleroma/pleroma:stable
|
||||
POSTGRES_USER: pleroma
|
||||
POSTGRES_PASSWORD: pleroma
|
||||
POSTGRES_DB: pleroma
|
||||
DB_USER: pleroma
|
||||
DB_PASS: pleroma
|
||||
DB_NAME: pleroma
|
||||
DB_HOST: db
|
||||
DB_PORT: 5432
|
||||
DOMAIN: localhost
|
||||
INSTANCE_NAME: Pleroma E2E
|
||||
E2E_ADMIN_USERNAME: admin
|
||||
E2E_ADMIN_PASSWORD: adminadmin
|
||||
E2E_ADMIN_EMAIL: admin@example.com
|
||||
ADMIN_EMAIL: $E2E_ADMIN_EMAIL
|
||||
NOTIFY_EMAIL: $E2E_ADMIN_EMAIL
|
||||
VITE_PROXY_TARGET: http://pleroma:4000
|
||||
VITE_PROXY_ORIGIN: http://localhost:4000
|
||||
E2E_BASE_URL: http://localhost:8080
|
||||
script:
|
||||
- npm install -g yarn@1.22.22
|
||||
- yarn --frozen-lockfile
|
||||
- |
|
||||
echo "-- Waiting for Pleroma API..."
|
||||
api_ok="false"
|
||||
for _i in $(seq 1 120); do
|
||||
if wget -qO- http://pleroma:4000/api/v1/instance >/dev/null 2>&1; then
|
||||
api_ok="true"
|
||||
break
|
||||
fi
|
||||
sleep 1s
|
||||
done
|
||||
if [ "$api_ok" != "true" ]; then
|
||||
echo "Timed out waiting for Pleroma API to become available"
|
||||
exit 1
|
||||
fi
|
||||
- yarn e2e:pw
|
||||
artifacts:
|
||||
when: on_failure
|
||||
paths:
|
||||
- test/e2e-playwright/test-results
|
||||
- test/e2e-playwright/playwright-report
|
||||
|
||||
build:
|
||||
stage: build
|
||||
tags:
|
||||
|
|
|
|||
8
.gitlab/merge_request_templates/Release.md
Normal file
8
.gitlab/merge_request_templates/Release.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
### Release checklist
|
||||
* [ ] Bump version in `package.json`
|
||||
* [ ] Compile a changelog with the `tools/collect-changelog` script
|
||||
* [ ] Create an MR with an announcement to pleroma.social
|
||||
#### post-merge
|
||||
* [ ] Tag the release on the merge commit
|
||||
* [ ] Make the tag into a Gitlab Release™
|
||||
* [ ] Merge `master` into `develop` (in case the fixes are already in develop, use `git merge -s ours --no-commit` and manually merge the changelogs)
|
||||
|
|
@ -12,6 +12,8 @@
|
|||
"custom-property-pattern": null,
|
||||
"keyframes-name-pattern": null,
|
||||
"scss/operator-no-newline-after": null,
|
||||
"declaration-property-value-no-unknown": true,
|
||||
"scss/declaration-property-value-no-unknown": true,
|
||||
"declaration-block-no-redundant-longhand-properties": [
|
||||
true,
|
||||
{
|
||||
|
|
|
|||
74
CHANGELOG.md
74
CHANGELOG.md
|
|
@ -2,6 +2,76 @@
|
|||
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/).
|
||||
|
||||
## 2.10.1
|
||||
### Fixed
|
||||
- fixed being unable to set actor type from profile page
|
||||
- fixed error when clicking mute menu itself (instead of submenu items)
|
||||
- fixed mute -> domain status submenu not working
|
||||
|
||||
### Internal
|
||||
- Add playwright E2E-tests with an optional docker-based backend
|
||||
|
||||
## 2.10.0
|
||||
### Changed
|
||||
- Temporary changes modal now shows actual countdown instead of fixed timeout
|
||||
- Disabled elements are more disabled now
|
||||
- Rearranged and split settings to make more sense and be less of a wall of text
|
||||
- On mobile settings now take up full width and presented in navigation style
|
||||
improved styles for settings
|
||||
|
||||
### Added
|
||||
- Most of the remaining AdminFE tabs were added into Admin Dashboard
|
||||
- It's now possible to customize PWA Manfiest from PleromaFE
|
||||
- Make every configuration option default-overridable by instance admins
|
||||
|
||||
### Fixed
|
||||
- Fixed settings not appearing if user never touched "show advanced" toggle
|
||||
- Fix display of the broken/deleted/banned users
|
||||
- Fixed incorrect emoji display in post interaction lists
|
||||
- Fixed list title not being saved when editing
|
||||
- Fixed poll notifications not being expandable
|
||||
|
||||
|
||||
## 2.9.3
|
||||
### Fixed
|
||||
- Being unable to update profile
|
||||
|
||||
## 2.9.2
|
||||
### Changed
|
||||
- BREAKING: due to some internal technical changes logging into AdminFE through PleromaFE is no longer possible
|
||||
- User card/profile got an overhaul
|
||||
- Profile editing overhaul
|
||||
- Visually combined subject and content fields in post form
|
||||
- Moved post form's emoji button into input field
|
||||
- Minor visual changes and fixes
|
||||
- Clicking on fav/rt/emoji notifications' contents expands/collapses it
|
||||
- Reduced time taken processing theme by half
|
||||
- Splash screen only appears if loading takes more than 2 seconds
|
||||
|
||||
### Added
|
||||
- Mutes received an update, adding support for regex, muting based on username and expiration time.
|
||||
- Mutes are now synchronized across sessions
|
||||
- Support for expiring mutes and blocks (if available)
|
||||
- Clicking on emoji shows bigger version of it alongside with its shortcode
|
||||
- Admins also are able to copy it into a local pack
|
||||
- Added support for Akkoma and IceShrimp.NET backends
|
||||
- Compatibility with stricter CSP (Akkoma backend)
|
||||
- Added a way to upload new packs from a URL or ZIP file via the Admin Dashboard
|
||||
- Unify show/hide content buttons
|
||||
- Add support for detachable scrollTop button
|
||||
- Option to left-align user bio
|
||||
- Cache assets and emojis with service worker
|
||||
- Indicate currently active V3 theme as a body element class
|
||||
- Add arithmetic blend ISS function
|
||||
|
||||
### Fixed
|
||||
- Display counter for status action buttons when they are in the menu
|
||||
- Fix bookmark button alignment in the extra actions menu
|
||||
- Instance favicons are no longer stretched
|
||||
- A lot more scalable UI fixes
|
||||
- Emoji picker now should work fine when emoji size is increased
|
||||
|
||||
## 2.8.0
|
||||
### Changed
|
||||
- BREAKING: static/img/nsfw.2958239.png is now static/img/nsfw.DepQPhG0.png, which may affect people who specify exactly this path as the cover image
|
||||
|
|
@ -34,8 +104,8 @@ This does not guarantee that browsers will or will not work.
|
|||
- Support displaying time in absolute format
|
||||
- Add draft management system
|
||||
- Compress most kinds of images on upload.
|
||||
- Added option to always convert images to JPEG format instead of using WebP when compressing images.
|
||||
- Added configurable image compression option in general settings, allowing users to control whether images are compressed before upload.
|
||||
- Added option to always convert images to JPEG format instead of using WebP when compressing images.
|
||||
- Added configurable image compression option in general settings, allowing users to control whether images are compressed before upload.
|
||||
- Inform users that Smithereen public polls are public
|
||||
- Splash screen + loading indicator to make process of identifying initialization issues and load performance
|
||||
- UI for making v3 themes and palettes, support for bundling v3 themes
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
# For Translators
|
||||
|
||||
To translate Pleroma-FE, use our weblate server: https://translate.pleroma.social/. If you need to add your language it should be added as a json file in [src/i18n/](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/i18n/) folder and added in a list within [src/i18n/languages.js](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/src/i18n/languages.js).
|
||||
To translate Pleroma-FE, use our weblate server: https://translate.pleroma.social/. If you need to add your language it should be added as a json file in [src/i18n/](https://git.pleroma.social/pleroma/pleroma-fe/src/src/i18n/) folder and added in a list within [src/i18n/languages.js](https://git.pleroma.social/pleroma/pleroma-fe/src/src/i18n/languages.js).
|
||||
|
||||
Pleroma-FE will set your language by your browser locale, but you can change language in settings.
|
||||
|
||||
|
|
@ -32,10 +32,10 @@ yarn unit
|
|||
|
||||
# For Contributors:
|
||||
|
||||
You can create file `/config/local.json` (see [example](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/config/local.example.json)) to enable some convenience dev options:
|
||||
You can create file `/config/local.json` (see [example](https://git.pleroma.social/pleroma/pleroma-fe/src/config/local.example.json)) to enable some convenience dev options:
|
||||
|
||||
* `target`: makes local dev server redirect to some existing instance's BE instead of local BE, useful for testing things in near-production environment and searching for real-life use-cases.
|
||||
* `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/statusnet/config.json`. Only works in dev mode.
|
||||
* `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/pleroma/frontend_configurations`. Only works in dev mode.
|
||||
|
||||
FE Build process also leaves current commit hash in global variable `___pleromafe_commit_hash` so that you can easily see which pleroma-fe commit instance is running, also helps pinpointing which commit was used when FE was bundled into BE.
|
||||
|
||||
|
|
|
|||
149
biome.json
Normal file
149
biome.json
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"includes": ["**", "!!**/dist", "!!tools/emojis.json"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space"
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"domains": {
|
||||
"vue": "recommended"
|
||||
},
|
||||
"rules": {
|
||||
"recommended": false,
|
||||
"complexity": {
|
||||
"noAdjacentSpacesInRegex": "error",
|
||||
"noExtraBooleanCast": "error",
|
||||
"noUselessCatch": "error",
|
||||
"noUselessEscapeInRegex": "error"
|
||||
},
|
||||
"correctness": {
|
||||
"noConstAssign": "error",
|
||||
"noConstantCondition": "error",
|
||||
"noEmptyCharacterClassInRegex": "error",
|
||||
"noEmptyPattern": "error",
|
||||
"noGlobalObjectCalls": "error",
|
||||
"noInvalidBuiltinInstantiation": "error",
|
||||
"noInvalidConstructorSuper": "error",
|
||||
"noNonoctalDecimalEscape": "error",
|
||||
"noPrecisionLoss": "error",
|
||||
"noSelfAssign": "error",
|
||||
"noSetterReturn": "error",
|
||||
"noSwitchDeclarations": "error",
|
||||
"noUndeclaredVariables": "error",
|
||||
"noUnreachable": "error",
|
||||
"noUnreachableSuper": "error",
|
||||
"noUnsafeFinally": "error",
|
||||
"noUnsafeOptionalChaining": "error",
|
||||
"noUnusedLabels": "error",
|
||||
"noUnusedPrivateClassMembers": "error",
|
||||
"noUnusedVariables": "error",
|
||||
"useIsNan": "error",
|
||||
"useValidForDirection": "error",
|
||||
"useValidTypeof": "error",
|
||||
"useYield": "error"
|
||||
},
|
||||
"suspicious": {
|
||||
"noAsyncPromiseExecutor": "error",
|
||||
"noCatchAssign": "error",
|
||||
"noClassAssign": "error",
|
||||
"noCompareNegZero": "error",
|
||||
"noConstantBinaryExpressions": "error",
|
||||
"noControlCharactersInRegex": "error",
|
||||
"noDebugger": "error",
|
||||
"noDuplicateCase": "error",
|
||||
"noDuplicateClassMembers": "error",
|
||||
"noDuplicateElseIf": "error",
|
||||
"noDuplicateObjectKeys": "error",
|
||||
"noDuplicateParameters": "error",
|
||||
"noEmptyBlockStatements": "error",
|
||||
"noFallthroughSwitchClause": "error",
|
||||
"noFunctionAssign": "error",
|
||||
"noGlobalAssign": "error",
|
||||
"noImportAssign": "error",
|
||||
"noIrregularWhitespace": "error",
|
||||
"noMisleadingCharacterClass": "error",
|
||||
"noPrototypeBuiltins": "error",
|
||||
"noRedeclare": "error",
|
||||
"noShadowRestrictedNames": "error",
|
||||
"noSparseArray": "error",
|
||||
"noUnsafeNegation": "error",
|
||||
"noUselessRegexBackrefs": "error",
|
||||
"noWith": "error",
|
||||
"useGetterReturn": "error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "single",
|
||||
"semicolons": "asNeeded"
|
||||
},
|
||||
"globals": []
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"includes": ["**/*.spec.js", "test/fixtures/*.js"],
|
||||
"javascript": {
|
||||
"globals": [
|
||||
"vi",
|
||||
"describe",
|
||||
"it",
|
||||
"test",
|
||||
"expect",
|
||||
"before",
|
||||
"beforeEach",
|
||||
"after",
|
||||
"afterEach"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["**/*.vue"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"useConst": "off",
|
||||
"useImportType": "off"
|
||||
},
|
||||
"correctness": {
|
||||
"noUnusedVariables": "off",
|
||||
"noUnusedImports": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"assist": {
|
||||
"enabled": true,
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": {
|
||||
"level": "on",
|
||||
"options": {
|
||||
"groups": [
|
||||
[":NODE:", ":PACKAGE:", "!src/**", "!@fortawesome/**"],
|
||||
":BLANK_LINE:",
|
||||
[":PATH:", "src/components/**"],
|
||||
":BLANK_LINE:",
|
||||
[":PATH:", "src/stores/**"],
|
||||
":BLANK_LINE:",
|
||||
[":PATH:", "src/**", "src/stores/**", "src/components/**"],
|
||||
":BLANK_LINE:",
|
||||
"@fortawesome/fontawesome-svg-core",
|
||||
"@fortawesome/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import semver from 'semver'
|
||||
import chalk from 'chalk'
|
||||
import semver from 'semver'
|
||||
|
||||
import packageConfig from '../package.json' with { type: 'json' }
|
||||
|
||||
|
|
@ -7,8 +7,8 @@ var versionRequirements = [
|
|||
{
|
||||
name: 'node',
|
||||
currentVersion: semver.clean(process.version),
|
||||
versionRequirement: packageConfig.engines.node
|
||||
}
|
||||
versionRequirement: packageConfig.engines.node,
|
||||
},
|
||||
]
|
||||
|
||||
export default function () {
|
||||
|
|
@ -16,15 +16,22 @@ export default function () {
|
|||
for (let i = 0; i < versionRequirements.length; i++) {
|
||||
const mod = versionRequirements[i]
|
||||
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
|
||||
warnings.push(mod.name + ': ' +
|
||||
chalk.red(mod.currentVersion) + ' should be ' +
|
||||
chalk.green(mod.versionRequirement)
|
||||
warnings.push(
|
||||
mod.name +
|
||||
': ' +
|
||||
chalk.red(mod.currentVersion) +
|
||||
' should be ' +
|
||||
chalk.green(mod.versionRequirement),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (warnings.length) {
|
||||
console.warn(chalk.yellow('\nTo use this template, you must update following to modules:\n'))
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
'\nTo use this template, you must update following to modules:\n',
|
||||
),
|
||||
)
|
||||
for (let i = 0; i < warnings.length; i++) {
|
||||
const warning = warnings[i]
|
||||
console.warn(' ' + warning)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import childProcess from 'child_process'
|
||||
|
||||
export const getCommitHash = (() => {
|
||||
const subst = "$Format:%h$"
|
||||
if(!subst.match(/Format:/)) {
|
||||
export const getCommitHash = () => {
|
||||
const subst = '$Format:%h$'
|
||||
if (!subst.match(/Format:/)) {
|
||||
return subst
|
||||
} else {
|
||||
try {
|
||||
|
|
@ -15,4 +15,4 @@ export const getCommitHash = (() => {
|
|||
return 'UNKNOWN'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import serveStatic from 'serve-static'
|
||||
import { resolve } from 'node:path'
|
||||
import { cp } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
import serveStatic from 'serve-static'
|
||||
|
||||
const getPrefix = s => {
|
||||
const getPrefix = (s) => {
|
||||
const padEnd = s.endsWith('/') ? s : s + '/'
|
||||
return padEnd.startsWith('/') ? padEnd : '/' + padEnd
|
||||
}
|
||||
|
|
@ -13,28 +13,31 @@ const copyPlugin = ({ inUrl, inFs }) => {
|
|||
let copyTarget
|
||||
const handler = serveStatic(inFs)
|
||||
|
||||
return [{
|
||||
name: 'copy-plugin-serve',
|
||||
apply: 'serve',
|
||||
configureServer (server) {
|
||||
server.middlewares.use(prefix, handler)
|
||||
}
|
||||
}, {
|
||||
name: 'copy-plugin-build',
|
||||
apply: 'build',
|
||||
configResolved (config) {
|
||||
copyTarget = resolve(config.root, config.build.outDir, subdir)
|
||||
return [
|
||||
{
|
||||
name: 'copy-plugin-serve',
|
||||
apply: 'serve',
|
||||
configureServer(server) {
|
||||
server.middlewares.use(prefix, handler)
|
||||
},
|
||||
},
|
||||
closeBundle: {
|
||||
order: 'post',
|
||||
sequential: true,
|
||||
async handler () {
|
||||
console.log(`Copying '${inFs}' to ${copyTarget}...`)
|
||||
await cp(inFs, copyTarget, { recursive: true })
|
||||
console.log('Done.')
|
||||
}
|
||||
}
|
||||
}]
|
||||
{
|
||||
name: 'copy-plugin-build',
|
||||
apply: 'build',
|
||||
configResolved(config) {
|
||||
copyTarget = resolve(config.root, config.build.outDir, subdir)
|
||||
},
|
||||
closeBundle: {
|
||||
order: 'post',
|
||||
sequential: true,
|
||||
async handler() {
|
||||
console.info(`Copying '${inFs}' to ${copyTarget}...`)
|
||||
await cp(inFs, copyTarget, { recursive: true })
|
||||
console.info('Done.')
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export default copyPlugin
|
||||
|
|
|
|||
|
|
@ -1,21 +1,23 @@
|
|||
import { resolve } from 'node:path'
|
||||
import { access } from 'node:fs/promises'
|
||||
import { languages, langCodeToCldrName } from '../src/i18n/languages.js'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
import { languages } from '../src/i18n/languages.js'
|
||||
|
||||
const annotationsImportPrefix = '@kazvmoe-infra/unicode-emoji-json/annotations/'
|
||||
const specialAnnotationsLocale = {
|
||||
ja_easy: 'ja'
|
||||
ja_easy: 'ja',
|
||||
}
|
||||
|
||||
const internalToAnnotationsLocale = (internal) => specialAnnotationsLocale[internal] || internal
|
||||
const internalToAnnotationsLocale = (internal) =>
|
||||
specialAnnotationsLocale[internal] || internal
|
||||
|
||||
// This gets all the annotations that are accessible (whose language
|
||||
// can be chosen in the settings). Data for other languages are
|
||||
// discarded because there is no way for it to be fetched.
|
||||
const getAllAccessibleAnnotations = async (projectRoot) => {
|
||||
const imports = (await Promise.all(
|
||||
languages
|
||||
.map(async lang => {
|
||||
const imports = (
|
||||
await Promise.all(
|
||||
languages.map(async (lang) => {
|
||||
const destLang = internalToAnnotationsLocale(lang)
|
||||
const importModule = `${annotationsImportPrefix}${destLang}.json`
|
||||
const importFile = resolve(projectRoot, 'node_modules', importModule)
|
||||
|
|
@ -23,11 +25,18 @@ const getAllAccessibleAnnotations = async (projectRoot) => {
|
|||
await access(importFile)
|
||||
return `'${lang}': () => import('${importModule}')`
|
||||
} catch (e) {
|
||||
if (e.message.match(/ENOENT/)) {
|
||||
console.warn(`Missing emoji annotations locale: ${destLang}`)
|
||||
} else {
|
||||
console.error('test', e.message)
|
||||
}
|
||||
return
|
||||
}
|
||||
})))
|
||||
.filter(k => k)
|
||||
.join(',\n')
|
||||
}),
|
||||
)
|
||||
)
|
||||
.filter((k) => k)
|
||||
.join(',\n')
|
||||
|
||||
return `
|
||||
export const annotationsLoader = {
|
||||
|
|
@ -43,21 +52,21 @@ const emojisPlugin = () => {
|
|||
let projectRoot
|
||||
return {
|
||||
name: 'emojis-plugin',
|
||||
configResolved (conf) {
|
||||
configResolved(conf) {
|
||||
projectRoot = conf.root
|
||||
},
|
||||
resolveId (id) {
|
||||
resolveId(id) {
|
||||
if (id === emojiAnnotationsId) {
|
||||
return emojiAnnotationsIdResolved
|
||||
}
|
||||
return null
|
||||
},
|
||||
async load (id) {
|
||||
async load(id) {
|
||||
if (id === emojiAnnotationsIdResolved) {
|
||||
return await getAllAccessibleAnnotations(projectRoot)
|
||||
}
|
||||
return null
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { resolve } from 'node:path'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const target = 'node_modules/msw/lib/mockServiceWorker.js'
|
||||
|
||||
|
|
@ -8,10 +8,10 @@ const mswPlugin = () => {
|
|||
return {
|
||||
name: 'msw-plugin',
|
||||
apply: 'serve',
|
||||
configResolved (conf) {
|
||||
configResolved(conf) {
|
||||
projectRoot = conf.root
|
||||
},
|
||||
configureServer (server) {
|
||||
configureServer(server) {
|
||||
server.middlewares.use(async (req, res, next) => {
|
||||
if (req.path === '/mockServiceWorker.js') {
|
||||
const file = await readFile(resolve(projectRoot, target))
|
||||
|
|
@ -21,7 +21,7 @@ const mswPlugin = () => {
|
|||
next()
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { languages, langCodeToJsonName } from '../src/i18n/languages.js'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { langCodeToJsonName, languages } from '../src/i18n/languages.js'
|
||||
|
||||
const i18nDir = resolve(
|
||||
dirname(dirname(fileURLToPath(import.meta.url))),
|
||||
'src/i18n'
|
||||
'src/i18n',
|
||||
)
|
||||
|
||||
export const i18nFiles = languages.reduce((acc, lang) => {
|
||||
|
|
@ -16,13 +17,15 @@ export const i18nFiles = languages.reduce((acc, lang) => {
|
|||
}, {})
|
||||
|
||||
export const generateServiceWorkerMessages = async () => {
|
||||
const msgArray = await Promise.all(Object.entries(i18nFiles).map(async ([lang, file]) => {
|
||||
const fileContent = await readFile(file, 'utf-8')
|
||||
const msg = {
|
||||
notifications: JSON.parse(fileContent).notifications || {}
|
||||
}
|
||||
return [lang, msg]
|
||||
}))
|
||||
const msgArray = await Promise.all(
|
||||
Object.entries(i18nFiles).map(async ([lang, file]) => {
|
||||
const fileContent = await readFile(file, 'utf-8')
|
||||
const msg = {
|
||||
notifications: JSON.parse(fileContent).notifications || {},
|
||||
}
|
||||
return [lang, msg]
|
||||
}),
|
||||
)
|
||||
return msgArray.reduce((acc, [lang, msg]) => {
|
||||
acc[lang] = msg
|
||||
return acc
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { build } from 'vite'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import * as esbuild from 'esbuild'
|
||||
import { generateServiceWorkerMessages, i18nFiles } from './service_worker_messages.js'
|
||||
import { build } from 'vite'
|
||||
|
||||
import {
|
||||
generateServiceWorkerMessages,
|
||||
i18nFiles,
|
||||
} from './service_worker_messages.js'
|
||||
|
||||
const getSWMessagesAsText = async () => {
|
||||
const messages = await generateServiceWorkerMessages()
|
||||
|
|
@ -14,14 +18,10 @@ const projectRoot = dirname(dirname(fileURLToPath(import.meta.url)))
|
|||
const swEnvName = 'virtual:pleroma-fe/service_worker_env'
|
||||
const swEnvNameResolved = '\0' + swEnvName
|
||||
const getDevSwEnv = () => `self.serviceWorkerOption = { assets: [] };`
|
||||
const getProdSwEnv = ({ assets }) => `self.serviceWorkerOption = { assets: ${JSON.stringify(assets)} };`
|
||||
const getProdSwEnv = ({ assets }) =>
|
||||
`self.serviceWorkerOption = { assets: ${JSON.stringify(assets)} };`
|
||||
|
||||
export const devSwPlugin = ({
|
||||
swSrc,
|
||||
swDest,
|
||||
transformSW,
|
||||
alias
|
||||
}) => {
|
||||
export const devSwPlugin = ({ swSrc, swDest, transformSW, alias }) => {
|
||||
const swFullSrc = resolve(projectRoot, swSrc)
|
||||
const esbuildAlias = {}
|
||||
Object.entries(alias).forEach(([source, dest]) => {
|
||||
|
|
@ -31,9 +31,10 @@ export const devSwPlugin = ({
|
|||
return {
|
||||
name: 'dev-sw-plugin',
|
||||
apply: 'serve',
|
||||
configResolved (conf) {
|
||||
configResolved() {
|
||||
/* no-op */
|
||||
},
|
||||
resolveId (id) {
|
||||
resolveId(id) {
|
||||
const name = id.startsWith('/') ? id.slice(1) : id
|
||||
if (name === swDest) {
|
||||
return swFullSrc
|
||||
|
|
@ -42,7 +43,7 @@ export const devSwPlugin = ({
|
|||
}
|
||||
return null
|
||||
},
|
||||
async load (id) {
|
||||
async load(id) {
|
||||
if (id === swFullSrc) {
|
||||
return readFile(swFullSrc, 'utf-8')
|
||||
} else if (id === swEnvNameResolved) {
|
||||
|
|
@ -55,7 +56,7 @@ export const devSwPlugin = ({
|
|||
* during dev, and firefox does not support ESM as service worker
|
||||
* https://bugzilla.mozilla.org/show_bug.cgi?id=1360870
|
||||
*/
|
||||
async transform (code, id) {
|
||||
async transform(code, id) {
|
||||
if (id === swFullSrc && transformSW) {
|
||||
const res = await esbuild.build({
|
||||
entryPoints: [swSrc],
|
||||
|
|
@ -63,52 +64,54 @@ export const devSwPlugin = ({
|
|||
write: false,
|
||||
outfile: 'sw-pleroma.js',
|
||||
alias: esbuildAlias,
|
||||
plugins: [{
|
||||
name: 'vite-like-root-resolve',
|
||||
setup (b) {
|
||||
b.onResolve(
|
||||
{ filter: new RegExp(/^\//) },
|
||||
args => ({
|
||||
path: resolve(projectRoot, args.path.slice(1))
|
||||
})
|
||||
)
|
||||
}
|
||||
}, {
|
||||
name: 'sw-messages',
|
||||
setup (b) {
|
||||
b.onResolve(
|
||||
{ filter: new RegExp('^' + swMessagesName + '$') },
|
||||
args => ({
|
||||
path: args.path,
|
||||
namespace: 'sw-messages'
|
||||
plugins: [
|
||||
{
|
||||
name: 'vite-like-root-resolve',
|
||||
setup(b) {
|
||||
b.onResolve({ filter: new RegExp(/^\//) }, (args) => ({
|
||||
path: resolve(projectRoot, args.path.slice(1)),
|
||||
}))
|
||||
b.onLoad(
|
||||
{ filter: /.*/, namespace: 'sw-messages' },
|
||||
async () => ({
|
||||
contents: await getSWMessagesAsText()
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'sw-messages',
|
||||
setup(b) {
|
||||
b.onResolve(
|
||||
{ filter: new RegExp('^' + swMessagesName + '$') },
|
||||
(args) => ({
|
||||
path: args.path,
|
||||
namespace: 'sw-messages',
|
||||
}),
|
||||
)
|
||||
b.onLoad(
|
||||
{ filter: /.*/, namespace: 'sw-messages' },
|
||||
async () => ({
|
||||
contents: await getSWMessagesAsText(),
|
||||
}),
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'sw-env',
|
||||
setup(b) {
|
||||
b.onResolve(
|
||||
{ filter: new RegExp('^' + swEnvName + '$') },
|
||||
(args) => ({
|
||||
path: args.path,
|
||||
namespace: 'sw-env',
|
||||
}),
|
||||
)
|
||||
b.onLoad({ filter: /.*/, namespace: 'sw-env' }, () => ({
|
||||
contents: getDevSwEnv(),
|
||||
}))
|
||||
}
|
||||
}, {
|
||||
name: 'sw-env',
|
||||
setup (b) {
|
||||
b.onResolve(
|
||||
{ filter: new RegExp('^' + swEnvName + '$') },
|
||||
args => ({
|
||||
path: args.path,
|
||||
namespace: 'sw-env'
|
||||
}))
|
||||
b.onLoad(
|
||||
{ filter: /.*/, namespace: 'sw-env' },
|
||||
() => ({
|
||||
contents: getDevSwEnv()
|
||||
}))
|
||||
}
|
||||
}]
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
const text = res.outputFiles[0].text
|
||||
return text
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -118,16 +121,13 @@ export const devSwPlugin = ({
|
|||
// however, we must compile the service worker to iife because of browser support.
|
||||
// Run another vite build just for the service worker targeting iife at
|
||||
// the end of the build.
|
||||
export const buildSwPlugin = ({
|
||||
swSrc,
|
||||
swDest,
|
||||
}) => {
|
||||
export const buildSwPlugin = ({ swSrc, swDest }) => {
|
||||
let config
|
||||
return {
|
||||
name: 'build-sw-plugin',
|
||||
enforce: 'post',
|
||||
apply: 'build',
|
||||
configResolved (resolvedConfig) {
|
||||
configResolved(resolvedConfig) {
|
||||
config = {
|
||||
define: resolvedConfig.define,
|
||||
resolve: resolvedConfig.resolve,
|
||||
|
|
@ -138,50 +138,50 @@ export const buildSwPlugin = ({
|
|||
lib: {
|
||||
entry: swSrc,
|
||||
formats: ['iife'],
|
||||
name: 'sw_pleroma'
|
||||
name: 'sw_pleroma',
|
||||
},
|
||||
emptyOutDir: false,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: swDest
|
||||
}
|
||||
}
|
||||
entryFileNames: swDest,
|
||||
},
|
||||
},
|
||||
},
|
||||
configFile: false
|
||||
configFile: false,
|
||||
}
|
||||
},
|
||||
generateBundle: {
|
||||
order: 'post',
|
||||
sequential: true,
|
||||
async handler (_, bundle) {
|
||||
async handler(_, bundle) {
|
||||
const assets = Object.keys(bundle)
|
||||
.filter(name => !/\.map$/.test(name))
|
||||
.map(name => '/' + name)
|
||||
.filter((name) => !/\.map$/.test(name))
|
||||
.map((name) => '/' + name)
|
||||
config.plugins.push({
|
||||
name: 'build-sw-env-plugin',
|
||||
resolveId (id) {
|
||||
resolveId(id) {
|
||||
if (id === swEnvName) {
|
||||
return swEnvNameResolved
|
||||
}
|
||||
return null
|
||||
},
|
||||
load (id) {
|
||||
load(id) {
|
||||
if (id === swEnvNameResolved) {
|
||||
return getProdSwEnv({ assets })
|
||||
}
|
||||
return null
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
closeBundle: {
|
||||
order: 'post',
|
||||
sequential: true,
|
||||
async handler () {
|
||||
console.log('Building service worker for production')
|
||||
async handler() {
|
||||
console.info('Building service worker for production')
|
||||
await build(config)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -191,9 +191,9 @@ const swMessagesNameResolved = '\0' + swMessagesName
|
|||
export const swMessagesPlugin = () => {
|
||||
return {
|
||||
name: 'sw-messages-plugin',
|
||||
resolveId (id) {
|
||||
resolveId(id) {
|
||||
if (id === swMessagesName) {
|
||||
Object.values(i18nFiles).forEach(f => {
|
||||
Object.values(i18nFiles).forEach((f) => {
|
||||
this.addWatchFile(f)
|
||||
})
|
||||
return swMessagesNameResolved
|
||||
|
|
@ -201,11 +201,11 @@ export const swMessagesPlugin = () => {
|
|||
return null
|
||||
}
|
||||
},
|
||||
async load (id) {
|
||||
async load(id) {
|
||||
if (id === swMessagesNameResolved) {
|
||||
return await getSWMessagesAsText()
|
||||
}
|
||||
return null
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,21 @@
|
|||
|
||||
import emojis from '@kazvmoe-infra/unicode-emoji-json/data-by-group.json' with { type: 'json' }
|
||||
import emojis from '@kazvmoe-infra/unicode-emoji-json/data-by-group.json' with {
|
||||
type: 'json',
|
||||
}
|
||||
import fs from 'fs'
|
||||
|
||||
Object.keys(emojis)
|
||||
.map(k => {
|
||||
emojis[k].map(e => {
|
||||
delete e.unicode_version
|
||||
delete e.emoji_version
|
||||
delete e.skin_tone_support_unicode_version
|
||||
})
|
||||
Object.keys(emojis).map((k) => {
|
||||
emojis[k].map((e) => {
|
||||
delete e.unicode_version
|
||||
delete e.emoji_version
|
||||
delete e.skin_tone_support_unicode_version
|
||||
})
|
||||
})
|
||||
|
||||
const res = {}
|
||||
Object.keys(emojis)
|
||||
.map(k => {
|
||||
const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase()
|
||||
res[groupId] = emojis[k]
|
||||
})
|
||||
Object.keys(emojis).map((k) => {
|
||||
const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase()
|
||||
res[groupId] = emojis[k]
|
||||
})
|
||||
|
||||
console.info('Updating emojis...')
|
||||
fs.writeFileSync('src/assets/emoji.json', JSON.stringify(res))
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
Display counter for status action buttons when they are on the menu
|
||||
|
|
@ -1 +0,0 @@
|
|||
Added support for Akkoma and IceShrimp.NET backend
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
Add arithmetic blend ISS function
|
||||
|
||||
1
changelog.d/attrs-parsing.fix
Normal file
1
changelog.d/attrs-parsing.fix
Normal file
|
|
@ -0,0 +1 @@
|
|||
Fix HTML attribute parsing for escaped quotes
|
||||
|
|
@ -1 +0,0 @@
|
|||
Add support for detachable scrollTop button
|
||||
|
|
@ -1 +0,0 @@
|
|||
Fix bookmark button alignment in the extra actions menu
|
||||
|
|
@ -1 +0,0 @@
|
|||
Compatibility with stricter CSP (Akkoma backend)
|
||||
1
changelog.d/fix-emojis-breaking-bio.fix
Normal file
1
changelog.d/fix-emojis-breaking-bio.fix
Normal file
|
|
@ -0,0 +1 @@
|
|||
Fix emojis breaking user bio/description editing
|
||||
|
|
@ -1 +0,0 @@
|
|||
Synchronized mutes, advanced mute control (regexp, expiry, naming)
|
||||
|
|
@ -1 +0,0 @@
|
|||
Fix error styling for user profiles
|
||||
|
|
@ -1 +0,0 @@
|
|||
Cache assets and emojis with service worker
|
||||
|
|
@ -1 +0,0 @@
|
|||
Indicate currently active V3 theme as a body element class
|
||||
|
|
@ -1 +0,0 @@
|
|||
Unify show/hide content buttons
|
||||
57
docker-compose.e2e.yml
Normal file
57
docker-compose.e2e.yml
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
services:
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
environment:
|
||||
POSTGRES_USER: pleroma
|
||||
POSTGRES_PASSWORD: pleroma
|
||||
POSTGRES_DB: pleroma
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U pleroma -d pleroma"]
|
||||
interval: 2s
|
||||
timeout: 2s
|
||||
retries: 30
|
||||
|
||||
pleroma:
|
||||
image: ${PLEROMA_IMAGE:-git.pleroma.social:5050/pleroma/pleroma:stable}
|
||||
environment:
|
||||
DB_USER: pleroma
|
||||
DB_PASS: pleroma
|
||||
DB_NAME: pleroma
|
||||
DB_HOST: db
|
||||
DB_PORT: 5432
|
||||
DOMAIN: localhost
|
||||
INSTANCE_NAME: Pleroma E2E
|
||||
ADMIN_EMAIL: ${E2E_ADMIN_EMAIL:-admin@example.com}
|
||||
NOTIFY_EMAIL: ${E2E_ADMIN_EMAIL:-admin@example.com}
|
||||
E2E_ADMIN_USERNAME: ${E2E_ADMIN_USERNAME:-admin}
|
||||
E2E_ADMIN_PASSWORD: ${E2E_ADMIN_PASSWORD:-adminadmin}
|
||||
E2E_ADMIN_EMAIL: ${E2E_ADMIN_EMAIL:-admin@example.com}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./docker/pleroma/entrypoint.e2e.sh:/opt/pleroma/entrypoint.e2e.sh:ro
|
||||
entrypoint: ["/bin/ash", "/opt/pleroma/entrypoint.e2e.sh"]
|
||||
healthcheck:
|
||||
# NOTE: "localhost" may resolve to ::1 in some images (IPv6) while Pleroma only
|
||||
# listens on IPv4 in this container. Use 127.0.0.1 to avoid false negatives.
|
||||
test: ["CMD-SHELL", "test -f /var/lib/pleroma/.e2e_seeded && wget -qO- http://127.0.0.1:4000/api/v1/instance >/dev/null || exit 1"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 60
|
||||
|
||||
e2e:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/e2e/Dockerfile.e2e
|
||||
depends_on:
|
||||
pleroma:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
CI: "1"
|
||||
VITE_PROXY_TARGET: http://pleroma:4000
|
||||
VITE_PROXY_ORIGIN: http://localhost:4000
|
||||
E2E_BASE_URL: http://localhost:8080
|
||||
E2E_ADMIN_USERNAME: ${E2E_ADMIN_USERNAME:-admin}
|
||||
E2E_ADMIN_PASSWORD: ${E2E_ADMIN_PASSWORD:-adminadmin}
|
||||
command: ["yarn", "e2e:pw"]
|
||||
16
docker/e2e/Dockerfile.e2e
Normal file
16
docker/e2e/Dockerfile.e2e
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
FROM mcr.microsoft.com/playwright:v1.57.0-jammy
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
||||
|
||||
RUN npm install -g yarn@1.22.22
|
||||
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
|
||||
ENV CI=1
|
||||
|
||||
CMD ["yarn", "e2e:pw"]
|
||||
71
docker/pleroma/entrypoint.e2e.sh
Normal file
71
docker/pleroma/entrypoint.e2e.sh
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
#!/bin/ash
|
||||
|
||||
set -eu
|
||||
|
||||
SEED_SENTINEL_PATH="/var/lib/pleroma/.e2e_seeded"
|
||||
CONFIG_OVERRIDE_PATH="/var/lib/pleroma/config.exs"
|
||||
|
||||
echo "-- Waiting for database..."
|
||||
while ! pg_isready -U "${DB_USER:-pleroma}" -d "postgres://${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-pleroma}" -t 1; do
|
||||
sleep 1s
|
||||
done
|
||||
|
||||
echo "-- Writing E2E config overrides..."
|
||||
cat > "$CONFIG_OVERRIDE_PATH" <<'EOF'
|
||||
import Config
|
||||
|
||||
config :pleroma, Pleroma.Captcha,
|
||||
enabled: false
|
||||
|
||||
config :pleroma, :instance,
|
||||
registrations_open: true,
|
||||
account_activation_required: false,
|
||||
approval_required: false
|
||||
EOF
|
||||
|
||||
echo "-- Running migrations..."
|
||||
/opt/pleroma/bin/pleroma_ctl migrate
|
||||
|
||||
echo "-- Starting!"
|
||||
/opt/pleroma/bin/pleroma start &
|
||||
PLEROMA_PID="$!"
|
||||
|
||||
cleanup() {
|
||||
if [ -n "${PLEROMA_PID:-}" ] && kill -0 "$PLEROMA_PID" 2>/dev/null; then
|
||||
kill -TERM "$PLEROMA_PID"
|
||||
wait "$PLEROMA_PID" || true
|
||||
fi
|
||||
}
|
||||
|
||||
trap cleanup INT TERM
|
||||
|
||||
echo "-- Waiting for API..."
|
||||
api_ok="false"
|
||||
for _i in $(seq 1 120); do
|
||||
if wget -qO- http://127.0.0.1:4000/api/v1/instance >/dev/null 2>&1; then
|
||||
api_ok="true"
|
||||
break
|
||||
fi
|
||||
sleep 1s
|
||||
done
|
||||
|
||||
if [ "$api_ok" != "true" ]; then
|
||||
echo "Timed out waiting for Pleroma API to become available"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$SEED_SENTINEL_PATH" ]; then
|
||||
if [ -n "${E2E_ADMIN_USERNAME:-}" ] && [ -n "${E2E_ADMIN_PASSWORD:-}" ] && [ -n "${E2E_ADMIN_EMAIL:-}" ]; then
|
||||
echo "-- Seeding admin user (${E2E_ADMIN_USERNAME})..."
|
||||
if ! /opt/pleroma/bin/pleroma_ctl user new "$E2E_ADMIN_USERNAME" "$E2E_ADMIN_EMAIL" --admin --password "$E2E_ADMIN_PASSWORD" -y; then
|
||||
echo "-- User already exists (or creation failed), ensuring admin + confirmed..."
|
||||
/opt/pleroma/bin/pleroma_ctl user set "$E2E_ADMIN_USERNAME" --admin --confirmed
|
||||
fi
|
||||
else
|
||||
echo "-- Skipping admin seeding (missing E2E_ADMIN_* env)"
|
||||
fi
|
||||
|
||||
touch "$SEED_SENTINEL_PATH"
|
||||
fi
|
||||
|
||||
wait "$PLEROMA_PID"
|
||||
|
|
@ -7,9 +7,9 @@
|
|||
|
||||
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](../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).
|
||||
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) )
|
||||
1. `/api/pleroma/frontend_configurations` - this is generated by backend and includes FE/Client-specific data. PleromaFE uses the `pleroma_fe` 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/src/public/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/src/src/stores/instance.js) )
|
||||
|
||||
## Instance-defaults
|
||||
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ server {
|
|||
|
||||
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](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.
|
||||
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/src/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.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,37 +1,34 @@
|
|||
import vue from "eslint-plugin-vue";
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import js from '@eslint/js'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
import vue from 'eslint-plugin-vue'
|
||||
import globals from 'globals'
|
||||
|
||||
|
||||
export default [
|
||||
export default defineConfig([
|
||||
...vue.configs['flat/recommended'],
|
||||
js.configs.recommended,
|
||||
globalIgnores(['**/*.js', 'build/', 'dist/', 'config/']),
|
||||
{
|
||||
files: ["**/*.js", "**/*.mjs", "**/*.vue"],
|
||||
ignores: ["build/*.js", "config/*.js"],
|
||||
|
||||
files: ['src/**/*.vue'],
|
||||
plugins: { js },
|
||||
extends: ['js/recommended'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2024,
|
||||
sourceType: "module",
|
||||
sourceType: 'module',
|
||||
|
||||
parserOptions: {
|
||||
parser: "@babel/eslint-parser",
|
||||
parser: '@babel/eslint-parser',
|
||||
},
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.vitest,
|
||||
...globals.chai,
|
||||
...globals.commonjs,
|
||||
...globals.serviceworker
|
||||
}
|
||||
...globals.serviceworker,
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
'arrow-parens': 0,
|
||||
'generator-star-spacing': 0,
|
||||
'no-debugger': 0,
|
||||
'vue/require-prop-types': 0,
|
||||
'vue/multi-word-component-names': 0,
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
])
|
||||
|
|
|
|||
|
|
@ -11,14 +11,12 @@
|
|||
<link rel="preload" href="/static/pleromatan_apology_fox_small.webp" as="image" />
|
||||
<!-- putting styles here to avoid having to wait for styles to load up -->
|
||||
<link rel="stylesheet" id="splashscreen" href="/static/splash.css" />
|
||||
<link rel="stylesheet" id="pleroma-eager-styles" type="text/css" href="/static/empty.css" />
|
||||
<link rel="stylesheet" id="pleroma-lazy-styles" type="text/css" href="/static/empty.css" />
|
||||
<link rel="stylesheet" id="theme-holder" type="text/css" href="/static/empty.css" />
|
||||
<link rel="stylesheet" id="custom-styles-holder" type="text/css" href="/static/empty.css" />
|
||||
<!--server-generated-meta-->
|
||||
</head>
|
||||
<body>
|
||||
<noscript>To use Pleroma, please enable JavaScript.</noscript>
|
||||
<div id="splash">
|
||||
<div id="splash" class="initial-hidden">
|
||||
<!-- we are hiding entire graphic so no point showing credit -->
|
||||
<div aria-hidden="true" id="splash-credit">
|
||||
Art by pipivovott
|
||||
|
|
|
|||
85
package.json
85
package.json
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "pleroma_fe",
|
||||
"version": "2.7.1",
|
||||
"version": "2.10.1",
|
||||
"description": "Pleroma frontend, the default frontend of Pleroma social network server",
|
||||
"author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/CONTRIBUTORS.md>",
|
||||
"author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/src/CONTRIBUTORS.md>",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"dev": "node build/update-emoji.js && vite dev",
|
||||
|
|
@ -10,36 +10,39 @@
|
|||
"unit": "node build/update-emoji.js && vitest --run",
|
||||
"unit-ci": "node build/update-emoji.js && vitest --run --browser.headless",
|
||||
"unit:watch": "node build/update-emoji.js && vitest",
|
||||
"e2e": "node test/e2e/runner.js",
|
||||
"e2e:pw": "playwright test --config test/e2e-playwright/playwright.config.mjs",
|
||||
"e2e": "sh ./tools/e2e/run.sh",
|
||||
"test": "yarn run unit && yarn run e2e",
|
||||
"stylelint": "yarn exec stylelint '**/*.scss' '**/*.vue'",
|
||||
"lint": "eslint src test/unit/specs test/e2e/specs",
|
||||
"lint-fix": "eslint --fix src test/unit/specs test/e2e/specs"
|
||||
"ci-biome": "yarn exec biome check",
|
||||
"ci-eslint": "yarn exec eslint",
|
||||
"ci-stylelint": "yarn exec stylelint '**/*.scss' '**/*.vue'",
|
||||
"lint": "yarn ci-biome; yarn ci-eslint; yarn ci-stylelint",
|
||||
"lint-fix": "yarn exec eslint -- --fix; yarn exec stylelint '**/*.scss' '**/*.vue' --fix; biome check --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.27.1",
|
||||
"@babel/runtime": "7.28.4",
|
||||
"@chenfengyuan/vue-qrcode": "2.0.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.7.2",
|
||||
"@fortawesome/vue-fontawesome": "3.0.8",
|
||||
"@floatingghost/pinch-zoom-element": "1.3.1",
|
||||
"@fortawesome/fontawesome-svg-core": "7.1.0",
|
||||
"@fortawesome/free-regular-svg-icons": "7.1.0",
|
||||
"@fortawesome/free-solid-svg-icons": "7.1.0",
|
||||
"@fortawesome/vue-fontawesome": "3.1.2",
|
||||
"@kazvmoe-infra/pinch-zoom-element": "1.3.0",
|
||||
"@kazvmoe-infra/unicode-emoji-json": "0.4.0",
|
||||
"@ruffle-rs/ruffle": "0.1.0-nightly.2025.1.13",
|
||||
"@ruffle-rs/ruffle": "0.1.0-nightly.2025.6.22",
|
||||
"@vuelidate/core": "2.0.3",
|
||||
"@vuelidate/validators": "2.0.4",
|
||||
"@web3-storage/parse-link-header": "^3.1.0",
|
||||
"body-scroll-lock": "3.1.5",
|
||||
"chromatism": "3.0.0",
|
||||
"click-outside-vue3": "4.0.1",
|
||||
"cropperjs": "2.0.0",
|
||||
"cropperjs": "2.0.1",
|
||||
"escape-html": "1.0.3",
|
||||
"globals": "^16.0.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
"js-cookie": "3.0.5",
|
||||
"localforage": "1.10.0",
|
||||
"parse-link-header": "2.0.0",
|
||||
"phoenix": "1.7.21",
|
||||
"phoenix": "1.8.1",
|
||||
"pinia": "^3.0.0",
|
||||
"punycode.js": "2.3.1",
|
||||
"qrcode": "1.5.4",
|
||||
|
|
@ -47,63 +50,64 @@
|
|||
"url": "0.11.4",
|
||||
"utf8": "3.0.0",
|
||||
"uuid": "11.1.0",
|
||||
"vue": "3.5.17",
|
||||
"vue": "3.5.22",
|
||||
"vue-i18n": "11",
|
||||
"vue-router": "4.5.1",
|
||||
"vue-router": "4.6.4",
|
||||
"vue-virtual-scroller": "^2.0.0-beta.7",
|
||||
"vuex": "4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.27.1",
|
||||
"@babel/eslint-parser": "7.27.1",
|
||||
"@babel/plugin-transform-runtime": "7.27.1",
|
||||
"@babel/preset-env": "7.27.2",
|
||||
"@babel/register": "7.27.1",
|
||||
"@babel/core": "7.28.5",
|
||||
"@babel/eslint-parser": "7.28.5",
|
||||
"@babel/plugin-transform-runtime": "7.28.5",
|
||||
"@babel/preset-env": "7.28.5",
|
||||
"@babel/register": "7.28.3",
|
||||
"@biomejs/biome": "2.3.11",
|
||||
"@pinia/testing": "1.0.3",
|
||||
"@ungap/event-target": "0.2.4",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitejs/plugin-vue-jsx": "^4.1.1",
|
||||
"@vitest/browser": "^3.0.7",
|
||||
"@vitest/ui": "^3.0.7",
|
||||
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
|
||||
"@vue/babel-plugin-jsx": "1.4.0",
|
||||
"@vue/compiler-sfc": "3.5.17",
|
||||
"@vue/babel-plugin-jsx": "1.5.0",
|
||||
"@vue/compiler-sfc": "3.5.22",
|
||||
"@vue/test-utils": "2.4.6",
|
||||
"autoprefixer": "10.4.21",
|
||||
"babel-plugin-lodash": "3.3.4",
|
||||
"chai": "5.2.0",
|
||||
"chalk": "5.4.1",
|
||||
"chai": "5.3.3",
|
||||
"chalk": "5.6.2",
|
||||
"chromedriver": "135.0.4",
|
||||
"connect-history-api-fallback": "2.0.0",
|
||||
"cross-spawn": "7.0.6",
|
||||
"custom-event-polyfill": "1.0.7",
|
||||
"eslint": "9.26.0",
|
||||
"vue-eslint-parser": "10.1.3",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-standard": "17.1.0",
|
||||
"eslint-formatter-friendly": "7.0.0",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"eslint-plugin-n": "17.18.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-n": "17.23.1",
|
||||
"eslint-plugin-promise": "7.2.1",
|
||||
"eslint-plugin-vue": "10.1.0",
|
||||
"eslint-plugin-vue": "10.6.2",
|
||||
"eventsource-polyfill": "0.9.6",
|
||||
"express": "5.1.0",
|
||||
"function-bind": "1.1.2",
|
||||
"http-proxy-middleware": "3.0.5",
|
||||
"iso-639-1": "3.1.5",
|
||||
"lodash": "4.17.21",
|
||||
"msw": "2.10.2",
|
||||
"nightwatch": "3.12.1",
|
||||
"playwright": "1.52.0",
|
||||
"postcss": "8.5.3",
|
||||
"msw": "2.10.5",
|
||||
"nightwatch": "3.12.2",
|
||||
"playwright": "1.57.0",
|
||||
"postcss": "8.5.6",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-scss": "^4.0.6",
|
||||
"sass": "1.89.2",
|
||||
"sass": "1.93.2",
|
||||
"selenium-server": "3.141.59",
|
||||
"semver": "7.7.2",
|
||||
"semver": "7.7.3",
|
||||
"serve-static": "2.2.0",
|
||||
"shelljs": "0.10.0",
|
||||
"sinon": "20.0.0",
|
||||
"sinon-chai": "4.0.0",
|
||||
"stylelint": "16.19.1",
|
||||
"sinon-chai": "4.0.1",
|
||||
"stylelint": "16.25.0",
|
||||
"stylelint-config-html": "^1.1.0",
|
||||
"stylelint-config-recommended": "^16.0.0",
|
||||
"stylelint-config-recommended-scss": "^14.0.0",
|
||||
|
|
@ -112,7 +116,8 @@
|
|||
"vite": "^6.1.0",
|
||||
"vite-plugin-eslint2": "^5.0.3",
|
||||
"vite-plugin-stylelint": "^6.0.0",
|
||||
"vitest": "^3.0.7"
|
||||
"vitest": "^3.0.7",
|
||||
"vue-eslint-parser": "10.2.0"
|
||||
},
|
||||
"type": "module",
|
||||
"engines": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import autoprefixer from 'autoprefixer'
|
||||
|
||||
export default {
|
||||
plugins: [
|
||||
autoprefixer
|
||||
]
|
||||
plugins: [autoprefixer],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
// nothing here //
|
||||
|
|
@ -1,6 +1,26 @@
|
|||
{
|
||||
"pleroma-dark": [ "Pleroma Dark", "#121a24", "#182230", "#b9b9ba", "#d8a070", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
|
||||
"pleroma-light": [ "Pleroma Light", "#f2f4f6", "#dbe0e8", "#304055", "#f86f0f", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
|
||||
"pleroma-dark": [
|
||||
"Pleroma Dark",
|
||||
"#121a24",
|
||||
"#182230",
|
||||
"#b9b9ba",
|
||||
"#d8a070",
|
||||
"#d31014",
|
||||
"#0fa00f",
|
||||
"#0095ff",
|
||||
"#ffa500"
|
||||
],
|
||||
"pleroma-light": [
|
||||
"Pleroma Light",
|
||||
"#f2f4f6",
|
||||
"#dbe0e8",
|
||||
"#304055",
|
||||
"#f86f0f",
|
||||
"#d31014",
|
||||
"#0fa00f",
|
||||
"#0095ff",
|
||||
"#ffa500"
|
||||
],
|
||||
"classic-dark": {
|
||||
"name": "Classic Dark",
|
||||
"bg": "#161c20",
|
||||
|
|
@ -12,8 +32,28 @@
|
|||
"cBlue": "#0095ff",
|
||||
"cOrange": "#ffa500"
|
||||
},
|
||||
"bird": [ "Bird", "#f8fafd", "#e6ecf0", "#14171a", "#0084b8", "#e0245e", "#17bf63", "#1b95e0", "#fab81e"],
|
||||
"pleroma-amoled": [ "Pleroma Dark AMOLED", "#000000", "#111111", "#b0b0b1", "#d8a070", "#aa0000", "#0fa00f", "#0095ff", "#d59500"],
|
||||
"bird": [
|
||||
"Bird",
|
||||
"#f8fafd",
|
||||
"#e6ecf0",
|
||||
"#14171a",
|
||||
"#0084b8",
|
||||
"#e0245e",
|
||||
"#17bf63",
|
||||
"#1b95e0",
|
||||
"#fab81e"
|
||||
],
|
||||
"pleroma-amoled": [
|
||||
"Pleroma Dark AMOLED",
|
||||
"#000000",
|
||||
"#111111",
|
||||
"#b0b0b1",
|
||||
"#d8a070",
|
||||
"#aa0000",
|
||||
"#0fa00f",
|
||||
"#0095ff",
|
||||
"#d59500"
|
||||
],
|
||||
"tomorrow-night": {
|
||||
"name": "Tomorrow Night",
|
||||
"bg": "#1d1f21",
|
||||
|
|
@ -36,8 +76,28 @@
|
|||
"cGreen": "#50FA7B",
|
||||
"cOrange": "#FFB86C"
|
||||
},
|
||||
"ir-black": [ "Ir Black", "#000000", "#242422", "#b5b3aa", "#ff6c60", "#FF6C60", "#A8FF60", "#96CBFE", "#FFFFB6" ],
|
||||
"monokai": [ "Monokai", "#272822", "#383830", "#f8f8f2", "#f92672", "#F92672", "#a6e22e", "#66d9ef", "#f4bf75" ],
|
||||
"ir-black": [
|
||||
"Ir Black",
|
||||
"#000000",
|
||||
"#242422",
|
||||
"#b5b3aa",
|
||||
"#ff6c60",
|
||||
"#FF6C60",
|
||||
"#A8FF60",
|
||||
"#96CBFE",
|
||||
"#FFFFB6"
|
||||
],
|
||||
"monokai": [
|
||||
"Monokai",
|
||||
"#272822",
|
||||
"#383830",
|
||||
"#f8f8f2",
|
||||
"#f92672",
|
||||
"#F92672",
|
||||
"#a6e22e",
|
||||
"#66d9ef",
|
||||
"#f4bf75"
|
||||
],
|
||||
"purple-stream": {
|
||||
"name": "Purple stream",
|
||||
"bg": "#17171A",
|
||||
|
|
|
|||
|
|
@ -5,15 +5,14 @@ body {
|
|||
|
||||
#splash {
|
||||
--scale: 1;
|
||||
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: grid;
|
||||
grid-template-rows: auto;
|
||||
grid-template-columns: auto;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
justify-items: center;
|
||||
place-items: center;
|
||||
flex-direction: column;
|
||||
background: #0f161e;
|
||||
font-family: sans-serif;
|
||||
|
|
@ -21,13 +20,20 @@ body {
|
|||
position: absolute;
|
||||
z-index: 9999;
|
||||
font-size: calc(1vw + 1vh + 1vmin);
|
||||
opacity: 1;
|
||||
transition: opacity 500ms ease-out 2s;
|
||||
}
|
||||
|
||||
#splash.hidden,
|
||||
#splash.initial-hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#splash-credit {
|
||||
position: absolute;
|
||||
font-size: 14px;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
font-size: 1em;
|
||||
bottom: 1em;
|
||||
right: 1em;
|
||||
}
|
||||
|
||||
#splash-container {
|
||||
|
|
@ -59,16 +65,17 @@ body {
|
|||
z-index: 2;
|
||||
grid-template-rows: repeat(8, 1fr);
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-areas: "P P . L L"
|
||||
"P P . L L"
|
||||
"P P . L L"
|
||||
"P P . L L"
|
||||
"P P . . ."
|
||||
"P P . . ."
|
||||
"P P . E E"
|
||||
"P P . E E";
|
||||
grid-template-areas:
|
||||
"P P . L L"
|
||||
"P P . L L"
|
||||
"P P . L L"
|
||||
"P P . L L"
|
||||
"P P . . ."
|
||||
"P P . . ."
|
||||
"P P . E E"
|
||||
"P P . E E";
|
||||
|
||||
--logoChunkSize: calc(2em * 0.5 * var(--scale))
|
||||
--logoChunkSize: calc(2em * 0.5 * var(--scale));
|
||||
}
|
||||
|
||||
.chunk {
|
||||
|
|
@ -78,7 +85,7 @@ body {
|
|||
|
||||
#chunk-P {
|
||||
grid-area: P;
|
||||
border-top-left-radius: calc(var(--logoChunkSize) / 2);
|
||||
border-top-left-radius: calc(var(--logoChunkSize) / 2);
|
||||
}
|
||||
|
||||
#chunk-L {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base"
|
||||
]
|
||||
"extends": ["config:base"]
|
||||
}
|
||||
|
|
|
|||
252
src/App.js
252
src/App.js
|
|
@ -1,34 +1,48 @@
|
|||
import UserPanel from './components/user_panel/user_panel.vue'
|
||||
import NavPanel from './components/nav_panel/nav_panel.vue'
|
||||
import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_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 ShoutPanel from './components/shout_panel/shout_panel.vue'
|
||||
import MediaModal from './components/media_modal/media_modal.vue'
|
||||
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.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 EditStatusModal from './components/edit_status_modal/edit_status_modal.vue'
|
||||
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
|
||||
import StatusHistoryModal from './components/status_history_modal/status_history_modal.vue'
|
||||
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
|
||||
import { getOrCreateServiceWorker } from './services/sw/sw'
|
||||
import { windowWidth, windowHeight } from './services/window_utils/window_utils'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { useShoutStore } from './stores/shout'
|
||||
import { useInterfaceStore } from './stores/interface'
|
||||
|
||||
import { throttle } from 'lodash'
|
||||
import { mapState } from 'pinia'
|
||||
import { defineAsyncComponent, toValue } from 'vue'
|
||||
|
||||
import DesktopNav from './components/desktop_nav/desktop_nav.vue'
|
||||
import EditStatusModal from './components/edit_status_modal/edit_status_modal.vue'
|
||||
import FeaturesPanel from './components/features_panel/features_panel.vue'
|
||||
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
|
||||
import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
|
||||
import MediaModal from './components/media_modal/media_modal.vue'
|
||||
import MobileNav from './components/mobile_nav/mobile_nav.vue'
|
||||
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
|
||||
import NavPanel from './components/nav_panel/nav_panel.vue'
|
||||
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
|
||||
import ShoutPanel from './components/shout_panel/shout_panel.vue'
|
||||
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||
import StatusHistoryModal from './components/status_history_modal/status_history_modal.vue'
|
||||
import UserPanel from './components/user_panel/user_panel.vue'
|
||||
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
|
||||
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
||||
import { getOrCreateServiceWorker } from './services/sw/sw'
|
||||
import { windowHeight, windowWidth } from './services/window_utils/window_utils'
|
||||
|
||||
import { useEmojiStore } from 'src/stores/emoji.js'
|
||||
import { useI18nStore } from 'src/stores/i18n.js'
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
import { useShoutStore } from 'src/stores/shout.js'
|
||||
|
||||
import messages from 'src/i18n/messages'
|
||||
import localeService from 'src/services/locale/locale.service.js'
|
||||
|
||||
// Helper to unwrap reactive proxies
|
||||
window.toValue = toValue
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
components: {
|
||||
UserPanel,
|
||||
NavPanel,
|
||||
Notifications: defineAsyncComponent(() => import('./components/notifications/notifications.vue')),
|
||||
Notifications: defineAsyncComponent(
|
||||
() => import('./components/notifications/notifications.vue'),
|
||||
),
|
||||
InstanceSpecificPanel,
|
||||
FeaturesPanel,
|
||||
WhoToFollowPanel,
|
||||
|
|
@ -38,150 +52,193 @@ export default {
|
|||
MobilePostStatusButton,
|
||||
MobileNav,
|
||||
DesktopNav,
|
||||
SettingsModal: defineAsyncComponent(() => import('./components/settings_modal/settings_modal.vue')),
|
||||
UpdateNotification: defineAsyncComponent(() => import('./components/update_notification/update_notification.vue')),
|
||||
SettingsModal: defineAsyncComponent(
|
||||
() => import('./components/settings_modal/settings_modal.vue'),
|
||||
),
|
||||
UpdateNotification: defineAsyncComponent(
|
||||
() => import('./components/update_notification/update_notification.vue'),
|
||||
),
|
||||
UserReportingModal,
|
||||
PostStatusModal,
|
||||
EditStatusModal,
|
||||
StatusHistoryModal,
|
||||
GlobalNoticeList
|
||||
GlobalNoticeList,
|
||||
},
|
||||
data: () => ({
|
||||
mobileActivePanel: 'timeline'
|
||||
mobileActivePanel: 'timeline',
|
||||
}),
|
||||
watch: {
|
||||
themeApplied () {
|
||||
themeApplied() {
|
||||
this.removeSplash()
|
||||
},
|
||||
currentTheme () {
|
||||
currentTheme() {
|
||||
this.setThemeBodyClass()
|
||||
},
|
||||
layoutType () {
|
||||
layoutType() {
|
||||
document.getElementById('modal').classList = ['-' + this.layoutType]
|
||||
}
|
||||
},
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
// Load the locale from the storage
|
||||
const val = this.$store.getters.mergedConfig.interfaceLanguage
|
||||
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
||||
const value = useMergedConfigStore().mergedConfig.interfaceLanguage
|
||||
useI18nStore().setLanguage(value)
|
||||
useEmojiStore().loadUnicodeEmojiData(value)
|
||||
|
||||
document.getElementById('modal').classList = ['-' + this.layoutType]
|
||||
|
||||
// Create bound handlers
|
||||
this.updateScrollState = throttle(this.scrollHandler, 200)
|
||||
this.updateMobileState = throttle(this.resizeHandler, 200)
|
||||
},
|
||||
mounted () {
|
||||
mounted() {
|
||||
window.addEventListener('resize', this.updateMobileState)
|
||||
this.scrollParent.addEventListener('scroll', this.updateScrollState)
|
||||
|
||||
if (useInterfaceStore().themeApplied) {
|
||||
if (this.themeApplied) {
|
||||
this.setThemeBodyClass()
|
||||
this.removeSplash()
|
||||
}
|
||||
getOrCreateServiceWorker()
|
||||
},
|
||||
unmounted () {
|
||||
unmounted() {
|
||||
window.removeEventListener('resize', this.updateMobileState)
|
||||
this.scrollParent.removeEventListener('scroll', this.updateScrollState)
|
||||
},
|
||||
computed: {
|
||||
themeApplied () {
|
||||
return useInterfaceStore().themeApplied
|
||||
},
|
||||
currentTheme () {
|
||||
if (useInterfaceStore().styleDataUsed) {
|
||||
const styleMeta = useInterfaceStore().styleDataUsed.find(x => x.component === '@meta')
|
||||
currentTheme() {
|
||||
if (this.styleDataUsed) {
|
||||
const styleMeta = this.styleDataUsed.find(
|
||||
(x) => x.component === '@meta',
|
||||
)
|
||||
|
||||
if (styleMeta !== undefined) {
|
||||
return styleMeta.directives.name.replaceAll(" ", "-").toLowerCase()
|
||||
return styleMeta.directives.name.replaceAll(' ', '-').toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
return 'stock'
|
||||
},
|
||||
layoutModalClass () {
|
||||
layoutModalClass() {
|
||||
return '-' + this.layoutType
|
||||
},
|
||||
classes () {
|
||||
classes() {
|
||||
return [
|
||||
{
|
||||
'-reverse': this.reverseLayout,
|
||||
'-no-sticky-headers': this.noSticky,
|
||||
'-has-new-post-button': this.newPostButtonShown
|
||||
'-has-new-post-button': this.newPostButtonShown,
|
||||
},
|
||||
'-' + this.layoutType
|
||||
'-' + this.layoutType,
|
||||
]
|
||||
},
|
||||
navClasses () {
|
||||
const { navbarColumnStretch } = this.$store.getters.mergedConfig
|
||||
navClasses() {
|
||||
const { navbarColumnStretch } = useMergedConfigStore().mergedConfig
|
||||
return [
|
||||
'-' + this.layoutType,
|
||||
...(navbarColumnStretch ? ['-column-stretch'] : [])
|
||||
...(navbarColumnStretch ? ['-column-stretch'] : []),
|
||||
]
|
||||
},
|
||||
currentUser () { return this.$store.state.users.currentUser },
|
||||
userBackground () { return this.currentUser.background_image },
|
||||
instanceBackground () {
|
||||
return this.mergedConfig.hideInstanceWallpaper
|
||||
? null
|
||||
: this.$store.state.instance.background
|
||||
currentUser() {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
background () { return this.userBackground || this.instanceBackground },
|
||||
bgStyle () {
|
||||
userBackground() {
|
||||
return this.currentUser.background_image
|
||||
},
|
||||
instanceBackground() {
|
||||
return useMergedConfigStore().mergedConfig.hideInstanceWallpaper
|
||||
? null
|
||||
: this.instanceBackgroundUrl
|
||||
},
|
||||
background() {
|
||||
return this.userBackground || this.instanceBackground
|
||||
},
|
||||
bgStyle() {
|
||||
if (this.background) {
|
||||
return {
|
||||
'--body-background-image': `url(${this.background})`
|
||||
'--body-background-image': `url(${this.background})`,
|
||||
}
|
||||
}
|
||||
},
|
||||
shout () { return useShoutStore().joined },
|
||||
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
|
||||
showInstanceSpecificPanel () {
|
||||
return this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
!this.$store.getters.mergedConfig.hideISP &&
|
||||
this.$store.state.instance.instanceSpecificPanelContent
|
||||
shoutJoined() {
|
||||
return useShoutStore().joined
|
||||
},
|
||||
isChats () {
|
||||
isChats() {
|
||||
return this.$route.name === 'chat' || this.$route.name === 'chats'
|
||||
},
|
||||
isListEdit () {
|
||||
isListEdit() {
|
||||
return this.$route.name === 'lists-edit'
|
||||
},
|
||||
newPostButtonShown () {
|
||||
newPostButtonShown() {
|
||||
if (this.isChats) return false
|
||||
if (this.isListEdit) return false
|
||||
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
|
||||
return (
|
||||
useMergedConfigStore().mergedConfig.alwaysShowNewPostButton ||
|
||||
this.layoutType === 'mobile'
|
||||
)
|
||||
},
|
||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||
editingAvailable () { return this.$store.state.instance.editingAvailable },
|
||||
shoutboxPosition () {
|
||||
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || false
|
||||
shoutboxPosition() {
|
||||
return (
|
||||
useMergedConfigStore().mergedConfig.alwaysShowNewPostButton || false
|
||||
)
|
||||
},
|
||||
hideShoutbox () {
|
||||
return this.$store.getters.mergedConfig.hideShoutbox
|
||||
hideShoutbox() {
|
||||
return useMergedConfigStore().mergedConfig.hideShoutbox
|
||||
},
|
||||
layoutType () { return useInterfaceStore().layoutType },
|
||||
privateMode () { return this.$store.state.instance.private },
|
||||
reverseLayout () {
|
||||
const { thirdColumnMode, sidebarRight: reverseSetting } = this.$store.getters.mergedConfig
|
||||
reverseLayout() {
|
||||
const { thirdColumnMode, sidebarRight: reverseSetting } =
|
||||
useMergedConfigStore().mergedConfig
|
||||
if (this.layoutType !== 'wide') {
|
||||
return reverseSetting
|
||||
} else {
|
||||
return thirdColumnMode === 'notifications' ? reverseSetting : !reverseSetting
|
||||
return thirdColumnMode === 'notifications'
|
||||
? reverseSetting
|
||||
: !reverseSetting
|
||||
}
|
||||
},
|
||||
noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders },
|
||||
showScrollbars () { return this.$store.getters.mergedConfig.showScrollbars },
|
||||
scrollParent () { return window; /* this.$refs.appContentRef */ },
|
||||
...mapGetters(['mergedConfig'])
|
||||
noSticky() {
|
||||
return useMergedConfigStore().mergedConfig.disableStickyHeaders
|
||||
},
|
||||
showScrollbars() {
|
||||
return useMergedConfigStore().mergedConfig.showScrollbars
|
||||
},
|
||||
scrollParent() {
|
||||
return window /* this.$refs.appContentRef */
|
||||
},
|
||||
showInstanceSpecificPanel() {
|
||||
return (
|
||||
this.instanceSpecificPanelPresent &&
|
||||
!useMergedConfigStore().mergedConfig.hideISP
|
||||
)
|
||||
},
|
||||
...mapState(useMergedConfigStore, ['mergedConfig']),
|
||||
...mapState(useInterfaceStore, [
|
||||
'themeApplied',
|
||||
'styleDataUsed',
|
||||
'layoutType',
|
||||
]),
|
||||
...mapState(useInstanceStore, ['styleDataUsed']),
|
||||
...mapState(useInstanceCapabilitiesStore, [
|
||||
'suggestionsEnabled',
|
||||
'editingAvailable',
|
||||
]),
|
||||
...mapState(useInstanceStore, {
|
||||
instanceBackgroundUrl: (store) => store.instanceIdentity.background,
|
||||
showFeaturesPanel: (store) => store.instanceIdentity.showFeaturesPanel,
|
||||
instanceSpecificPanelPresent: (store) =>
|
||||
store.instanceIdentity.showInstanceSpecificPanel &&
|
||||
store.instanceIdentity.instanceSpecificPanelContent,
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
resizeHandler () {
|
||||
resizeHandler() {
|
||||
useInterfaceStore().setLayoutWidth(windowWidth())
|
||||
useInterfaceStore().setLayoutHeight(windowHeight())
|
||||
},
|
||||
scrollHandler () {
|
||||
const scrollPosition = this.scrollParent === window ? window.scrollY : this.scrollParent.scrollTop
|
||||
scrollHandler() {
|
||||
const scrollPosition =
|
||||
this.scrollParent === window
|
||||
? window.scrollY
|
||||
: this.scrollParent.scrollTop
|
||||
|
||||
if (scrollPosition != 0) {
|
||||
this.$refs.appContentRef.classList.add(['-scrolled'])
|
||||
|
|
@ -189,10 +246,10 @@ export default {
|
|||
this.$refs.appContentRef.classList.remove(['-scrolled'])
|
||||
}
|
||||
},
|
||||
setThemeBodyClass () {
|
||||
setThemeBodyClass() {
|
||||
const themeName = this.currentTheme
|
||||
const classList = Array.from(document.body.classList)
|
||||
const oldTheme = classList.filter(c => c.startsWith('theme-'))
|
||||
const oldTheme = classList.filter((c) => c.startsWith('theme-'))
|
||||
|
||||
if (themeName !== null && themeName !== '') {
|
||||
const newTheme = `theme-${themeName.toLowerCase()}`
|
||||
|
|
@ -208,14 +265,19 @@ export default {
|
|||
document.body.classList.remove(...oldTheme)
|
||||
}
|
||||
},
|
||||
removeSplash () {
|
||||
document.querySelector('#status').textContent = this.$t('splash.fun_' + Math.ceil(Math.random() * 4))
|
||||
removeSplash() {
|
||||
document.querySelector('#status').textContent = this.$t(
|
||||
'splash.fun_' + Math.ceil(Math.random() * 4),
|
||||
)
|
||||
const splashscreenRoot = document.querySelector('#splash')
|
||||
splashscreenRoot.addEventListener('transitionend', () => {
|
||||
splashscreenRoot.remove()
|
||||
})
|
||||
setTimeout(() => {
|
||||
splashscreenRoot.remove() // forcibly remove it, should fix my plasma browser widget t. HJ
|
||||
}, 600)
|
||||
splashscreenRoot.classList.add('hidden')
|
||||
document.querySelector('#app').classList.remove('hidden')
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
81
src/App.scss
81
src/App.scss
|
|
@ -3,7 +3,7 @@
|
|||
@use "panel";
|
||||
|
||||
@import '@fortawesome/fontawesome-svg-core/styles.css';
|
||||
@import '@floatingghost/pinch-zoom-element/dist/pinch-zoom.css';
|
||||
@import '@kazvmoe-infra/pinch-zoom-element/dist/pinch-zoom.css';
|
||||
|
||||
:root {
|
||||
--status-margin: 0.75em;
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
}
|
||||
|
||||
html {
|
||||
font-size: var(--textSize, 14px);
|
||||
font-size: var(--textSize, 1rem);
|
||||
|
||||
--navbar-height: var(--navbarSize, 3.5rem);
|
||||
--emoji-size: var(--emojiSize, 32px);
|
||||
|
|
@ -382,6 +382,10 @@ nav {
|
|||
font-family: sans-serif;
|
||||
font-family: var(--font);
|
||||
|
||||
&.-transparent {
|
||||
backdrop-filter: blur(0.125em) contrast(60%);
|
||||
}
|
||||
|
||||
&::-moz-focus-inner {
|
||||
border: none;
|
||||
}
|
||||
|
|
@ -509,6 +513,12 @@ nav {
|
|||
}
|
||||
}
|
||||
|
||||
label {
|
||||
&.-disabled {
|
||||
color: var(--textFaint);
|
||||
}
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
border: none;
|
||||
|
|
@ -525,6 +535,10 @@ textarea {
|
|||
height: unset;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--textFaint)
|
||||
}
|
||||
|
||||
--_padding: 0.5em;
|
||||
|
||||
border: none;
|
||||
|
|
@ -545,6 +559,10 @@ textarea {
|
|||
&[disabled="disabled"],
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
color: var(--textFaint);
|
||||
|
||||
/* stylelint-disable-next-line declaration-no-important */
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
&[type="range"] {
|
||||
|
|
@ -570,6 +588,8 @@ textarea {
|
|||
& + label::before {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
+ label::before {
|
||||
|
|
@ -669,7 +689,8 @@ option {
|
|||
list-style: none;
|
||||
display: grid;
|
||||
grid-auto-flow: row dense;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: repeat(auto-fit, minmax(20em, 1fr));
|
||||
grid-gap: 0.5em;
|
||||
|
||||
li {
|
||||
border: 1px solid var(--border);
|
||||
|
|
@ -679,11 +700,6 @@ option {
|
|||
}
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
|
|
@ -695,7 +711,6 @@ option {
|
|||
--_roundness-right: 0;
|
||||
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
> *:first-child,
|
||||
|
|
@ -742,17 +757,15 @@ option {
|
|||
}
|
||||
|
||||
&.-dot {
|
||||
min-height: 8px;
|
||||
max-height: 8px;
|
||||
min-width: 8px;
|
||||
max-width: 8px;
|
||||
padding: 0;
|
||||
min-height: 0.6em;
|
||||
max-height: 0.6em;
|
||||
min-width: 0.6em;
|
||||
max-width: 0.6em;
|
||||
left: calc(50% + 0.5em);
|
||||
top: calc(50% - 1em);
|
||||
line-height: 0;
|
||||
font-size: 0;
|
||||
left: calc(50% - 4px);
|
||||
top: calc(50% - 4px);
|
||||
margin-left: 6px;
|
||||
margin-top: -6px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.-counter {
|
||||
|
|
@ -783,21 +796,18 @@ option {
|
|||
color: var(--text);
|
||||
}
|
||||
|
||||
.visibility-notice {
|
||||
padding: 0.5em;
|
||||
border: 1px solid var(--textFaint);
|
||||
border-radius: var(--roundness);
|
||||
}
|
||||
|
||||
.notice-dismissible {
|
||||
padding-right: 4rem;
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 0.75em 1em;
|
||||
align-items: baseline;
|
||||
line-height: 1.5;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.dismiss {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 0.5em;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
|
@ -936,12 +946,7 @@ option {
|
|||
|
||||
#splash {
|
||||
pointer-events: none;
|
||||
transition: opacity 0.5s;
|
||||
opacity: 1;
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
// transition: opacity 0.5s;
|
||||
|
||||
#status {
|
||||
&.css-ok {
|
||||
|
|
@ -1080,7 +1085,7 @@ option {
|
|||
scale: 1.0063 0.9938;
|
||||
translate: 0 -10%;
|
||||
transform: rotateZ(var(--defaultZ));
|
||||
animation-timing-function: ease-in-ou;
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
90% {
|
||||
|
|
|
|||
|
|
@ -60,8 +60,8 @@
|
|||
/>
|
||||
</div>
|
||||
<MediaModal />
|
||||
<shout-panel
|
||||
v-if="currentUser && shout && !hideShoutbox"
|
||||
<ShoutPanel
|
||||
v-if="currentUser && !hideShoutbox && shoutJoined"
|
||||
:floating="true"
|
||||
class="floating-shout mobile-hidden"
|
||||
:class="{ '-left': shoutboxPosition }"
|
||||
|
|
|
|||
|
|
@ -1,29 +1,48 @@
|
|||
/* global process */
|
||||
|
||||
import vClickOutside from 'click-outside-vue3'
|
||||
import { createApp } from 'vue'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import vClickOutside from 'click-outside-vue3'
|
||||
import VueVirtualScroller from 'vue-virtual-scroller'
|
||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
|
||||
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
||||
import { config } from '@fortawesome/fontawesome-svg-core';
|
||||
import { config } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeLayers,
|
||||
} from '@fortawesome/vue-fontawesome'
|
||||
|
||||
config.autoAddCss = false
|
||||
|
||||
import App from '../App.vue'
|
||||
import routes from './routes'
|
||||
import VBodyScrollLock from 'src/directives/body_scroll_lock'
|
||||
|
||||
import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
|
||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||
import { applyConfig } from '../services/style_setter/style_setter.js'
|
||||
import FaviconService from '../services/favicon_service/favicon_service.js'
|
||||
import { applyStyleConfig } from '../services/style_setter/style_setter.js'
|
||||
import { initServiceWorker, updateFocus } from '../services/sw/sw.js'
|
||||
import {
|
||||
windowHeight,
|
||||
windowWidth,
|
||||
} from '../services/window_utils/window_utils'
|
||||
import routes from './routes'
|
||||
|
||||
import { useOAuthStore } from 'src/stores/oauth'
|
||||
import { useI18nStore } from 'src/stores/i18n'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow'
|
||||
import { useEmojiStore } from 'src/stores/emoji.js'
|
||||
import { useI18nStore } from 'src/stores/i18n'
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
import { useLocalConfigStore } from 'src/stores/local_config.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth'
|
||||
import { useSyncConfigStore } from 'src/stores/sync_config.js'
|
||||
import { useUserHighlightStore } from 'src/stores/user_highlight.js'
|
||||
|
||||
import VBodyScrollLock from 'src/directives/body_scroll_lock'
|
||||
import {
|
||||
INSTANCE_DEFAULT_CONFIG_DEFINITIONS,
|
||||
INSTANCE_IDENTITY_DEFAULT_DEFINITIONS,
|
||||
} from 'src/modules/default_config_state.js'
|
||||
|
||||
let staticInitialResults = null
|
||||
|
||||
|
|
@ -32,7 +51,9 @@ const parsedInitialResults = () => {
|
|||
return null
|
||||
}
|
||||
if (!staticInitialResults) {
|
||||
staticInitialResults = JSON.parse(document.getElementById('initial-results').textContent)
|
||||
staticInitialResults = JSON.parse(
|
||||
document.getElementById('initial-results').textContent,
|
||||
)
|
||||
}
|
||||
return staticInitialResults
|
||||
}
|
||||
|
|
@ -54,7 +75,7 @@ const preloadFetch = async (request) => {
|
|||
return {
|
||||
ok: true,
|
||||
json: () => requestData,
|
||||
text: () => requestData
|
||||
text: () => requestData,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -63,20 +84,38 @@ const getInstanceConfig = async ({ store }) => {
|
|||
const res = await preloadFetch('/api/v1/instance')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
const textlimit = data.max_toot_chars
|
||||
const textLimit = data.max_toot_chars
|
||||
const vapidPublicKey = data.pleroma.vapid_public_key
|
||||
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaExtensionsAvailable', value: data.pleroma })
|
||||
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
|
||||
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required })
|
||||
store.dispatch('setInstanceOption', { name: 'birthdayRequired', value: !!data.pleroma?.metadata.birthday_required })
|
||||
store.dispatch('setInstanceOption', { name: 'birthdayMinAge', value: data.pleroma?.metadata.birthday_min_age || 0 })
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'pleromaExtensionsAvailable',
|
||||
data.pleroma,
|
||||
)
|
||||
useInstanceStore().set({
|
||||
path: 'limits.textLimit',
|
||||
value: textLimit,
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'accountApprovalRequired',
|
||||
value: data.approval_required,
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'birthdayRequired',
|
||||
value: !!data.pleroma?.metadata.birthday_required,
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'birthdayMinAge',
|
||||
value: data.pleroma?.metadata.birthday_min_age || 0,
|
||||
})
|
||||
|
||||
if (vapidPublicKey) {
|
||||
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
||||
useInstanceStore().set({
|
||||
path: 'vapidPublicKey',
|
||||
value: vapidPublicKey,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
throw (res)
|
||||
throw res
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Could not load instance config, potentially fatal')
|
||||
|
|
@ -93,10 +132,12 @@ const getBackendProvidedConfig = async () => {
|
|||
const data = await res.json()
|
||||
return data.pleroma_fe
|
||||
} else {
|
||||
throw (res)
|
||||
throw res
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Could not load backend-provided frontend config, potentially fatal')
|
||||
console.error(
|
||||
'Could not load backend-provided frontend config, potentially fatal',
|
||||
)
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
|
@ -107,7 +148,7 @@ const getStaticConfig = async () => {
|
|||
if (res.ok) {
|
||||
return res.json()
|
||||
} else {
|
||||
throw (res)
|
||||
throw res
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load static/config.json, continuing without it.')
|
||||
|
|
@ -129,51 +170,21 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||
config = Object.assign({}, staticConfig, apiConfig)
|
||||
}
|
||||
|
||||
const copyInstanceOption = (name) => {
|
||||
store.dispatch('setInstanceOption', { name, value: config[name] })
|
||||
}
|
||||
Object.keys(INSTANCE_IDENTITY_DEFAULT_DEFINITIONS).forEach((source) =>
|
||||
useInstanceStore().set({
|
||||
value: config[source],
|
||||
path: `instanceIdentity.${source}`,
|
||||
}),
|
||||
)
|
||||
|
||||
copyInstanceOption('theme')
|
||||
copyInstanceOption('style')
|
||||
copyInstanceOption('palette')
|
||||
copyInstanceOption('embeddedToS')
|
||||
copyInstanceOption('nsfwCensorImage')
|
||||
copyInstanceOption('background')
|
||||
copyInstanceOption('hidePostStats')
|
||||
copyInstanceOption('hideBotIndication')
|
||||
copyInstanceOption('hideUserStats')
|
||||
copyInstanceOption('hideFilteredStatuses')
|
||||
copyInstanceOption('logo')
|
||||
Object.keys(INSTANCE_DEFAULT_CONFIG_DEFINITIONS).forEach((source) =>
|
||||
useInstanceStore().set({
|
||||
value: config[source],
|
||||
path: `prefsStorage.${source}`,
|
||||
}),
|
||||
)
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'logoMask',
|
||||
value: typeof config.logoMask === 'undefined'
|
||||
? true
|
||||
: config.logoMask
|
||||
})
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'logoMargin',
|
||||
value: typeof config.logoMargin === 'undefined'
|
||||
? 0
|
||||
: config.logoMargin
|
||||
})
|
||||
copyInstanceOption('logoLeft')
|
||||
useAuthFlowStore().setInitialStrategy(config.loginMethod)
|
||||
|
||||
copyInstanceOption('redirectRootNoLogin')
|
||||
copyInstanceOption('redirectRootLogin')
|
||||
copyInstanceOption('showInstanceSpecificPanel')
|
||||
copyInstanceOption('minimalScopesMode')
|
||||
copyInstanceOption('hideMutedPosts')
|
||||
copyInstanceOption('collapseMessageWithSubject')
|
||||
copyInstanceOption('scopeCopy')
|
||||
copyInstanceOption('subjectLineBehavior')
|
||||
copyInstanceOption('postContentType')
|
||||
copyInstanceOption('alwaysShowSubjectInput')
|
||||
copyInstanceOption('showFeaturesPanel')
|
||||
copyInstanceOption('hideSitename')
|
||||
copyInstanceOption('sidebarRight')
|
||||
}
|
||||
|
||||
const getTOS = async ({ store }) => {
|
||||
|
|
@ -181,9 +192,9 @@ const getTOS = async ({ store }) => {
|
|||
const res = await window.fetch('/static/terms-of-service.html')
|
||||
if (res.ok) {
|
||||
const html = await res.text()
|
||||
store.dispatch('setInstanceOption', { name: 'tos', value: html })
|
||||
useInstanceStore().set({ path: 'instanceIdentity.tos', value: html })
|
||||
} else {
|
||||
throw (res)
|
||||
throw res
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Can't load TOS\n", e)
|
||||
|
|
@ -195,9 +206,12 @@ const getInstancePanel = async ({ store }) => {
|
|||
const res = await preloadFetch('/instance/panel.html')
|
||||
if (res.ok) {
|
||||
const html = await res.text()
|
||||
store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
|
||||
useInstanceStore().set({
|
||||
path: 'instanceIdentity.instanceSpecificPanelContent',
|
||||
value: html,
|
||||
})
|
||||
} else {
|
||||
throw (res)
|
||||
throw res
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Can't load instance panel\n", e)
|
||||
|
|
@ -209,25 +223,27 @@ const getStickers = async ({ store }) => {
|
|||
const res = await window.fetch('/static/stickers.json')
|
||||
if (res.ok) {
|
||||
const values = await res.json()
|
||||
const stickers = (await Promise.all(
|
||||
Object.entries(values).map(async ([name, path]) => {
|
||||
const resPack = await window.fetch(path + 'pack.json')
|
||||
let meta = {}
|
||||
if (resPack.ok) {
|
||||
meta = await resPack.json()
|
||||
}
|
||||
return {
|
||||
pack: name,
|
||||
path,
|
||||
meta
|
||||
}
|
||||
})
|
||||
)).sort((a, b) => {
|
||||
const stickers = (
|
||||
await Promise.all(
|
||||
Object.entries(values).map(async ([name, path]) => {
|
||||
const resPack = await window.fetch(path + 'pack.json')
|
||||
let meta = {}
|
||||
if (resPack.ok) {
|
||||
meta = await resPack.json()
|
||||
}
|
||||
return {
|
||||
pack: name,
|
||||
path,
|
||||
meta,
|
||||
}
|
||||
}),
|
||||
)
|
||||
).sort((a, b) => {
|
||||
return a.meta.title.localeCompare(b.meta.title)
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'stickers', value: stickers })
|
||||
useEmojiStore().setStickers(stickers)
|
||||
} else {
|
||||
throw (res)
|
||||
throw res
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Can't load stickers\n", e)
|
||||
|
|
@ -237,13 +253,19 @@ const getStickers = async ({ store }) => {
|
|||
const getAppSecret = async ({ store }) => {
|
||||
const oauth = useOAuthStore()
|
||||
if (oauth.userToken) {
|
||||
store.commit('setBackendInteractor', backendInteractorService(oauth.getToken))
|
||||
store.commit(
|
||||
'setBackendInteractor',
|
||||
backendInteractorService(oauth.getToken),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const resolveStaffAccounts = ({ store, accounts }) => {
|
||||
const nicknames = accounts.map(uri => uri.split('/').pop())
|
||||
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
|
||||
const nicknames = accounts.map((uri) => uri.split('/').pop())
|
||||
useInstanceStore().set({
|
||||
path: 'staffAccounts',
|
||||
value: nicknames,
|
||||
})
|
||||
}
|
||||
|
||||
const getNodeInfo = async ({ store }) => {
|
||||
|
|
@ -254,76 +276,165 @@ const getNodeInfo = async ({ store }) => {
|
|||
const data = await res.json()
|
||||
const metadata = data.metadata
|
||||
const features = metadata.features
|
||||
store.dispatch('setInstanceOption', { name: 'name', value: metadata.nodeName })
|
||||
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations })
|
||||
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: 'shoutAvailable', value: features.includes('chat') })
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'pleromaCustomEmojiReactionsAvailable',
|
||||
value:
|
||||
features.includes('pleroma_custom_emoji_reactions') ||
|
||||
features.includes('custom_emoji_reactions')
|
||||
useInstanceStore().set({
|
||||
path: 'name',
|
||||
value: metadata.nodeName,
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaBookmarkFoldersAvailable', value: features.includes('pleroma:bookmark_folders') })
|
||||
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
||||
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
||||
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
|
||||
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
|
||||
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
|
||||
store.dispatch('setInstanceOption', { name: 'quotingAvailable', value: features.includes('quote_posting') })
|
||||
store.dispatch('setInstanceOption', { name: 'groupActorAvailable', value: features.includes('pleroma:group_actors') })
|
||||
store.dispatch('setInstanceOption', { name: 'localBubbleInstances', value: metadata.localBubbleInstances ?? [] })
|
||||
useInstanceStore().set({
|
||||
path: 'registrationOpen',
|
||||
value: data.openRegistrations,
|
||||
})
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'mediaProxyAvailable',
|
||||
features.includes('media_proxy'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'safeDM',
|
||||
features.includes('safe_dm_mentions'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'shoutAvailable',
|
||||
features.includes('chat'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'pleromaChatMessagesAvailable',
|
||||
features.includes('pleroma_chat_messages'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'pleromaCustomEmojiReactionsAvailable',
|
||||
|
||||
features.includes('pleroma_custom_emoji_reactions') ||
|
||||
features.includes('custom_emoji_reactions'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'pleromaBookmarkFoldersAvailable',
|
||||
features.includes('pleroma:bookmark_folders'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'gopherAvailable',
|
||||
features.includes('gopher'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'pollsAvailable',
|
||||
features.includes('polls'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'editingAvailable',
|
||||
features.includes('editing'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'mailerEnabled',
|
||||
metadata.mailerEnabled,
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'quotingAvailable',
|
||||
features.includes('quote_posting'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'groupActorAvailable',
|
||||
features.includes('pleroma:group_actors'),
|
||||
)
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'blockExpiration',
|
||||
features.includes('pleroma:block_expiration'),
|
||||
)
|
||||
useInstanceStore().set({
|
||||
path: 'localBubbleInstances',
|
||||
value: metadata.localBubbleInstances ?? [],
|
||||
})
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'localBubble',
|
||||
(metadata.localBubbleInstances ?? []).length > 0,
|
||||
)
|
||||
|
||||
useInstanceStore().set({
|
||||
path: 'limits.pollLimits',
|
||||
value: metadata.pollLimits,
|
||||
})
|
||||
const uploadLimits = metadata.uploadLimits
|
||||
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
|
||||
store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadLimits.avatar) })
|
||||
store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadLimits.background) })
|
||||
store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadLimits.banner) })
|
||||
store.dispatch('setInstanceOption', { name: 'fieldsLimits', value: metadata.fieldsLimits })
|
||||
useInstanceStore().set({
|
||||
path: 'limits.uploadlimit',
|
||||
value: parseInt(uploadLimits.general),
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'limits.avatarlimit',
|
||||
value: parseInt(uploadLimits.avatar),
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'limits.backgroundlimit',
|
||||
value: parseInt(uploadLimits.background),
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'limits.bannerlimit',
|
||||
value: parseInt(uploadLimits.banner),
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'limits.fieldsLimits',
|
||||
value: metadata.fieldsLimits,
|
||||
})
|
||||
|
||||
store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames })
|
||||
store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })
|
||||
useInstanceStore().set({
|
||||
path: 'restrictedNicknames',
|
||||
value: metadata.restrictedNicknames,
|
||||
})
|
||||
useInstanceCapabilitiesStore().set('postFormats', metadata.postFormats)
|
||||
|
||||
const suggestions = metadata.suggestions
|
||||
store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled })
|
||||
store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web })
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'suggestionsEnabled',
|
||||
suggestions.enabled,
|
||||
)
|
||||
// this is unused, why?
|
||||
useInstanceCapabilitiesStore().set('suggestionsWeb', suggestions.web)
|
||||
|
||||
const software = data.software
|
||||
store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version })
|
||||
store.dispatch('setInstanceOption', { name: 'backendRepository', value: software.repository })
|
||||
useInstanceStore().set({
|
||||
path: 'backendVersion',
|
||||
value: software.version,
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'backendRepository',
|
||||
value: software.repository,
|
||||
})
|
||||
|
||||
const priv = metadata.private
|
||||
store.dispatch('setInstanceOption', { name: 'private', value: priv })
|
||||
useInstanceStore().set({ path: 'privateMode', value: priv })
|
||||
|
||||
const frontendVersion = window.___pleromafe_commit_hash
|
||||
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
|
||||
useInstanceStore().set({
|
||||
path: 'frontendVersion',
|
||||
value: frontendVersion,
|
||||
})
|
||||
|
||||
const federation = metadata.federation
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'tagPolicyAvailable',
|
||||
value: typeof federation.mrf_policies === 'undefined'
|
||||
useInstanceCapabilitiesStore().set(
|
||||
'tagPolicyAvailable',
|
||||
typeof federation.mrf_policies === 'undefined'
|
||||
? false
|
||||
: metadata.federation.mrf_policies.includes('TagPolicy')
|
||||
})
|
||||
: metadata.federation.mrf_policies.includes('TagPolicy'),
|
||||
)
|
||||
|
||||
store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation })
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'federating',
|
||||
value: typeof federation.enabled === 'undefined'
|
||||
? true
|
||||
: federation.enabled
|
||||
useInstanceStore().set({
|
||||
path: 'federationPolicy',
|
||||
value: federation,
|
||||
})
|
||||
useInstanceStore().set({
|
||||
path: 'federating',
|
||||
value:
|
||||
typeof federation.enabled === 'undefined' ? true : federation.enabled,
|
||||
})
|
||||
|
||||
const accountActivationRequired = metadata.accountActivationRequired
|
||||
store.dispatch('setInstanceOption', { name: 'accountActivationRequired', value: accountActivationRequired })
|
||||
useInstanceStore().set({
|
||||
path: 'accountActivationRequired',
|
||||
value: accountActivationRequired,
|
||||
})
|
||||
|
||||
const accounts = metadata.staffAccounts
|
||||
resolveStaffAccounts({ store, accounts })
|
||||
} else {
|
||||
throw (res)
|
||||
throw res
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not load nodeinfo')
|
||||
|
|
@ -333,7 +444,10 @@ const getNodeInfo = async ({ store }) => {
|
|||
|
||||
const setConfig = async ({ store }) => {
|
||||
// apiConfig, staticConfig
|
||||
const configInfos = await Promise.all([getBackendProvidedConfig({ store }), getStaticConfig()])
|
||||
const configInfos = await Promise.all([
|
||||
getBackendProvidedConfig({ store }),
|
||||
getStaticConfig(),
|
||||
])
|
||||
const apiConfig = configInfos[0]
|
||||
const staticConfig = configInfos[1]
|
||||
|
||||
|
|
@ -364,29 +478,37 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
|||
if (process.env.NODE_ENV === 'development') {
|
||||
// do some checks to avoid common errors
|
||||
if (!Object.keys(allStores).length) {
|
||||
throw new Error('No stores are available. Check the code in src/boot/after_store.js')
|
||||
throw new Error(
|
||||
'No stores are available. Check the code in src/boot/after_store.js',
|
||||
)
|
||||
}
|
||||
}
|
||||
await Promise.all(
|
||||
Object.entries(allStores)
|
||||
.map(async ([name, mod]) => {
|
||||
const isStoreName = name => name.startsWith('use')
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (Object.keys(mod).filter(isStoreName).length !== 1) {
|
||||
throw new Error('Each store file must export exactly one store as a named export. Check your code in src/stores/')
|
||||
}
|
||||
Object.entries(allStores).map(async ([name, mod]) => {
|
||||
const isStoreName = (name) => name.startsWith('use')
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (Object.keys(mod).filter(isStoreName).length !== 1) {
|
||||
throw new Error(
|
||||
'Each store file must export exactly one store as a named export. Check your code in src/stores/',
|
||||
)
|
||||
}
|
||||
const storeFuncName = Object.keys(mod).find(isStoreName)
|
||||
if (storeFuncName && typeof mod[storeFuncName] === 'function') {
|
||||
const p = mod[storeFuncName]().$persistLoaded
|
||||
if (!(p instanceof Promise)) {
|
||||
throw new Error(`${name} store's $persistLoaded is not a Promise. The persist plugin is not applied.`)
|
||||
}
|
||||
await p
|
||||
} else {
|
||||
throw new Error(`Store module ${name} does not export a 'use...' function`)
|
||||
}
|
||||
const storeFuncName = Object.keys(mod).find(isStoreName)
|
||||
if (storeFuncName && typeof mod[storeFuncName] === 'function') {
|
||||
const p = mod[storeFuncName]().$persistLoaded
|
||||
if (!(p instanceof Promise)) {
|
||||
throw new Error(
|
||||
`${name} store's $persistLoaded is not a Promise. The persist plugin is not applied.`,
|
||||
)
|
||||
}
|
||||
}))
|
||||
await p
|
||||
} else {
|
||||
throw new Error(
|
||||
`Store module ${name} does not export a 'use...' function`,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -397,11 +519,18 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
|||
}
|
||||
|
||||
if (storageError) {
|
||||
useInterfaceStore().pushGlobalNotice({ messageKey: 'errors.storage_unavailable', level: 'error' })
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
messageKey: 'errors.storage_unavailable',
|
||||
level: 'error',
|
||||
})
|
||||
}
|
||||
|
||||
useInterfaceStore().setLayoutWidth(windowWidth())
|
||||
useInterfaceStore().setLayoutHeight(windowHeight())
|
||||
window.syncConfig = useSyncConfigStore()
|
||||
window.mergedConfig = useMergedConfigStore()
|
||||
window.localConfig = useLocalConfigStore()
|
||||
window.highlightConfig = useUserHighlightStore()
|
||||
|
||||
FaviconService.initFaviconService()
|
||||
initServiceWorker(store)
|
||||
|
|
@ -409,18 +538,25 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
|||
window.addEventListener('focus', () => updateFocus())
|
||||
|
||||
const overrides = window.___pleromafe_dev_overrides || {}
|
||||
const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin
|
||||
store.dispatch('setInstanceOption', { name: 'server', value: server })
|
||||
const server =
|
||||
typeof overrides.target !== 'undefined'
|
||||
? overrides.target
|
||||
: window.location.origin
|
||||
useInstanceStore().set({ path: 'server', value: server })
|
||||
|
||||
await setConfig({ store })
|
||||
try {
|
||||
await useInterfaceStore().applyTheme().catch((e) => { console.error('Error setting theme', e) })
|
||||
await useInterfaceStore()
|
||||
.applyTheme()
|
||||
.catch((e) => {
|
||||
console.error('Error setting theme', e)
|
||||
})
|
||||
} catch (e) {
|
||||
window.splashError(e)
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
applyConfig(store.state.config, i18n.global)
|
||||
applyStyleConfig(useMergedConfigStore().mergedConfig, i18n.global)
|
||||
|
||||
// Now we can try getting the server settings and logging in
|
||||
// Most of these are preloaded into the index.html so blocking is minimized
|
||||
|
|
@ -428,8 +564,8 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
|||
checkOAuthToken({ store }),
|
||||
getInstancePanel({ store }),
|
||||
getNodeInfo({ store }),
|
||||
getInstanceConfig({ store })
|
||||
]).catch(e => Promise.reject(e))
|
||||
getInstanceConfig({ store }),
|
||||
]).catch((e) => Promise.reject(e))
|
||||
|
||||
// Start fetching things that don't need to block the UI
|
||||
store.dispatch('fetchMutes')
|
||||
|
|
@ -442,11 +578,11 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
|
|||
history: createWebHistory(),
|
||||
routes: routes(store),
|
||||
scrollBehavior: (to, _from, savedPosition) => {
|
||||
if (to.matched.some(m => m.meta.dontScroll)) {
|
||||
if (to.matched.some((m) => m.meta.dontScroll)) {
|
||||
return false
|
||||
}
|
||||
return savedPosition || { left: 0, top: 0 }
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
useI18nStore().setI18n(i18n)
|
||||
|
|
|
|||
|
|
@ -1,42 +1,48 @@
|
|||
import PublicTimeline from 'components/public_timeline/public_timeline.vue'
|
||||
import BubbleTimeline from 'components/bubble_timeline/bubble_timeline.vue'
|
||||
import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue'
|
||||
import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
|
||||
import TagTimeline from 'components/tag_timeline/tag_timeline.vue'
|
||||
import BookmarkTimeline from 'components/bookmark_timeline/bookmark_timeline.vue'
|
||||
import ConversationPage from 'components/conversation-page/conversation-page.vue'
|
||||
import Interactions from 'components/interactions/interactions.vue'
|
||||
import DMs from 'components/dm_timeline/dm_timeline.vue'
|
||||
import ChatList from 'components/chat_list/chat_list.vue'
|
||||
import Chat from 'components/chat/chat.vue'
|
||||
import UserProfile from 'components/user_profile/user_profile.vue'
|
||||
import Search from 'components/search/search.vue'
|
||||
import Registration from 'components/registration/registration.vue'
|
||||
import PasswordReset from 'components/password_reset/password_reset.vue'
|
||||
import FollowRequests from 'components/follow_requests/follow_requests.vue'
|
||||
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
|
||||
import Notifications from 'components/notifications/notifications.vue'
|
||||
import AuthForm from 'components/auth_form/auth_form.js'
|
||||
import ShoutPanel from 'components/shout_panel/shout_panel.vue'
|
||||
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
|
||||
import About from 'components/about/about.vue'
|
||||
import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue'
|
||||
import Lists from 'components/lists/lists.vue'
|
||||
import ListsTimeline from 'components/lists_timeline/lists_timeline.vue'
|
||||
import ListsEdit from 'components/lists_edit/lists_edit.vue'
|
||||
import NavPanel from 'src/components/nav_panel/nav_panel.vue'
|
||||
import AnnouncementsPage from 'components/announcements_page/announcements_page.vue'
|
||||
import QuotesTimeline from '../components/quotes_timeline/quotes_timeline.vue'
|
||||
import AuthForm from 'components/auth_form/auth_form.js'
|
||||
import BookmarkTimeline from 'components/bookmark_timeline/bookmark_timeline.vue'
|
||||
import BubbleTimeline from 'components/bubble_timeline/bubble_timeline.vue'
|
||||
import Chat from 'components/chat/chat.vue'
|
||||
import ChatList from 'components/chat_list/chat_list.vue'
|
||||
import ConversationPage from 'components/conversation-page/conversation-page.vue'
|
||||
import DMs from 'components/dm_timeline/dm_timeline.vue'
|
||||
import Drafts from 'components/drafts/drafts.vue'
|
||||
import BookmarkFolders from '../components/bookmark_folders/bookmark_folders.vue'
|
||||
import FollowRequests from 'components/follow_requests/follow_requests.vue'
|
||||
import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
|
||||
import Interactions from 'components/interactions/interactions.vue'
|
||||
import Lists from 'components/lists/lists.vue'
|
||||
import ListsEdit from 'components/lists_edit/lists_edit.vue'
|
||||
import ListsTimeline from 'components/lists_timeline/lists_timeline.vue'
|
||||
import Notifications from 'components/notifications/notifications.vue'
|
||||
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
|
||||
import PasswordReset from 'components/password_reset/password_reset.vue'
|
||||
import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue'
|
||||
import PublicTimeline from 'components/public_timeline/public_timeline.vue'
|
||||
import Registration from 'components/registration/registration.vue'
|
||||
import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue'
|
||||
import Search from 'components/search/search.vue'
|
||||
import ShoutPanel from 'components/shout_panel/shout_panel.vue'
|
||||
import TagTimeline from 'components/tag_timeline/tag_timeline.vue'
|
||||
import UserProfile from 'components/user_profile/user_profile.vue'
|
||||
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
|
||||
|
||||
import NavPanel from 'src/components/nav_panel/nav_panel.vue'
|
||||
import BookmarkFolderEdit from '../components/bookmark_folder_edit/bookmark_folder_edit.vue'
|
||||
import BookmarkFolders from '../components/bookmark_folders/bookmark_folders.vue'
|
||||
import QuotesTimeline from '../components/quotes_timeline/quotes_timeline.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
|
||||
export default (store) => {
|
||||
const validateAuthenticatedRoute = (to, from, next) => {
|
||||
if (store.state.users.currentUser) {
|
||||
next()
|
||||
} else {
|
||||
next(store.state.instance.redirectRootNoLogin || '/main/all')
|
||||
next(
|
||||
useInstanceStore().instanceIdentity.redirectRootNoLogin || '/main/all',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -45,46 +51,125 @@ export default (store) => {
|
|||
name: 'root',
|
||||
path: '/',
|
||||
redirect: () => {
|
||||
return (store.state.users.currentUser
|
||||
? store.state.instance.redirectRootLogin
|
||||
: store.state.instance.redirectRootNoLogin) || '/main/all'
|
||||
}
|
||||
return (
|
||||
(store.state.users.currentUser
|
||||
? useInstanceStore().instanceIdentity.redirectRootLogin
|
||||
: useInstanceStore().instanceIdentity.redirectRootNoLogin) ||
|
||||
'/main/all'
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'public-external-timeline',
|
||||
path: '/main/all',
|
||||
component: PublicAndExternalTimeline,
|
||||
},
|
||||
{
|
||||
name: 'public-timeline',
|
||||
path: '/main/public',
|
||||
component: PublicTimeline,
|
||||
},
|
||||
{
|
||||
name: 'friends',
|
||||
path: '/main/friends',
|
||||
component: FriendsTimeline,
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{ name: 'public-external-timeline', path: '/main/all', component: PublicAndExternalTimeline },
|
||||
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline },
|
||||
{ name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
|
||||
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
|
||||
{ name: 'bubble', path: '/bubble', component: BubbleTimeline },
|
||||
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
||||
{
|
||||
name: 'conversation',
|
||||
path: '/notice/:id',
|
||||
component: ConversationPage,
|
||||
meta: { dontScroll: true },
|
||||
},
|
||||
{ name: 'quotes', path: '/notice/:id/quotes', component: QuotesTimeline },
|
||||
{
|
||||
name: 'remote-user-profile-acct',
|
||||
path: '/remote-users/:_(@)?:username([^/@]+)@:hostname([^/@]+)',
|
||||
component: RemoteUserResolver,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
name: 'remote-user-profile',
|
||||
path: '/remote-users/:hostname/:username',
|
||||
component: RemoteUserResolver,
|
||||
beforeEnter: validateAuthenticatedRoute
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
name: 'external-user-profile',
|
||||
path: '/users/$:id',
|
||||
component: UserProfile,
|
||||
},
|
||||
{
|
||||
name: 'interactions',
|
||||
path: '/users/:username/interactions',
|
||||
component: Interactions,
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
name: 'dms',
|
||||
path: '/users/:username/dms',
|
||||
component: DMs,
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{ name: 'external-user-profile', path: '/users/$:id', component: UserProfile },
|
||||
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'registration', path: '/registration', component: Registration },
|
||||
{ name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
|
||||
{ name: 'registration-token', path: '/registration/:token', component: Registration },
|
||||
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'notifications', path: '/:username/notifications', component: Notifications, props: () => ({ disableTeleport: true }), beforeEnter: validateAuthenticatedRoute },
|
||||
{
|
||||
name: 'password-reset',
|
||||
path: '/password-reset',
|
||||
component: PasswordReset,
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
name: 'registration-token',
|
||||
path: '/registration/:token',
|
||||
component: Registration,
|
||||
},
|
||||
{
|
||||
name: 'friend-requests',
|
||||
path: '/friend-requests',
|
||||
component: FollowRequests,
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
name: 'notifications',
|
||||
path: '/:username/notifications',
|
||||
component: Notifications,
|
||||
props: () => ({ disableTeleport: true }),
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{ name: 'login', path: '/login', component: AuthForm },
|
||||
{ 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: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
|
||||
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
||||
{
|
||||
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: 'search',
|
||||
path: '/search',
|
||||
component: Search,
|
||||
props: (route) => ({ query: route.query.query }),
|
||||
},
|
||||
{
|
||||
name: 'who-to-follow',
|
||||
path: '/who-to-follow',
|
||||
component: WhoToFollow,
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{ name: 'about', path: '/about', component: About },
|
||||
{ name: 'announcements', path: '/announcements', component: AnnouncementsPage },
|
||||
{
|
||||
name: 'announcements',
|
||||
path: '/announcements',
|
||||
component: AnnouncementsPage,
|
||||
},
|
||||
{ name: 'drafts', path: '/drafts', component: Drafts },
|
||||
{ name: 'user-profile', path: '/users/:name', component: UserProfile },
|
||||
{ name: 'legacy-user-profile', path: '/:name', component: UserProfile },
|
||||
|
|
@ -92,17 +177,51 @@ export default (store) => {
|
|||
{ name: 'lists-timeline', path: '/lists/:id', component: ListsTimeline },
|
||||
{ name: 'lists-edit', path: '/lists/:id/edit', component: ListsEdit },
|
||||
{ name: 'lists-new', path: '/lists/new', component: ListsEdit },
|
||||
{ name: 'edit-navigation', path: '/nav-edit', component: NavPanel, props: () => ({ forceExpand: true, forceEditMode: true }), beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'bookmark-folders', path: '/bookmark_folders', component: BookmarkFolders },
|
||||
{ name: 'bookmark-folder-new', path: '/bookmarks/new-folder', component: BookmarkFolderEdit },
|
||||
{ name: 'bookmark-folder', path: '/bookmarks/:id', component: BookmarkTimeline },
|
||||
{ name: 'bookmark-folder-edit', path: '/bookmarks/:id/edit', component: BookmarkFolderEdit }
|
||||
{
|
||||
name: 'edit-navigation',
|
||||
path: '/nav-edit',
|
||||
component: NavPanel,
|
||||
props: () => ({ forceExpand: true, forceEditMode: true }),
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
name: 'bookmark-folders',
|
||||
path: '/bookmark_folders',
|
||||
component: BookmarkFolders,
|
||||
},
|
||||
{
|
||||
name: 'bookmark-folder-new',
|
||||
path: '/bookmarks/new-folder',
|
||||
component: BookmarkFolderEdit,
|
||||
},
|
||||
{
|
||||
name: 'bookmark-folder',
|
||||
path: '/bookmarks/:id',
|
||||
component: BookmarkTimeline,
|
||||
},
|
||||
{
|
||||
name: 'bookmark-folder-edit',
|
||||
path: '/bookmarks/:id/edit',
|
||||
component: BookmarkFolderEdit,
|
||||
},
|
||||
]
|
||||
|
||||
if (store.state.instance.pleromaChatMessagesAvailable) {
|
||||
if (useInstanceCapabilitiesStore().pleromaChatMessagesAvailable) {
|
||||
routes = routes.concat([
|
||||
{ name: 'chat', path: '/users/:username/chats/:recipient_id', component: Chat, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'chats', path: '/users/:username/chats', component: ChatList, meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute }
|
||||
{
|
||||
name: 'chat',
|
||||
path: '/users/:username/chats/:recipient_id',
|
||||
component: Chat,
|
||||
meta: { dontScroll: false },
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
{
|
||||
name: 'chats',
|
||||
path: '/users/:username/chats',
|
||||
component: ChatList,
|
||||
meta: { dontScroll: false },
|
||||
beforeEnter: validateAuthenticatedRoute,
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import InstanceSpecificPanel from '../instance_specific_panel/instance_specific_panel.vue'
|
||||
import FeaturesPanel from '../features_panel/features_panel.vue'
|
||||
import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue'
|
||||
import StaffPanel from '../staff_panel/staff_panel.vue'
|
||||
import InstanceSpecificPanel from '../instance_specific_panel/instance_specific_panel.vue'
|
||||
import MRFTransparencyPanel from '../mrf_transparency_panel/mrf_transparency_panel.vue'
|
||||
import StaffPanel from '../staff_panel/staff_panel.vue'
|
||||
import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
|
||||
const About = {
|
||||
components: {
|
||||
|
|
@ -10,16 +13,20 @@ const About = {
|
|||
FeaturesPanel,
|
||||
TermsOfServicePanel,
|
||||
StaffPanel,
|
||||
MRFTransparencyPanel
|
||||
MRFTransparencyPanel,
|
||||
},
|
||||
computed: {
|
||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||
showInstanceSpecificPanel () {
|
||||
return this.$store.state.instance.showInstanceSpecificPanel &&
|
||||
!this.$store.getters.mergedConfig.hideISP &&
|
||||
this.$store.state.instance.instanceSpecificPanelContent
|
||||
}
|
||||
}
|
||||
showFeaturesPanel() {
|
||||
return useInstanceStore().instanceIdentity.showFeaturesPanel
|
||||
},
|
||||
showInstanceSpecificPanel() {
|
||||
return (
|
||||
useInstanceStore().instanceIdentity.showInstanceSpecificPanel &&
|
||||
!useMergedConfigStore().mergedConfig.hideISP &&
|
||||
useInstanceStore().instanceIdentity.instanceSpecificPanelContent
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default About
|
||||
|
|
|
|||
|
|
@ -1,99 +1,105 @@
|
|||
import { mapState } from 'vuex'
|
||||
import ProgressButton from '../progress_button/progress_button.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
import { mapState } from 'pinia'
|
||||
|
||||
import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue'
|
||||
import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faEllipsisV
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import Popover from '../popover/popover.vue'
|
||||
import ProgressButton from '../progress_button/progress_button.vue'
|
||||
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
import { useReportsStore } from 'src/stores/reports'
|
||||
|
||||
library.add(
|
||||
faEllipsisV
|
||||
)
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faEllipsisV } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faEllipsisV)
|
||||
|
||||
const AccountActions = {
|
||||
props: [
|
||||
'user', 'relationship'
|
||||
],
|
||||
data () {
|
||||
props: ['user', 'relationship'],
|
||||
data() {
|
||||
return {
|
||||
showingConfirmBlock: false,
|
||||
showingConfirmRemoveFollower: false
|
||||
showingConfirmRemoveFollower: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ProgressButton,
|
||||
Popover,
|
||||
UserListMenu,
|
||||
ConfirmModal
|
||||
ConfirmModal,
|
||||
UserTimedFilterModal,
|
||||
},
|
||||
methods: {
|
||||
showConfirmBlock () {
|
||||
this.showingConfirmBlock = true
|
||||
},
|
||||
hideConfirmBlock () {
|
||||
this.showingConfirmBlock = false
|
||||
},
|
||||
showConfirmRemoveUserFromFollowers () {
|
||||
showConfirmRemoveUserFromFollowers() {
|
||||
this.showingConfirmRemoveFollower = true
|
||||
},
|
||||
hideConfirmRemoveUserFromFollowers () {
|
||||
hideConfirmRemoveUserFromFollowers() {
|
||||
this.showingConfirmRemoveFollower = false
|
||||
},
|
||||
showRepeats () {
|
||||
hideConfirmBlock() {
|
||||
this.showingConfirmBlock = false
|
||||
},
|
||||
showRepeats() {
|
||||
this.$store.dispatch('showReblogs', this.user.id)
|
||||
},
|
||||
hideRepeats () {
|
||||
hideRepeats() {
|
||||
this.$store.dispatch('hideReblogs', this.user.id)
|
||||
},
|
||||
blockUser () {
|
||||
if (!this.shouldConfirmBlock) {
|
||||
this.doBlockUser()
|
||||
blockUser() {
|
||||
if (this.$refs.timedBlockDialog) {
|
||||
this.$refs.timedBlockDialog.optionallyPrompt()
|
||||
} else {
|
||||
this.showConfirmBlock()
|
||||
if (!this.shouldConfirmBlock) {
|
||||
this.doBlockUser()
|
||||
} else {
|
||||
this.showingConfirmBlock = true
|
||||
}
|
||||
}
|
||||
},
|
||||
doBlockUser () {
|
||||
this.$store.dispatch('blockUser', this.user.id)
|
||||
doBlockUser() {
|
||||
this.$store.dispatch('blockUser', { id: this.user.id })
|
||||
this.hideConfirmBlock()
|
||||
},
|
||||
unblockUser () {
|
||||
unblockUser() {
|
||||
this.$store.dispatch('unblockUser', this.user.id)
|
||||
},
|
||||
removeUserFromFollowers () {
|
||||
removeUserFromFollowers() {
|
||||
if (!this.shouldConfirmRemoveUserFromFollowers) {
|
||||
this.doRemoveUserFromFollowers()
|
||||
} else {
|
||||
this.showConfirmRemoveUserFromFollowers()
|
||||
}
|
||||
},
|
||||
doRemoveUserFromFollowers () {
|
||||
doRemoveUserFromFollowers() {
|
||||
this.$store.dispatch('removeUserFromFollowers', this.user.id)
|
||||
this.hideConfirmRemoveUserFromFollowers()
|
||||
},
|
||||
reportUser () {
|
||||
reportUser() {
|
||||
useReportsStore().openUserReportingModal({ userId: this.user.id })
|
||||
},
|
||||
openChat () {
|
||||
openChat() {
|
||||
this.$router.push({
|
||||
name: 'chat',
|
||||
params: { username: this.$store.state.users.currentUser.screen_name, recipient_id: this.user.id }
|
||||
params: {
|
||||
username: this.$store.state.users.currentUser.screen_name,
|
||||
recipient_id: this.user.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
shouldConfirmBlock () {
|
||||
return this.$store.getters.mergedConfig.modalOnBlock
|
||||
shouldConfirmBlock() {
|
||||
return useMergedConfigStore().mergedConfig.modalOnBlock
|
||||
},
|
||||
shouldConfirmRemoveUserFromFollowers () {
|
||||
return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers
|
||||
shouldConfirmRemoveUserFromFollowers() {
|
||||
return useMergedConfigStore().mergedConfig.modalOnRemoveUserFromFollowers
|
||||
},
|
||||
...mapState({
|
||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
||||
})
|
||||
}
|
||||
...mapState(useInstanceCapabilitiesStore, [
|
||||
'blockExpiration',
|
||||
'pleromaChatMessagesAvailable',
|
||||
]),
|
||||
},
|
||||
}
|
||||
|
||||
export default AccountActions
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
<Popover
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
:bound-to="{ x: 'container' }"
|
||||
remove-padding
|
||||
>
|
||||
<template #content>
|
||||
|
|
@ -96,7 +95,8 @@
|
|||
</Popover>
|
||||
<teleport to="#modal">
|
||||
<confirm-modal
|
||||
v-if="showingConfirmBlock"
|
||||
v-if="showingConfirmBlock && !blockExpiration"
|
||||
ref="blockDialog"
|
||||
:title="$t('user_card.block_confirm_title')"
|
||||
:confirm-text="$t('user_card.block_confirm_accept_button')"
|
||||
:cancel-text="$t('user_card.block_confirm_cancel_button')"
|
||||
|
|
@ -137,6 +137,12 @@
|
|||
</template>
|
||||
</i18n-t>
|
||||
</confirm-modal>
|
||||
<UserTimedFilterModal
|
||||
v-if="blockExpiration"
|
||||
ref="timedBlockDialog"
|
||||
:is-mute="false"
|
||||
:user="user"
|
||||
/>
|
||||
</teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,57 +1,51 @@
|
|||
export default {
|
||||
name: 'Alert',
|
||||
selector: '.alert',
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Icon',
|
||||
'Link',
|
||||
'Border',
|
||||
'ButtonUnstyled'
|
||||
],
|
||||
validInnerComponents: ['Text', 'Icon', 'Link', 'Border', 'ButtonUnstyled'],
|
||||
variants: {
|
||||
normal: '.neutral',
|
||||
error: '.error',
|
||||
warning: '.warning',
|
||||
success: '.success'
|
||||
success: '.success',
|
||||
},
|
||||
editor: {
|
||||
border: 1,
|
||||
aspect: '3 / 1'
|
||||
aspect: '3 / 1',
|
||||
},
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
background: '--text',
|
||||
opacity: 0.5,
|
||||
blur: '9px'
|
||||
}
|
||||
blur: '9px',
|
||||
},
|
||||
},
|
||||
{
|
||||
parent: {
|
||||
component: 'Alert'
|
||||
component: 'Alert',
|
||||
},
|
||||
component: 'Border',
|
||||
directives: {
|
||||
textColor: '--parent'
|
||||
}
|
||||
textColor: '--parent',
|
||||
},
|
||||
},
|
||||
{
|
||||
variant: 'error',
|
||||
directives: {
|
||||
background: '--cRed'
|
||||
}
|
||||
background: '--cRed',
|
||||
},
|
||||
},
|
||||
{
|
||||
variant: 'warning',
|
||||
directives: {
|
||||
background: '--cOrange'
|
||||
}
|
||||
background: '--cOrange',
|
||||
},
|
||||
},
|
||||
{
|
||||
variant: 'success',
|
||||
directives: {
|
||||
background: '--cGreen'
|
||||
}
|
||||
}
|
||||
]
|
||||
background: '--cGreen',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,109 +1,130 @@
|
|||
import { mapState } from 'vuex'
|
||||
|
||||
import localeService from '../../services/locale/locale.service.js'
|
||||
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
|
||||
import RichContent from '../rich_content/rich_content.jsx'
|
||||
import localeService from '../../services/locale/locale.service.js'
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements.js'
|
||||
|
||||
const Announcement = {
|
||||
components: {
|
||||
AnnouncementEditor,
|
||||
RichContent
|
||||
RichContent,
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
editing: false,
|
||||
editedAnnouncement: {
|
||||
content: '',
|
||||
startsAt: undefined,
|
||||
endsAt: undefined,
|
||||
allDay: undefined
|
||||
allDay: undefined,
|
||||
},
|
||||
editError: ''
|
||||
editError: '',
|
||||
}
|
||||
},
|
||||
props: {
|
||||
announcement: Object
|
||||
announcement: Object,
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
}),
|
||||
canEditAnnouncement () {
|
||||
return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements')
|
||||
canEditAnnouncement() {
|
||||
return (
|
||||
this.currentUser &&
|
||||
this.currentUser.privileges.includes(
|
||||
'announcements_manage_announcements',
|
||||
)
|
||||
)
|
||||
},
|
||||
content () {
|
||||
content() {
|
||||
return this.announcement.content
|
||||
},
|
||||
isRead () {
|
||||
isRead() {
|
||||
return this.announcement.read
|
||||
},
|
||||
publishedAt () {
|
||||
publishedAt() {
|
||||
const time = this.announcement.published_at
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
return this.formatTimeOrDate(
|
||||
time,
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale),
|
||||
)
|
||||
},
|
||||
startsAt () {
|
||||
startsAt() {
|
||||
const time = this.announcement.starts_at
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
return this.formatTimeOrDate(
|
||||
time,
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale),
|
||||
)
|
||||
},
|
||||
endsAt () {
|
||||
endsAt() {
|
||||
const time = this.announcement.ends_at
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
return this.formatTimeOrDate(
|
||||
time,
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale),
|
||||
)
|
||||
},
|
||||
inactive () {
|
||||
inactive() {
|
||||
return this.announcement.inactive
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
markAsRead () {
|
||||
markAsRead() {
|
||||
if (!this.isRead) {
|
||||
return useAnnouncementsStore().markAnnouncementAsRead(this.announcement.id)
|
||||
return useAnnouncementsStore().markAnnouncementAsRead(
|
||||
this.announcement.id,
|
||||
)
|
||||
}
|
||||
},
|
||||
deleteAnnouncement () {
|
||||
deleteAnnouncement() {
|
||||
return useAnnouncementsStore().deleteAnnouncement(this.announcement.id)
|
||||
},
|
||||
formatTimeOrDate (time, locale) {
|
||||
formatTimeOrDate(time, locale) {
|
||||
const d = new Date(time)
|
||||
return this.announcement.all_day ? d.toLocaleDateString(locale) : d.toLocaleString(locale)
|
||||
return this.announcement.all_day
|
||||
? d.toLocaleDateString(locale)
|
||||
: d.toLocaleString(locale)
|
||||
},
|
||||
enterEditMode () {
|
||||
enterEditMode() {
|
||||
this.editedAnnouncement.content = this.announcement.pleroma.raw_content
|
||||
this.editedAnnouncement.startsAt = this.announcement.starts_at
|
||||
this.editedAnnouncement.endsAt = this.announcement.ends_at
|
||||
this.editedAnnouncement.allDay = this.announcement.all_day
|
||||
this.editing = true
|
||||
},
|
||||
submitEdit () {
|
||||
useAnnouncementsStore().editAnnouncement({
|
||||
id: this.announcement.id,
|
||||
...this.editedAnnouncement
|
||||
})
|
||||
submitEdit() {
|
||||
useAnnouncementsStore()
|
||||
.editAnnouncement({
|
||||
id: this.announcement.id,
|
||||
...this.editedAnnouncement,
|
||||
})
|
||||
.then(() => {
|
||||
this.editing = false
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
this.editError = error.error
|
||||
})
|
||||
},
|
||||
cancelEdit () {
|
||||
cancelEdit() {
|
||||
this.editing = false
|
||||
},
|
||||
clearError () {
|
||||
clearError() {
|
||||
this.editError = undefined
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default Announcement
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ import Checkbox from '../checkbox/checkbox.vue'
|
|||
|
||||
const AnnouncementEditor = {
|
||||
components: {
|
||||
Checkbox
|
||||
Checkbox,
|
||||
},
|
||||
props: {
|
||||
announcement: Object,
|
||||
disabled: Boolean
|
||||
}
|
||||
disabled: Boolean,
|
||||
},
|
||||
}
|
||||
|
||||
export default AnnouncementEditor
|
||||
|
|
|
|||
|
|
@ -1,59 +1,67 @@
|
|||
import { mapState } from 'vuex'
|
||||
|
||||
import Announcement from '../announcement/announcement.vue'
|
||||
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements'
|
||||
|
||||
import { useAnnouncementsStore } from 'src/stores/announcements.js'
|
||||
|
||||
const AnnouncementsPage = {
|
||||
components: {
|
||||
Announcement,
|
||||
AnnouncementEditor
|
||||
AnnouncementEditor,
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
newAnnouncement: {
|
||||
content: '',
|
||||
startsAt: undefined,
|
||||
endsAt: undefined,
|
||||
allDay: false
|
||||
allDay: false,
|
||||
},
|
||||
posting: false,
|
||||
error: undefined
|
||||
error: undefined,
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
mounted() {
|
||||
useAnnouncementsStore().fetchAnnouncements()
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
}),
|
||||
announcements () {
|
||||
announcements() {
|
||||
return useAnnouncementsStore().announcements
|
||||
},
|
||||
canPostAnnouncement () {
|
||||
return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements')
|
||||
}
|
||||
canPostAnnouncement() {
|
||||
return (
|
||||
this.currentUser &&
|
||||
this.currentUser.privileges.includes(
|
||||
'announcements_manage_announcements',
|
||||
)
|
||||
)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
postAnnouncement () {
|
||||
postAnnouncement() {
|
||||
this.posting = true
|
||||
useAnnouncementsStore().postAnnouncement(this.newAnnouncement)
|
||||
useAnnouncementsStore()
|
||||
.postAnnouncement(this.newAnnouncement)
|
||||
.then(() => {
|
||||
this.newAnnouncement.content = ''
|
||||
this.startsAt = undefined
|
||||
this.endsAt = undefined
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
this.error = error.error
|
||||
})
|
||||
.finally(() => {
|
||||
this.posting = false
|
||||
})
|
||||
},
|
||||
clearError () {
|
||||
clearError() {
|
||||
this.error = undefined
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default AnnouncementsPage
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@
|
|||
export default {
|
||||
emits: ['resetAsyncComponent'],
|
||||
methods: {
|
||||
retry () {
|
||||
retry() {
|
||||
this.$emit('resetAsyncComponent')
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,29 @@
|
|||
import StillImage from '../still-image/still-image.vue'
|
||||
import Flash from '../flash/flash.vue'
|
||||
import VideoAttachment from '../video_attachment/video_attachment.vue'
|
||||
import { mapState } from 'pinia'
|
||||
|
||||
import nsfwImage from '../../assets/nsfw.png'
|
||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||
import { mapGetters } from 'vuex'
|
||||
import Flash from '../flash/flash.vue'
|
||||
import StillImage from '../still-image/still-image.vue'
|
||||
import VideoAttachment from '../video_attachment/video_attachment.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
import { useMediaViewerStore } from 'src/stores/media_viewer'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faAlignRight,
|
||||
faFile,
|
||||
faMusic,
|
||||
faImage,
|
||||
faVideo,
|
||||
faPlayCircle,
|
||||
faTimes,
|
||||
faStop,
|
||||
faSearchPlus,
|
||||
faTrashAlt,
|
||||
faMusic,
|
||||
faPencilAlt,
|
||||
faAlignRight
|
||||
faPlayCircle,
|
||||
faSearchPlus,
|
||||
faStop,
|
||||
faTimes,
|
||||
faTrashAlt,
|
||||
faVideo,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useMediaViewerStore } from 'src/stores/media_viewer'
|
||||
|
||||
library.add(
|
||||
faFile,
|
||||
|
|
@ -31,7 +36,7 @@ library.add(
|
|||
faSearchPlus,
|
||||
faTrashAlt,
|
||||
faPencilAlt,
|
||||
faAlignRight
|
||||
faAlignRight,
|
||||
)
|
||||
|
||||
const Attachment = {
|
||||
|
|
@ -46,72 +51,72 @@ const Attachment = {
|
|||
'remove',
|
||||
'shiftUp',
|
||||
'shiftDn',
|
||||
'edit'
|
||||
'edit',
|
||||
],
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
localDescription: this.description || this.attachment.description,
|
||||
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
|
||||
hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
|
||||
preloadImage: this.$store.getters.mergedConfig.preloadImage,
|
||||
nsfwImage:
|
||||
useInstanceStore().instanceIdentity.nsfwCensorImage || nsfwImage,
|
||||
hideNsfwLocal: useMergedConfigStore().mergedConfig.hideNsfw,
|
||||
preloadImage: useMergedConfigStore().mergedConfig.preloadImage,
|
||||
loading: false,
|
||||
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
|
||||
img: this.attachment.type === 'image' && document.createElement('img'),
|
||||
modalOpen: false,
|
||||
showHidden: false,
|
||||
flashLoaded: false,
|
||||
showDescription: false
|
||||
showDescription: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Flash,
|
||||
StillImage,
|
||||
VideoAttachment
|
||||
VideoAttachment,
|
||||
},
|
||||
computed: {
|
||||
classNames () {
|
||||
classNames() {
|
||||
return [
|
||||
{
|
||||
'-loading': this.loading,
|
||||
'-nsfw-placeholder': this.hidden,
|
||||
'-editable': this.edit !== undefined,
|
||||
'-compact': this.compact
|
||||
'-compact': this.compact,
|
||||
},
|
||||
'-type-' + this.type,
|
||||
'-type-' + this.attachment.type,
|
||||
this.size && '-size-' + this.size,
|
||||
`-${this.useContainFit ? 'contain' : 'cover'}-fit`
|
||||
`-${this.useContainFit ? 'contain' : 'cover'}-fit`,
|
||||
]
|
||||
},
|
||||
usePlaceholder () {
|
||||
usePlaceholder() {
|
||||
return this.size === 'hide'
|
||||
},
|
||||
useContainFit () {
|
||||
return this.$store.getters.mergedConfig.useContainFit
|
||||
useContainFit() {
|
||||
return this.mergedConfig.useContainFit
|
||||
},
|
||||
placeholderName () {
|
||||
placeholderName() {
|
||||
if (this.attachment.description === '' || !this.attachment.description) {
|
||||
return this.type.toUpperCase()
|
||||
return this.attachment.type.toUpperCase()
|
||||
}
|
||||
return this.attachment.description
|
||||
},
|
||||
placeholderIconClass () {
|
||||
if (this.type === 'image') return 'image'
|
||||
if (this.type === 'video') return 'video'
|
||||
if (this.type === 'audio') return 'music'
|
||||
placeholderIconClass() {
|
||||
if (this.attachment.type === 'image') return 'image'
|
||||
if (this.attachment.type === 'video') return 'video'
|
||||
if (this.attachment.type === 'audio') return 'music'
|
||||
return 'file'
|
||||
},
|
||||
referrerpolicy () {
|
||||
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
|
||||
referrerpolicy() {
|
||||
return useInstanceCapabilitiesStore().mediaProxyAvailable
|
||||
? ''
|
||||
: 'no-referrer'
|
||||
},
|
||||
type () {
|
||||
return fileTypeService.fileType(this.attachment.mimetype)
|
||||
},
|
||||
hidden () {
|
||||
hidden() {
|
||||
return this.nsfw && this.hideNsfwLocal && !this.showHidden
|
||||
},
|
||||
isEmpty () {
|
||||
return (this.type === 'html' && !this.attachment.oembed)
|
||||
isEmpty() {
|
||||
return this.attachment.type === 'html' && !this.attachment.oembed
|
||||
},
|
||||
useModal () {
|
||||
useModal() {
|
||||
let modalTypes = []
|
||||
switch (this.size) {
|
||||
case 'hide':
|
||||
|
|
@ -124,64 +129,66 @@ const Attachment = {
|
|||
: ['image']
|
||||
break
|
||||
}
|
||||
return modalTypes.includes(this.type)
|
||||
return modalTypes.includes(this.attachment.type)
|
||||
},
|
||||
videoTag () {
|
||||
videoTag() {
|
||||
return this.useModal ? 'button' : 'span'
|
||||
},
|
||||
...mapGetters(['mergedConfig'])
|
||||
...mapState(useMergedConfigStore, ['mergedConfig']),
|
||||
},
|
||||
watch: {
|
||||
'attachment.description' (newVal) {
|
||||
'attachment.description'(newVal) {
|
||||
this.localDescription = newVal
|
||||
},
|
||||
localDescription (newVal) {
|
||||
localDescription(newVal) {
|
||||
this.onEdit(newVal)
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
linkClicked ({ target }) {
|
||||
linkClicked({ target }) {
|
||||
if (target.tagName === 'A') {
|
||||
window.open(target.href, '_blank')
|
||||
}
|
||||
},
|
||||
openModal () {
|
||||
openModal() {
|
||||
if (this.useModal) {
|
||||
this.$emit('setMedia')
|
||||
useMediaViewerStore().setCurrentMedia(this.attachment)
|
||||
} else if (this.type === 'unknown') {
|
||||
} else if (this.attachment.type === 'unknown') {
|
||||
window.open(this.attachment.url)
|
||||
}
|
||||
},
|
||||
openModalForce () {
|
||||
openModalForce() {
|
||||
this.$emit('setMedia')
|
||||
useMediaViewerStore().setCurrentMedia(this.attachment)
|
||||
},
|
||||
onEdit (event) {
|
||||
onEdit(event) {
|
||||
this.edit && this.edit(this.attachment, event)
|
||||
},
|
||||
onRemove () {
|
||||
onRemove() {
|
||||
this.remove && this.remove(this.attachment)
|
||||
},
|
||||
onShiftUp () {
|
||||
onShiftUp() {
|
||||
this.shiftUp && this.shiftUp(this.attachment)
|
||||
},
|
||||
onShiftDn () {
|
||||
onShiftDn() {
|
||||
this.shiftDn && this.shiftDn(this.attachment)
|
||||
},
|
||||
stopFlash () {
|
||||
stopFlash() {
|
||||
this.$refs.flash.closePlayer()
|
||||
},
|
||||
setFlashLoaded (event) {
|
||||
setFlashLoaded(event) {
|
||||
this.flashLoaded = event
|
||||
},
|
||||
toggleDescription () {
|
||||
toggleDescription() {
|
||||
this.showDescription = !this.showDescription
|
||||
},
|
||||
toggleHidden (event) {
|
||||
toggleHidden(event) {
|
||||
if (
|
||||
(this.mergedConfig.useOneClickNsfw && !this.showHidden) &&
|
||||
(this.type !== 'video' || this.mergedConfig.playVideosInModal)
|
||||
this.mergedConfig.useOneClickNsfw &&
|
||||
!this.showHidden &&
|
||||
(this.attachment.type !== 'video' ||
|
||||
this.mergedConfig.playVideosInModal)
|
||||
) {
|
||||
this.openModal(event)
|
||||
return
|
||||
|
|
@ -201,12 +208,12 @@ const Attachment = {
|
|||
this.showHidden = !this.showHidden
|
||||
}
|
||||
},
|
||||
onImageLoad (image) {
|
||||
onImageLoad(image) {
|
||||
const width = image.naturalWidth
|
||||
const height = image.naturalHeight
|
||||
this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height })
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default Attachment
|
||||
|
|
|
|||
|
|
@ -107,9 +107,9 @@
|
|||
|
||||
.play-icon {
|
||||
position: absolute;
|
||||
font-size: 64px;
|
||||
top: calc(50% - 32px);
|
||||
left: calc(50% - 32px);
|
||||
font-size: 4.5em;
|
||||
top: calc(50% - 2.25rem);
|
||||
left: calc(50% - 2.25rem);
|
||||
color: rgb(255 255 255 / 75%);
|
||||
text-shadow: 0 0 2px rgb(0 0 0 / 40%);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
export default {
|
||||
name: 'Attachment',
|
||||
selector: '.Attachment',
|
||||
notEditable: true,
|
||||
validInnerComponents: [
|
||||
'Border',
|
||||
'Button',
|
||||
'Input'
|
||||
],
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
roundness: 3
|
||||
}
|
||||
},
|
||||
{
|
||||
component: 'Button',
|
||||
parent: {
|
||||
component: 'Attachment'
|
||||
},
|
||||
directives: {
|
||||
background: '#FFFFFF',
|
||||
opacity: 0.5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
@click="openModal"
|
||||
>
|
||||
<a
|
||||
v-if="type !== 'html'"
|
||||
v-if="attachment.type !== 'html'"
|
||||
class="placeholder"
|
||||
target="_blank"
|
||||
:href="attachment.url"
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
>
|
||||
<button
|
||||
v-if="remove"
|
||||
class="button-default attachment-button"
|
||||
class="button-default attachment-button -transparent"
|
||||
@click.prevent="onRemove"
|
||||
>
|
||||
<FAIcon icon="trash-alt" />
|
||||
|
|
@ -70,7 +70,7 @@
|
|||
:src="nsfwImage"
|
||||
>
|
||||
<FAIcon
|
||||
v-if="type === 'video'"
|
||||
v-if="attachment.type === 'video'"
|
||||
class="play-icon"
|
||||
icon="play-circle"
|
||||
/>
|
||||
|
|
@ -80,24 +80,24 @@
|
|||
class="attachment-buttons"
|
||||
>
|
||||
<button
|
||||
v-if="type === 'flash' && flashLoaded"
|
||||
class="button-default attachment-button"
|
||||
v-if="attachment.type === 'flash' && flashLoaded"
|
||||
class="button-default attachment-button -transparent"
|
||||
: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-default attachment-button"
|
||||
v-if="attachment.description && size !== 'small' && !edit && attachment.type !== 'unknown'"
|
||||
class="button-default attachment-button -transparent"
|
||||
:title="$t('status.show_attachment_description')"
|
||||
@click.prevent="toggleDescription"
|
||||
>
|
||||
<FAIcon icon="align-right" />
|
||||
</button>
|
||||
<button
|
||||
v-if="!useModal && type !== 'unknown'"
|
||||
class="button-default attachment-button"
|
||||
v-if="!useModal && attachment.type !== 'unknown'"
|
||||
class="button-default attachment-button -transparent"
|
||||
:title="$t('status.show_attachment_in_modal')"
|
||||
@click.prevent="openModalForce"
|
||||
>
|
||||
|
|
@ -105,7 +105,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="nsfw && hideNsfwLocal"
|
||||
class="button-default attachment-button"
|
||||
class="button-default attachment-button -transparent"
|
||||
:title="$t('status.hide_attachment')"
|
||||
@click.prevent="toggleHidden"
|
||||
>
|
||||
|
|
@ -113,7 +113,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="shiftUp"
|
||||
class="button-default attachment-button"
|
||||
class="button-default attachment-button -transparent"
|
||||
:title="$t('status.move_up')"
|
||||
@click.prevent="onShiftUp"
|
||||
>
|
||||
|
|
@ -121,7 +121,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="shiftDn"
|
||||
class="button-default attachment-button"
|
||||
class="button-default attachment-button -transparent"
|
||||
:title="$t('status.move_down')"
|
||||
@click.prevent="onShiftDn"
|
||||
>
|
||||
|
|
@ -129,7 +129,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="remove"
|
||||
class="button-default attachment-button"
|
||||
class="button-default attachment-button -transparent"
|
||||
:title="$t('status.remove_attachment')"
|
||||
@click.prevent="onRemove"
|
||||
>
|
||||
|
|
@ -138,7 +138,7 @@
|
|||
</div>
|
||||
|
||||
<a
|
||||
v-if="type === 'image' && (!hidden || preloadImage)"
|
||||
v-if="attachment.type === 'image' && (!hidden || preloadImage)"
|
||||
class="image-container"
|
||||
:class="{'-hidden': hidden && preloadImage }"
|
||||
:href="attachment.url"
|
||||
|
|
@ -156,7 +156,7 @@
|
|||
</a>
|
||||
|
||||
<a
|
||||
v-if="type === 'unknown' && !hidden"
|
||||
v-if="attachment.type === 'unknown' && !hidden"
|
||||
class="placeholder-container"
|
||||
:href="attachment.url"
|
||||
target="_blank"
|
||||
|
|
@ -173,7 +173,7 @@
|
|||
|
||||
<component
|
||||
:is="videoTag"
|
||||
v-if="type === 'video' && !hidden"
|
||||
v-if="attachment.type === 'video' && !hidden"
|
||||
class="video-container"
|
||||
:href="attachment.url"
|
||||
@click.stop.prevent="openModal"
|
||||
|
|
@ -193,13 +193,13 @@
|
|||
</component>
|
||||
|
||||
<span
|
||||
v-if="type === 'audio' && !hidden"
|
||||
v-if="attachment.type === 'audio' && !hidden"
|
||||
class="audio-container"
|
||||
:href="attachment.url"
|
||||
@click.stop.prevent="openModal"
|
||||
>
|
||||
<audio
|
||||
v-if="type === 'audio'"
|
||||
v-if="attachment.type === 'audio'"
|
||||
:src="attachment.url"
|
||||
:alt="attachment.description"
|
||||
:title="attachment.description"
|
||||
|
|
@ -210,7 +210,7 @@
|
|||
</span>
|
||||
|
||||
<div
|
||||
v-if="type === 'html' && attachment.oembed"
|
||||
v-if="attachment.type === 'html' && attachment.oembed"
|
||||
class="oembed-container"
|
||||
@click.prevent="linkClicked"
|
||||
>
|
||||
|
|
@ -229,7 +229,7 @@
|
|||
</div>
|
||||
|
||||
<span
|
||||
v-if="type === 'flash' && !hidden"
|
||||
v-if="attachment.type === 'flash' && !hidden"
|
||||
class="flash-container"
|
||||
:href="attachment.url"
|
||||
@click.stop.prevent="openModal"
|
||||
|
|
|
|||
|
|
@ -1,28 +1,34 @@
|
|||
import { mapState } from 'pinia'
|
||||
import { h, resolveComponent } from 'vue'
|
||||
|
||||
import LoginForm from '../login_form/login_form.vue'
|
||||
import MFARecoveryForm from '../mfa_form/recovery_form.vue'
|
||||
import MFATOTPForm from '../mfa_form/totp_form.vue'
|
||||
import { mapState } from 'pinia'
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow'
|
||||
|
||||
import { useAuthFlowStore } from 'src/stores/auth_flow.js'
|
||||
|
||||
const AuthForm = {
|
||||
name: 'AuthForm',
|
||||
render () {
|
||||
render() {
|
||||
return h(resolveComponent(this.authForm))
|
||||
},
|
||||
computed: {
|
||||
authForm () {
|
||||
if (this.requiredTOTP) { return 'MFATOTPForm' }
|
||||
if (this.requiredRecovery) { return 'MFARecoveryForm' }
|
||||
authForm() {
|
||||
if (this.requiredTOTP) {
|
||||
return 'MFATOTPForm'
|
||||
}
|
||||
if (this.requiredRecovery) {
|
||||
return 'MFARecoveryForm'
|
||||
}
|
||||
return 'LoginForm'
|
||||
},
|
||||
...mapState(useAuthFlowStore, ['requiredTOTP', 'requiredRecovery'])
|
||||
...mapState(useAuthFlowStore, ['requiredTOTP', 'requiredRecovery']),
|
||||
},
|
||||
components: {
|
||||
MFARecoveryForm,
|
||||
MFATOTPForm,
|
||||
LoginForm
|
||||
}
|
||||
LoginForm,
|
||||
},
|
||||
}
|
||||
|
||||
export default AuthForm
|
||||
|
|
|
|||
|
|
@ -2,51 +2,55 @@ const debounceMilliseconds = 500
|
|||
|
||||
export default {
|
||||
props: {
|
||||
query: { // function to query results and return a promise
|
||||
query: {
|
||||
// function to query results and return a promise
|
||||
type: Function,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
filter: { // function to filter results in real time
|
||||
type: Function
|
||||
filter: {
|
||||
// function to filter results in real time
|
||||
type: Function,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Search...'
|
||||
}
|
||||
default: 'Search...',
|
||||
},
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
term: '',
|
||||
timeout: null,
|
||||
results: [],
|
||||
resultsVisible: false
|
||||
resultsVisible: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filtered () {
|
||||
filtered() {
|
||||
return this.filter ? this.filter(this.results) : this.results
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
term (val) {
|
||||
term(val) {
|
||||
this.fetchResults(val)
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchResults (term) {
|
||||
fetchResults(term) {
|
||||
clearTimeout(this.timeout)
|
||||
this.timeout = setTimeout(() => {
|
||||
this.results = []
|
||||
if (term) {
|
||||
this.query(term).then((results) => { this.results = results })
|
||||
this.query(term).then((results) => {
|
||||
this.results = results
|
||||
})
|
||||
}
|
||||
}, debounceMilliseconds)
|
||||
},
|
||||
onInputClick () {
|
||||
onInputClick() {
|
||||
this.resultsVisible = true
|
||||
},
|
||||
onClickOutside () {
|
||||
onClickOutside() {
|
||||
this.resultsVisible = false
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,28 @@
|
|||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
const AvatarList = {
|
||||
props: ['users'],
|
||||
computed: {
|
||||
slicedUsers () {
|
||||
slicedUsers() {
|
||||
return this.users ? this.users.slice(0, 15) : []
|
||||
}
|
||||
},
|
||||
},
|
||||
components: {
|
||||
UserAvatar
|
||||
UserAvatar,
|
||||
},
|
||||
methods: {
|
||||
userProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
}
|
||||
}
|
||||
userProfileLink(user) {
|
||||
return generateProfileLink(
|
||||
user.id,
|
||||
user.screen_name,
|
||||
useInstanceStore().restrictedNicknames,
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default AvatarList
|
||||
|
|
|
|||
|
|
@ -1,30 +1,27 @@
|
|||
export default {
|
||||
name: 'Badge',
|
||||
selector: '.badge',
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Icon'
|
||||
],
|
||||
validInnerComponents: ['Text', 'Icon'],
|
||||
variants: {
|
||||
notification: '.-notification'
|
||||
notification: '.-notification',
|
||||
},
|
||||
defaultRules: [
|
||||
{
|
||||
component: 'Root',
|
||||
directives: {
|
||||
'--badgeNotification': 'color | --cRed'
|
||||
}
|
||||
'--badgeNotification': 'color | --cRed',
|
||||
},
|
||||
},
|
||||
{
|
||||
directives: {
|
||||
background: '--cGreen'
|
||||
}
|
||||
background: '--cGreen',
|
||||
},
|
||||
},
|
||||
{
|
||||
variant: 'notification',
|
||||
directives: {
|
||||
background: '--cRed'
|
||||
}
|
||||
}
|
||||
]
|
||||
background: '--cRed',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,29 @@
|
|||
import UserPopover from '../user_popover/user_popover.vue'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import UserLink from '../user_link/user_link.vue'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import UserPopover from '../user_popover/user_popover.vue'
|
||||
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
const BasicUserCard = {
|
||||
props: [
|
||||
'user'
|
||||
],
|
||||
props: ['user'],
|
||||
components: {
|
||||
UserPopover,
|
||||
UserAvatar,
|
||||
RichContent,
|
||||
UserLink
|
||||
UserLink,
|
||||
},
|
||||
methods: {
|
||||
userProfileLink (user) {
|
||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||
}
|
||||
}
|
||||
userProfileLink(user) {
|
||||
return generateProfileLink(
|
||||
user.id,
|
||||
user.screen_name,
|
||||
useInstanceStore().restrictedNicknames,
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default BasicUserCard
|
||||
|
|
|
|||
|
|
@ -1,40 +1,50 @@
|
|||
import { mapState } from 'pinia'
|
||||
|
||||
import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
|
||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
|
||||
const BlockCard = {
|
||||
props: ['userId'],
|
||||
data () {
|
||||
return {
|
||||
progress: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
user() {
|
||||
return this.$store.getters.findUser(this.userId)
|
||||
},
|
||||
relationship () {
|
||||
relationship() {
|
||||
return this.$store.getters.relationship(this.userId)
|
||||
},
|
||||
blocked () {
|
||||
blocked() {
|
||||
return this.relationship.blocking
|
||||
}
|
||||
},
|
||||
blockExpiryAvailable() {
|
||||
return Object.hasOwn(this.user, 'block_expires_at')
|
||||
},
|
||||
blockExpiry() {
|
||||
return this.user.block_expires_at === false
|
||||
? this.$t('user_card.block_expires_forever')
|
||||
: this.$t('user_card.block_expires_at', [
|
||||
new Date(this.user.mute_expires_at).toLocaleString(),
|
||||
])
|
||||
},
|
||||
...mapState(useInstanceCapabilitiesStore, ['blockExpiration']),
|
||||
},
|
||||
components: {
|
||||
BasicUserCard
|
||||
BasicUserCard,
|
||||
UserTimedFilterModal,
|
||||
},
|
||||
methods: {
|
||||
unblockUser () {
|
||||
this.progress = true
|
||||
this.$store.dispatch('unblockUser', this.user.id).then(() => {
|
||||
this.progress = false
|
||||
})
|
||||
unblockUser() {
|
||||
this.$store.dispatch('unblockUser', this.user.id)
|
||||
},
|
||||
blockUser () {
|
||||
this.progress = true
|
||||
this.$store.dispatch('blockUser', this.user.id).then(() => {
|
||||
this.progress = false
|
||||
})
|
||||
}
|
||||
}
|
||||
blockUser() {
|
||||
if (this.blockExpiration) {
|
||||
this.$refs.timedBlockDialog.optionallyPrompt()
|
||||
} else {
|
||||
this.$store.dispatch('blockUser', { id: this.user.id })
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default BlockCard
|
||||
|
|
|
|||
|
|
@ -1,33 +1,35 @@
|
|||
<template>
|
||||
<basic-user-card :user="user">
|
||||
<div class="block-card-content-container">
|
||||
<span
|
||||
v-if="blocked && blockExpiryAvailable"
|
||||
class="alert neutral"
|
||||
>
|
||||
{{ blockExpiry }}
|
||||
</span>
|
||||
{{ ' ' }}
|
||||
<button
|
||||
v-if="blocked"
|
||||
class="btn button-default"
|
||||
:disabled="progress"
|
||||
@click="unblockUser"
|
||||
>
|
||||
<template v-if="progress">
|
||||
{{ $t('user_card.unblock_progress') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('user_card.unblock') }}
|
||||
</template>
|
||||
{{ $t('user_card.unblock') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="btn button-default"
|
||||
:disabled="progress"
|
||||
@click="blockUser"
|
||||
>
|
||||
<template v-if="progress">
|
||||
{{ $t('user_card.block_progress') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('user_card.block') }}
|
||||
</template>
|
||||
{{ $t('user_card.block') }}
|
||||
</button>
|
||||
</div>
|
||||
<teleport to="#modal">
|
||||
<UserTimedFilterModal
|
||||
ref="timedBlockDialog"
|
||||
:user="user"
|
||||
:is-mute="false"
|
||||
/>
|
||||
</teleport>
|
||||
</basic-user-card>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,15 @@
|
|||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faEllipsisH
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faEllipsisH
|
||||
)
|
||||
library.add(faEllipsisH)
|
||||
|
||||
const BookmarkFolderCard = {
|
||||
props: [
|
||||
'folder',
|
||||
'allBookmarks'
|
||||
],
|
||||
props: ['folder', 'allBookmarks'],
|
||||
computed: {
|
||||
firstLetter () {
|
||||
firstLetter() {
|
||||
return this.folder ? this.folder.name[0] : null
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default BookmarkFolderCard
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
||||
import apiService from '../../services/api/api.service'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
|
||||
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
||||
|
||||
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
|
||||
const BookmarkFolderEdit = {
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
name: '',
|
||||
nameDraft: '',
|
||||
|
|
@ -13,54 +14,59 @@ const BookmarkFolderEdit = {
|
|||
emojiDraft: '',
|
||||
emojiUrlDraft: null,
|
||||
emojiPickerExpanded: false,
|
||||
reallyDelete: false
|
||||
reallyDelete: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
EmojiPicker
|
||||
EmojiPicker,
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
if (!this.id) return
|
||||
const credentials = this.$store.state.users.currentUser.credentials
|
||||
apiService.fetchBookmarkFolders({ credentials })
|
||||
.then((folders) => {
|
||||
const folder = folders.find(folder => folder.id === this.id)
|
||||
if (!folder) return
|
||||
apiService.fetchBookmarkFolders({ credentials }).then((folders) => {
|
||||
const folder = folders.find((folder) => folder.id === this.id)
|
||||
if (!folder) return
|
||||
|
||||
this.nameDraft = this.name = folder.name
|
||||
this.emojiDraft = this.emoji = folder.emoji
|
||||
this.emojiUrlDraft = this.emojiUrl = folder.emoji_url
|
||||
})
|
||||
this.nameDraft = this.name = folder.name
|
||||
this.emojiDraft = this.emoji = folder.emoji
|
||||
this.emojiUrlDraft = this.emojiUrl = folder.emoji_url
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
id () {
|
||||
id() {
|
||||
return this.$route.params.id
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selectEmoji (event) {
|
||||
selectEmoji(event) {
|
||||
this.emojiDraft = event.insertion
|
||||
this.emojiUrlDraft = event.insertionUrl
|
||||
},
|
||||
showEmojiPicker () {
|
||||
showEmojiPicker() {
|
||||
if (!this.emojiPickerExpanded) {
|
||||
this.$refs.picker.showPicker()
|
||||
}
|
||||
},
|
||||
onShowPicker () {
|
||||
onShowPicker() {
|
||||
this.emojiPickerExpanded = true
|
||||
},
|
||||
onClosePicker () {
|
||||
onClosePicker() {
|
||||
this.emojiPickerExpanded = false
|
||||
},
|
||||
updateFolder () {
|
||||
useBookmarkFoldersStore().updateBookmarkFolder({ folderId: this.id, name: this.nameDraft, emoji: this.emojiDraft })
|
||||
updateFolder() {
|
||||
useBookmarkFoldersStore()
|
||||
.updateBookmarkFolder({
|
||||
folderId: this.id,
|
||||
name: this.nameDraft,
|
||||
emoji: this.emojiDraft,
|
||||
})
|
||||
.then(() => {
|
||||
this.$router.push({ name: 'bookmark-folders' })
|
||||
})
|
||||
},
|
||||
createFolder () {
|
||||
useBookmarkFoldersStore().createBookmarkFolder({ name: this.nameDraft, emoji: this.emojiDraft })
|
||||
createFolder() {
|
||||
useBookmarkFoldersStore()
|
||||
.createBookmarkFolder({ name: this.nameDraft, emoji: this.emojiDraft })
|
||||
.then(() => {
|
||||
this.$router.push({ name: 'bookmark-folders' })
|
||||
})
|
||||
|
|
@ -68,15 +74,15 @@ const BookmarkFolderEdit = {
|
|||
useInterfaceStore().pushGlobalNotice({
|
||||
messageKey: 'bookmark_folders.error',
|
||||
messageArgs: [e.message],
|
||||
level: 'error'
|
||||
level: 'error',
|
||||
})
|
||||
})
|
||||
},
|
||||
deleteFolder () {
|
||||
deleteFolder() {
|
||||
useBookmarkFoldersStore().deleteBookmarkFolder({ folderId: this.id })
|
||||
this.$router.push({ name: 'bookmark-folders' })
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default BookmarkFolderEdit
|
||||
|
|
|
|||
|
|
@ -1,28 +1,29 @@
|
|||
import BookmarkFolderCard from '../bookmark_folder_card/bookmark_folder_card.vue'
|
||||
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
|
||||
|
||||
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
|
||||
|
||||
const BookmarkFolders = {
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
isNew: false
|
||||
isNew: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BookmarkFolderCard
|
||||
BookmarkFolderCard,
|
||||
},
|
||||
computed: {
|
||||
bookmarkFolders () {
|
||||
bookmarkFolders() {
|
||||
return useBookmarkFoldersStore().allFolders
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
cancelNewFolder () {
|
||||
cancelNewFolder() {
|
||||
this.isNew = false
|
||||
},
|
||||
newFolder () {
|
||||
newFolder() {
|
||||
this.isNew = true
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default BookmarkFolders
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
import { mapState } from 'pinia'
|
||||
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
|
||||
|
||||
import { getBookmarkFolderEntries } from 'src/components/navigation/filter.js'
|
||||
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
|
||||
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
|
||||
|
||||
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
|
||||
|
||||
export const BookmarkFoldersMenuContent = {
|
||||
props: [
|
||||
'showPin'
|
||||
],
|
||||
props: ['showPin'],
|
||||
components: {
|
||||
NavigationEntry
|
||||
NavigationEntry,
|
||||
},
|
||||
computed: {
|
||||
...mapState(useBookmarkFoldersStore, {
|
||||
folders: getBookmarkFolderEntries
|
||||
})
|
||||
}
|
||||
folders: getBookmarkFolderEntries,
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
export default BookmarkFoldersMenuContent
|
||||
|
|
|
|||
|
|
@ -1,32 +1,38 @@
|
|||
import Timeline from '../timeline/timeline.vue'
|
||||
|
||||
const Bookmarks = {
|
||||
created () {
|
||||
created() {
|
||||
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'bookmarks', bookmarkFolderId: this.folderId || null })
|
||||
this.$store.dispatch('startFetchingTimeline', {
|
||||
timeline: 'bookmarks',
|
||||
bookmarkFolderId: this.folderId || null,
|
||||
})
|
||||
},
|
||||
components: {
|
||||
Timeline
|
||||
Timeline,
|
||||
},
|
||||
computed: {
|
||||
folderId () {
|
||||
folderId() {
|
||||
return this.$route.params.id
|
||||
},
|
||||
timeline () {
|
||||
timeline() {
|
||||
return this.$store.state.statuses.timelines.bookmarks
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
folderId () {
|
||||
folderId() {
|
||||
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
|
||||
this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'bookmarks', bookmarkFolderId: this.folderId || null })
|
||||
}
|
||||
this.$store.dispatch('startFetchingTimeline', {
|
||||
timeline: 'bookmarks',
|
||||
bookmarkFolderId: this.folderId || null,
|
||||
})
|
||||
},
|
||||
},
|
||||
unmounted () {
|
||||
unmounted() {
|
||||
this.$store.commit('clearTimeline', { timeline: 'bookmarks' })
|
||||
this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default Bookmarks
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ export default {
|
|||
{
|
||||
directives: {
|
||||
textColor: '$mod(--parent 10)',
|
||||
textAuto: 'no-auto'
|
||||
}
|
||||
}
|
||||
]
|
||||
textAuto: 'no-auto',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
import Timeline from '../timeline/timeline.vue'
|
||||
|
||||
const BubbleTimeline = {
|
||||
components: {
|
||||
Timeline
|
||||
Timeline,
|
||||
},
|
||||
computed: {
|
||||
timeline () { return this.$store.state.statuses.timelines.bubble }
|
||||
timeline() {
|
||||
return this.$store.state.statuses.timelines.bubble
|
||||
},
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
this.$store.dispatch('startFetchingTimeline', { timeline: 'bubble' })
|
||||
},
|
||||
unmounted () {
|
||||
unmounted() {
|
||||
this.$store.dispatch('stopFetchingTimeline', 'bubble')
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
export default BubbleTimeline
|
||||
|
|
|
|||
|
|
@ -10,26 +10,24 @@ export default {
|
|||
// normal: '' // normal state is implicitly added, it is always included
|
||||
toggled: '.toggled',
|
||||
focused: ':focus-within',
|
||||
pressed: ':focus:active',
|
||||
pressed: ':active',
|
||||
hover: ':is(:hover, :focus-visible):not(:disabled)',
|
||||
disabled: ':disabled'
|
||||
disabled: ':disabled',
|
||||
},
|
||||
// Variants are mutually exclusive, each component implicitly has "normal" variant, and all other variants inherit from it.
|
||||
variants: {
|
||||
// Variants save on computation time since adding new variant just adds one more "set".
|
||||
// normal: '', // you can override normal variant, it will be appenended to the main class
|
||||
danger: '.danger'
|
||||
danger: '.-danger',
|
||||
transparent: '.-transparent',
|
||||
// Overall the compuation difficulty is N*((1/6)M^3+M) where M is number of distinct states and N is number of variants.
|
||||
// This (currently) is further multipled by number of places where component can exist.
|
||||
},
|
||||
editor: {
|
||||
aspect: '2 / 1'
|
||||
aspect: '2 / 1',
|
||||
},
|
||||
// This lists all other components that can possibly exist within one. Recursion is currently not supported (and probably won't be supported ever).
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Icon'
|
||||
],
|
||||
validInnerComponents: ['Text', 'Icon'],
|
||||
// Default rules, used as "default theme", essentially.
|
||||
defaultRules: [
|
||||
{
|
||||
|
|
@ -38,9 +36,11 @@ export default {
|
|||
'--buttonDefaultHoverGlow': 'shadow | 0 0 1 2 --text / 0.4',
|
||||
'--buttonDefaultFocusGlow': 'shadow | 0 0 1 2 --link / 0.5',
|
||||
'--buttonDefaultShadow': 'shadow | 0 0 2 #000000',
|
||||
'--buttonDefaultBevel': 'shadow | $borderSide(#FFFFFF top 0.2 1), $borderSide(#000000 bottom 0.2 1)',
|
||||
'--buttonPressedBevel': 'shadow | inset 0 0 4 #000000, $borderSide(#FFFFFF bottom 0.2 1), $borderSide(#000000 top 0.2 1)'
|
||||
}
|
||||
'--buttonDefaultBevel':
|
||||
'shadow | $borderSide(#FFFFFF top 0.2 1), $borderSide(#000000 bottom 0.2 1)',
|
||||
'--buttonPressedBevel':
|
||||
'shadow | inset 0 0 4 #000000, $borderSide(#FFFFFF bottom 0.2 1), $borderSide(#000000 top 0.2 1)',
|
||||
},
|
||||
},
|
||||
{
|
||||
// component: 'Button', // no need to specify components every time unless you're specifying how other component should look
|
||||
|
|
@ -48,89 +48,128 @@ export default {
|
|||
directives: {
|
||||
background: '--fg',
|
||||
shadow: ['--buttonDefaultShadow', '--buttonDefaultBevel'],
|
||||
roundness: 3
|
||||
}
|
||||
roundness: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['hover'],
|
||||
variant: 'danger',
|
||||
directives: {
|
||||
shadow: ['--buttonDefaultHoverGlow', '--buttonDefaultBevel']
|
||||
}
|
||||
background: '--cRed',
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['focused'],
|
||||
variant: 'transparent',
|
||||
directives: {
|
||||
shadow: ['--buttonDefaultFocusGlow', '--buttonDefaultBevel']
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['pressed'],
|
||||
directives: {
|
||||
shadow: ['--buttonDefaultShadow', '--buttonPressedBevel']
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['pressed', 'hover'],
|
||||
directives: {
|
||||
shadow: ['--buttonPressedBevel', '--buttonDefaultHoverGlow']
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['toggled'],
|
||||
directives: {
|
||||
background: '--accent,-24.2',
|
||||
shadow: ['--buttonDefaultShadow', '--buttonPressedBevel']
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['toggled', 'hover'],
|
||||
directives: {
|
||||
background: '--accent,-24.2',
|
||||
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel']
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['toggled', 'focused'],
|
||||
directives: {
|
||||
background: '--accent,-24.2',
|
||||
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel']
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['toggled', 'disabled'],
|
||||
directives: {
|
||||
background: '$blend(--accent 0.25 --parent)',
|
||||
shadow: ['--buttonPressedBevel']
|
||||
}
|
||||
},
|
||||
{
|
||||
state: ['disabled'],
|
||||
directives: {
|
||||
background: '$blend(--inheritedBackground 0.25 --parent)',
|
||||
shadow: ['--buttonDefaultBevel']
|
||||
}
|
||||
opacity: 0.5,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Text',
|
||||
parent: {
|
||||
component: 'Button',
|
||||
state: ['disabled']
|
||||
variant: 'transparent',
|
||||
},
|
||||
directives: {
|
||||
textOpacity: 0.25,
|
||||
textOpacityMode: 'blend'
|
||||
}
|
||||
textColor: '--text',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'Button',
|
||||
state: ['disabled']
|
||||
variant: 'transparent',
|
||||
},
|
||||
directives: {
|
||||
textColor: '--text',
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['hover'],
|
||||
directives: {
|
||||
shadow: ['--buttonDefaultHoverGlow', '--buttonDefaultBevel'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['focused'],
|
||||
directives: {
|
||||
shadow: ['--buttonDefaultFocusGlow', '--buttonDefaultBevel'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['pressed'],
|
||||
directives: {
|
||||
shadow: ['--buttonDefaultShadow', '--buttonPressedBevel'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['pressed', 'hover'],
|
||||
directives: {
|
||||
shadow: ['--buttonPressedBevel', '--buttonDefaultHoverGlow'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['toggled'],
|
||||
directives: {
|
||||
background: '--accent,-24.2',
|
||||
shadow: ['--buttonDefaultShadow', '--buttonPressedBevel'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['toggled', 'hover'],
|
||||
directives: {
|
||||
background: '--accent,-24.2',
|
||||
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['toggled', 'focused'],
|
||||
directives: {
|
||||
background: '--accent,-24.2',
|
||||
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['toggled', 'hover', 'focused'],
|
||||
directives: {
|
||||
background: '--accent,-24.2',
|
||||
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['toggled', 'disabled'],
|
||||
directives: {
|
||||
background: '$blend(--accent 0.25 --parent)',
|
||||
shadow: ['--buttonPressedBevel'],
|
||||
},
|
||||
},
|
||||
{
|
||||
state: ['disabled'],
|
||||
directives: {
|
||||
background: '$blend(--inheritedBackground 0.25 --parent)',
|
||||
shadow: ['--buttonDefaultBevel'],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Text',
|
||||
parent: {
|
||||
component: 'Button',
|
||||
state: ['disabled'],
|
||||
},
|
||||
directives: {
|
||||
textOpacity: 0.25,
|
||||
textOpacityMode: 'blend'
|
||||
}
|
||||
}
|
||||
]
|
||||
textOpacityMode: 'blend',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'Button',
|
||||
state: ['disabled'],
|
||||
},
|
||||
directives: {
|
||||
textOpacity: 0.25,
|
||||
textOpacityMode: 'blend',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,91 +7,86 @@ export default {
|
|||
toggled: '.toggled',
|
||||
disabled: ':disabled',
|
||||
hover: ':is(:hover, :focus-visible):not(:disabled)',
|
||||
focused: ':focus-within:not(:is(:focus-visible))'
|
||||
focused: ':focus-within:not(:is(:focus-visible))',
|
||||
},
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Link',
|
||||
'Icon',
|
||||
'Badge'
|
||||
],
|
||||
validInnerComponents: ['Text', 'Link', 'Icon', 'Badge'],
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
shadow: []
|
||||
}
|
||||
shadow: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'ButtonUnstyled',
|
||||
state: ['hover']
|
||||
state: ['hover'],
|
||||
},
|
||||
directives: {
|
||||
textColor: '--parent--text'
|
||||
}
|
||||
textColor: '--parent--text',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'ButtonUnstyled',
|
||||
state: ['toggled']
|
||||
state: ['toggled'],
|
||||
},
|
||||
directives: {
|
||||
textColor: '--parent--text'
|
||||
}
|
||||
textColor: '--parent--text',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'ButtonUnstyled',
|
||||
state: ['toggled', 'hover']
|
||||
state: ['toggled', 'hover'],
|
||||
},
|
||||
directives: {
|
||||
textColor: '--parent--text'
|
||||
}
|
||||
textColor: '--parent--text',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'ButtonUnstyled',
|
||||
state: ['toggled', 'focused']
|
||||
state: ['toggled', 'focused'],
|
||||
},
|
||||
directives: {
|
||||
textColor: '--parent--text'
|
||||
}
|
||||
textColor: '--parent--text',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'ButtonUnstyled',
|
||||
state: ['toggled', 'focused', 'hover']
|
||||
state: ['toggled', 'focused', 'hover'],
|
||||
},
|
||||
directives: {
|
||||
textColor: '--parent--text'
|
||||
}
|
||||
textColor: '--parent--text',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Text',
|
||||
parent: {
|
||||
component: 'ButtonUnstyled',
|
||||
state: ['disabled']
|
||||
state: ['disabled'],
|
||||
},
|
||||
directives: {
|
||||
textOpacity: 0.25,
|
||||
textOpacityMode: 'blend'
|
||||
}
|
||||
textOpacityMode: 'blend',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'Icon',
|
||||
parent: {
|
||||
component: 'ButtonUnstyled',
|
||||
state: ['disabled']
|
||||
state: ['disabled'],
|
||||
},
|
||||
directives: {
|
||||
textOpacity: 0.25,
|
||||
textOpacityMode: 'blend'
|
||||
}
|
||||
}
|
||||
]
|
||||
textOpacityMode: 'blend',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,27 @@
|
|||
import _ from 'lodash'
|
||||
import { WSConnectionStatus } from '../../services/api/api.service.js'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import { mapState as mapPiniaState } from 'pinia'
|
||||
import ChatMessage from '../chat_message/chat_message.vue'
|
||||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||
import ChatTitle from '../chat_title/chat_title.vue'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
|
||||
import { WSConnectionStatus } from '../../services/api/api.service.js'
|
||||
import chatService from '../../services/chat_service/chat_service.js'
|
||||
import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
|
||||
import { getScrollPosition, getNewTopPosition, isBottomedOut, 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'
|
||||
import { promiseInterval } from '../../services/promise_interval/promise_interval.js'
|
||||
import ChatMessage from '../chat_message/chat_message.vue'
|
||||
import ChatTitle from '../chat_title/chat_title.vue'
|
||||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||
import {
|
||||
getNewTopPosition,
|
||||
getScrollPosition,
|
||||
isBottomedOut,
|
||||
isScrollable,
|
||||
} from './chat_layout_utils.js'
|
||||
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
|
||||
library.add(
|
||||
faChevronDown,
|
||||
faChevronLeft
|
||||
)
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faChevronDown, faChevronLeft)
|
||||
|
||||
const BOTTOMED_OUT_OFFSET = 10
|
||||
const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 10
|
||||
|
|
@ -31,78 +33,95 @@ const Chat = {
|
|||
components: {
|
||||
ChatMessage,
|
||||
ChatTitle,
|
||||
PostStatusForm
|
||||
PostStatusForm,
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
jumpToBottomButtonVisible: false,
|
||||
hoveredMessageChainId: undefined,
|
||||
lastScrollPosition: {},
|
||||
scrollableContainerHeight: '100%',
|
||||
errorLoadingChat: false,
|
||||
messageRetriers: {}
|
||||
messageRetriers: {},
|
||||
}
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
this.startFetching()
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
},
|
||||
mounted () {
|
||||
mounted() {
|
||||
window.addEventListener('scroll', this.handleScroll)
|
||||
if (typeof document.hidden !== 'undefined') {
|
||||
document.addEventListener('visibilitychange', this.handleVisibilityChange, false)
|
||||
document.addEventListener(
|
||||
'visibilitychange',
|
||||
this.handleVisibilityChange,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.handleResize()
|
||||
})
|
||||
},
|
||||
unmounted () {
|
||||
unmounted() {
|
||||
window.removeEventListener('scroll', this.handleScroll)
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
|
||||
if (typeof document.hidden !== 'undefined')
|
||||
document.removeEventListener(
|
||||
'visibilitychange',
|
||||
this.handleVisibilityChange,
|
||||
false,
|
||||
)
|
||||
this.$store.dispatch('clearCurrentChat')
|
||||
},
|
||||
computed: {
|
||||
recipient () {
|
||||
recipient() {
|
||||
return this.currentChat && this.currentChat.account
|
||||
},
|
||||
recipientId () {
|
||||
recipientId() {
|
||||
return this.$route.params.recipient_id
|
||||
},
|
||||
formPlaceholder () {
|
||||
formPlaceholder() {
|
||||
if (this.recipient) {
|
||||
return this.$t('chats.message_user', { nickname: this.recipient.screen_name_ui })
|
||||
return this.$t('chats.message_user', {
|
||||
nickname: this.recipient.screen_name_ui,
|
||||
})
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
chatViewItems () {
|
||||
chatViewItems() {
|
||||
return chatService.getView(this.currentChatMessageService)
|
||||
},
|
||||
newMessageCount () {
|
||||
return this.currentChatMessageService && this.currentChatMessageService.newMessageCount
|
||||
newMessageCount() {
|
||||
return (
|
||||
this.currentChatMessageService &&
|
||||
this.currentChatMessageService.newMessageCount
|
||||
)
|
||||
},
|
||||
streamingEnabled () {
|
||||
return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
||||
streamingEnabled() {
|
||||
return (
|
||||
this.mergedConfig.useStreamingApi &&
|
||||
this.mastoUserSocketStatus === WSConnectionStatus.JOINED
|
||||
)
|
||||
},
|
||||
...mapGetters([
|
||||
'currentChat',
|
||||
'currentChatMessageService',
|
||||
'findOpenedChatByRecipientId',
|
||||
'mergedConfig'
|
||||
'mergedConfig',
|
||||
]),
|
||||
...mapPiniaState(useInterfaceStore, {
|
||||
mobileLayout: store => store.layoutType === 'mobile'
|
||||
mobileLayout: (store) => store.layoutType === 'mobile',
|
||||
}),
|
||||
...mapState({
|
||||
backendInteractor: state => state.api.backendInteractor,
|
||||
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus,
|
||||
currentUser: state => state.users.currentUser
|
||||
})
|
||||
backendInteractor: (state) => state.api.backendInteractor,
|
||||
mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus,
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
}),
|
||||
},
|
||||
watch: {
|
||||
chatViewItems () {
|
||||
chatViewItems() {
|
||||
// We don't want to scroll to the bottom on a new message when the user is viewing older messages.
|
||||
// Therefore we need to know whether the scroll position was at the bottom before the DOM update.
|
||||
const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET)
|
||||
|
|
@ -115,23 +134,23 @@ const Chat = {
|
|||
$route: function () {
|
||||
this.startFetching()
|
||||
},
|
||||
mastoUserSocketStatus (newValue) {
|
||||
mastoUserSocketStatus(newValue) {
|
||||
if (newValue === WSConnectionStatus.JOINED) {
|
||||
this.fetchChat({ isFirstFetch: true })
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// Used to animate the avatar near the first message of the message chain when any message belonging to the chain is hovered
|
||||
onMessageHover ({ isHovered, messageChainId }) {
|
||||
onMessageHover({ isHovered, messageChainId }) {
|
||||
this.hoveredMessageChainId = isHovered ? messageChainId : undefined
|
||||
},
|
||||
onFilesDropped () {
|
||||
onFilesDropped() {
|
||||
this.$nextTick(() => {
|
||||
this.handleResize()
|
||||
})
|
||||
},
|
||||
handleVisibilityChange () {
|
||||
handleVisibilityChange() {
|
||||
this.$nextTick(() => {
|
||||
if (!document.hidden && this.bottomedOut(BOTTOMED_OUT_OFFSET)) {
|
||||
this.scrollDown({ forceRead: true })
|
||||
|
|
@ -139,7 +158,7 @@ const Chat = {
|
|||
})
|
||||
},
|
||||
// "Sticks" scroll to bottom instead of top, helps with OSK resizing the viewport
|
||||
handleResize (opts = {}) {
|
||||
handleResize(opts = {}) {
|
||||
const { delayed = false } = opts
|
||||
|
||||
if (delayed) {
|
||||
|
|
@ -160,40 +179,56 @@ const Chat = {
|
|||
this.lastScrollPosition = getScrollPosition()
|
||||
})
|
||||
},
|
||||
scrollDown (options = {}) {
|
||||
scrollDown(options = {}) {
|
||||
const { behavior = 'auto', forceRead = false } = options
|
||||
this.$nextTick(() => {
|
||||
window.scrollTo({ top: document.documentElement.scrollHeight, behavior })
|
||||
window.scrollTo({
|
||||
top: document.documentElement.scrollHeight,
|
||||
behavior,
|
||||
})
|
||||
})
|
||||
if (forceRead) {
|
||||
this.readChat()
|
||||
}
|
||||
},
|
||||
readChat () {
|
||||
if (!(this.currentChatMessageService && this.currentChatMessageService.maxId)) { return }
|
||||
if (document.hidden) { return }
|
||||
readChat() {
|
||||
if (
|
||||
!(
|
||||
this.currentChatMessageService && this.currentChatMessageService.maxId
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (document.hidden) {
|
||||
return
|
||||
}
|
||||
const lastReadId = this.currentChatMessageService.maxId
|
||||
this.$store.dispatch('readChat', {
|
||||
id: this.currentChat.id,
|
||||
lastReadId
|
||||
lastReadId,
|
||||
})
|
||||
},
|
||||
bottomedOut (offset) {
|
||||
bottomedOut(offset) {
|
||||
return isBottomedOut(offset)
|
||||
},
|
||||
reachedTop () {
|
||||
reachedTop() {
|
||||
return window.scrollY <= 0
|
||||
},
|
||||
cullOlderCheck () {
|
||||
cullOlderCheck() {
|
||||
window.setTimeout(() => {
|
||||
if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
|
||||
this.$store.dispatch('cullOlderMessages', this.currentChatMessageService.chatId)
|
||||
this.$store.dispatch(
|
||||
'cullOlderMessages',
|
||||
this.currentChatMessageService.chatId,
|
||||
)
|
||||
}
|
||||
}, 5000)
|
||||
},
|
||||
handleScroll: _.throttle(function () {
|
||||
this.lastScrollPosition = getScrollPosition()
|
||||
if (!this.currentChat) { return }
|
||||
if (!this.currentChat) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.reachedTop()) {
|
||||
this.fetchChat({ maxId: this.currentChatMessageService.minId })
|
||||
|
|
@ -213,22 +248,27 @@ const Chat = {
|
|||
this.jumpToBottomButtonVisible = true
|
||||
}
|
||||
}, 200),
|
||||
handleScrollUp (positionBeforeLoading) {
|
||||
handleScrollUp(positionBeforeLoading) {
|
||||
const positionAfterLoading = getScrollPosition()
|
||||
window.scrollTo({
|
||||
top: getNewTopPosition(positionBeforeLoading, positionAfterLoading)
|
||||
top: getNewTopPosition(positionBeforeLoading, positionAfterLoading),
|
||||
})
|
||||
},
|
||||
fetchChat ({ isFirstFetch = false, fetchLatest = false, maxId }) {
|
||||
fetchChat({ isFirstFetch = false, fetchLatest = false, maxId }) {
|
||||
const chatMessageService = this.currentChatMessageService
|
||||
if (!chatMessageService) { return }
|
||||
if (fetchLatest && this.streamingEnabled) { return }
|
||||
if (!chatMessageService) {
|
||||
return
|
||||
}
|
||||
if (fetchLatest && this.streamingEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const chatId = chatMessageService.chatId
|
||||
const fetchOlderMessages = !!maxId
|
||||
const sinceId = fetchLatest && chatMessageService.maxId
|
||||
|
||||
return this.backendInteractor.chatMessages({ id: chatId, maxId, sinceId })
|
||||
return this.backendInteractor
|
||||
.chatMessages({ id: chatId, maxId, sinceId })
|
||||
.then((messages) => {
|
||||
// Clear the current chat in case we're recovering from a ws connection loss.
|
||||
if (isFirstFetch) {
|
||||
|
|
@ -236,28 +276,34 @@ const Chat = {
|
|||
}
|
||||
|
||||
const positionBeforeUpdate = getScrollPosition()
|
||||
this.$store.dispatch('addChatMessages', { chatId, messages }).then(() => {
|
||||
this.$nextTick(() => {
|
||||
if (fetchOlderMessages) {
|
||||
this.handleScrollUp(positionBeforeUpdate)
|
||||
}
|
||||
this.$store
|
||||
.dispatch('addChatMessages', { chatId, messages })
|
||||
.then(() => {
|
||||
this.$nextTick(() => {
|
||||
if (fetchOlderMessages) {
|
||||
this.handleScrollUp(positionBeforeUpdate)
|
||||
}
|
||||
|
||||
// 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() && messages.length > 0) {
|
||||
this.fetchChat({ maxId: this.currentChatMessageService.minId })
|
||||
}
|
||||
// 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() && messages.length > 0) {
|
||||
this.fetchChat({
|
||||
maxId: this.currentChatMessageService.minId,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
async startFetching () {
|
||||
async startFetching() {
|
||||
let chat = this.findOpenedChatByRecipientId(this.recipientId)
|
||||
if (!chat) {
|
||||
try {
|
||||
chat = await this.backendInteractor.getOrCreateChat({ accountId: this.recipientId })
|
||||
chat = await this.backendInteractor.getOrCreateChat({
|
||||
accountId: this.recipientId,
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Error creating or getting a chat', e)
|
||||
this.errorLoadingChat = true
|
||||
|
|
@ -271,13 +317,14 @@ const Chat = {
|
|||
this.doStartFetching()
|
||||
}
|
||||
},
|
||||
doStartFetching () {
|
||||
doStartFetching() {
|
||||
this.$store.dispatch('startFetchingCurrentChat', {
|
||||
fetcher: () => promiseInterval(() => this.fetchChat({ fetchLatest: true }), 5000)
|
||||
fetcher: () =>
|
||||
promiseInterval(() => this.fetchChat({ fetchLatest: true }), 5000),
|
||||
})
|
||||
this.fetchChat({ isFirstFetch: true })
|
||||
},
|
||||
handleAttachmentPosting () {
|
||||
handleAttachmentPosting() {
|
||||
this.$nextTick(() => {
|
||||
this.handleResize()
|
||||
// When the posting form size changes because of a media attachment, we need an extra resize
|
||||
|
|
@ -285,11 +332,11 @@ const Chat = {
|
|||
this.scrollDown({ forceRead: true })
|
||||
})
|
||||
},
|
||||
sendMessage ({ status, media, idempotencyKey }) {
|
||||
sendMessage({ status, media, idempotencyKey }) {
|
||||
const params = {
|
||||
id: this.currentChat.id,
|
||||
content: status,
|
||||
idempotencyKey
|
||||
idempotencyKey,
|
||||
}
|
||||
|
||||
if (media[0]) {
|
||||
|
|
@ -301,52 +348,72 @@ const Chat = {
|
|||
chatId: this.currentChat.id,
|
||||
content: status,
|
||||
userId: this.currentUser.id,
|
||||
idempotencyKey
|
||||
idempotencyKey,
|
||||
})
|
||||
|
||||
this.$store.dispatch('addChatMessages', {
|
||||
chatId: this.currentChat.id,
|
||||
messages: [fakeMessage]
|
||||
}).then(() => {
|
||||
this.handleAttachmentPosting()
|
||||
})
|
||||
this.$store
|
||||
.dispatch('addChatMessages', {
|
||||
chatId: this.currentChat.id,
|
||||
messages: [fakeMessage],
|
||||
})
|
||||
.then(() => {
|
||||
this.handleAttachmentPosting()
|
||||
})
|
||||
|
||||
return this.doSendMessage({ params, fakeMessage, retriesLeft: MAX_RETRIES })
|
||||
return this.doSendMessage({
|
||||
params,
|
||||
fakeMessage,
|
||||
retriesLeft: MAX_RETRIES,
|
||||
})
|
||||
},
|
||||
doSendMessage ({ params, fakeMessage, retriesLeft = MAX_RETRIES }) {
|
||||
doSendMessage({ params, fakeMessage, retriesLeft = MAX_RETRIES }) {
|
||||
if (retriesLeft <= 0) return
|
||||
|
||||
this.backendInteractor.sendChatMessage(params)
|
||||
.then(data => {
|
||||
this.backendInteractor
|
||||
.sendChatMessage(params)
|
||||
.then((data) => {
|
||||
this.$store.dispatch('addChatMessages', {
|
||||
chatId: this.currentChat.id,
|
||||
updateMaxId: false,
|
||||
messages: [{ ...data, fakeId: fakeMessage.id }]
|
||||
messages: [{ ...data, fakeId: fakeMessage.id }],
|
||||
})
|
||||
|
||||
return data
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error('Error sending message', error)
|
||||
this.$store.dispatch('handleMessageError', {
|
||||
chatId: this.currentChat.id,
|
||||
fakeId: fakeMessage.id,
|
||||
isRetry: retriesLeft !== MAX_RETRIES
|
||||
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)))
|
||||
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 () {
|
||||
this.$router.push({ name: 'chats', params: { username: this.currentUser.screen_name } })
|
||||
}
|
||||
}
|
||||
goBack() {
|
||||
this.$router.push({
|
||||
name: 'chats',
|
||||
params: { username: this.currentUser.screen_name },
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default Chat
|
||||
|
|
|
|||
|
|
@ -1,19 +1,13 @@
|
|||
export default {
|
||||
name: 'Chat',
|
||||
selector: '.chat-message-list',
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Link',
|
||||
'Icon',
|
||||
'Avatar',
|
||||
'ChatMessage'
|
||||
],
|
||||
validInnerComponents: ['Text', 'Link', 'Icon', 'Avatar', 'ChatMessage'],
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
background: '--bg',
|
||||
blur: '5px'
|
||||
}
|
||||
}
|
||||
]
|
||||
blur: '5px',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,17 @@ export const getScrollPosition = () => {
|
|||
return {
|
||||
scrollTop: window.scrollY,
|
||||
scrollHeight: document.documentElement.scrollHeight,
|
||||
offsetHeight: window.innerHeight
|
||||
offsetHeight: window.innerHeight,
|
||||
}
|
||||
}
|
||||
|
||||
// A helper function that is used to keep the scroll position fixed as the new elements are added to the top
|
||||
// Takes two scroll positions, before and after the update.
|
||||
export const getNewTopPosition = (previousPosition, newPosition) => {
|
||||
return previousPosition.scrollTop + (newPosition.scrollHeight - previousPosition.scrollHeight)
|
||||
return (
|
||||
previousPosition.scrollTop +
|
||||
(newPosition.scrollHeight - previousPosition.scrollHeight)
|
||||
)
|
||||
}
|
||||
|
||||
export const isBottomedOut = (offset = 0) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { mapState, mapGetters } from 'vuex'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
|
||||
import ChatListItem from '../chat_list_item/chat_list_item.vue'
|
||||
import ChatNew from '../chat_new/chat_new.vue'
|
||||
import List from '../list/list.vue'
|
||||
|
|
@ -7,31 +8,31 @@ const ChatList = {
|
|||
components: {
|
||||
ChatListItem,
|
||||
List,
|
||||
ChatNew
|
||||
ChatNew,
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
}),
|
||||
...mapGetters(['sortedChatList'])
|
||||
...mapGetters(['sortedChatList']),
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
isNew: false
|
||||
isNew: false,
|
||||
}
|
||||
},
|
||||
created () {
|
||||
created() {
|
||||
this.$store.dispatch('fetchChats', { latest: true })
|
||||
},
|
||||
methods: {
|
||||
cancelNewChat () {
|
||||
cancelNewChat() {
|
||||
this.isNew = false
|
||||
this.$store.dispatch('fetchChats', { latest: true })
|
||||
},
|
||||
newChat () {
|
||||
newChat() {
|
||||
this.isNew = true
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default ChatList
|
||||
|
|
|
|||
|
|
@ -1,31 +1,31 @@
|
|||
import { mapState } from 'vuex'
|
||||
import StatusBody from '../status_content/status_content.vue'
|
||||
import fileType from 'src/services/file_type/file_type.service'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
|
||||
import AvatarList from '../avatar_list/avatar_list.vue'
|
||||
import Timeago from '../timeago/timeago.vue'
|
||||
import ChatTitle from '../chat_title/chat_title.vue'
|
||||
import StatusBody from '../status_content/status_content.vue'
|
||||
import Timeago from '../timeago/timeago.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
|
||||
const ChatListItem = {
|
||||
name: 'ChatListItem',
|
||||
props: [
|
||||
'chat'
|
||||
],
|
||||
props: ['chat'],
|
||||
components: {
|
||||
UserAvatar,
|
||||
AvatarList,
|
||||
Timeago,
|
||||
ChatTitle,
|
||||
StatusBody
|
||||
StatusBody,
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
}),
|
||||
attachmentInfo () {
|
||||
if (this.chat.lastMessage.attachments.length === 0) { return }
|
||||
attachmentInfo() {
|
||||
if (this.chat.lastMessage.attachments.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const types = this.chat.lastMessage.attachments.map(file => fileType.fileType(file.mimetype))
|
||||
const types = this.chat.lastMessage.attachments.map((file) => file.type)
|
||||
if (types.includes('video')) {
|
||||
return this.$t('file_type.video')
|
||||
} else if (types.includes('audio')) {
|
||||
|
|
@ -36,34 +36,36 @@ const ChatListItem = {
|
|||
return this.$t('file_type.file')
|
||||
}
|
||||
},
|
||||
messageForStatusContent () {
|
||||
messageForStatusContent() {
|
||||
const message = this.chat.lastMessage
|
||||
const messageEmojis = message ? message.emojis : []
|
||||
const isYou = message && message.account_id === this.currentUser.id
|
||||
const content = message ? (this.attachmentInfo || message.content) : ''
|
||||
const messagePreview = isYou ? `<i>${this.$t('chats.you')}</i> ${content}` : content
|
||||
const content = message ? this.attachmentInfo || message.content : ''
|
||||
const messagePreview = isYou
|
||||
? `<i>${this.$t('chats.you')}</i> ${content}`
|
||||
: content
|
||||
return {
|
||||
summary: '',
|
||||
emojis: messageEmojis,
|
||||
raw_html: messagePreview,
|
||||
text: messagePreview,
|
||||
attachments: []
|
||||
attachments: [],
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openChat () {
|
||||
openChat() {
|
||||
if (this.chat.id) {
|
||||
this.$router.push({
|
||||
name: 'chat',
|
||||
params: {
|
||||
username: this.currentUser.screen_name,
|
||||
recipient_id: this.chat.account.id
|
||||
}
|
||||
recipient_id: this.chat.account.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default ChatListItem
|
||||
|
|
|
|||
|
|
@ -1,24 +1,23 @@
|
|||
import { mapState, mapGetters } from 'vuex'
|
||||
import { mapState as mapPiniaState } from 'pinia'
|
||||
import Popover from '../popover/popover.vue'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
|
||||
import Attachment from '../attachment/attachment.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import ChatMessageDate from '../chat_message_date/chat_message_date.vue'
|
||||
import Gallery from '../gallery/gallery.vue'
|
||||
import LinkPreview from '../link-preview/link-preview.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
import StatusContent from '../status_content/status_content.vue'
|
||||
import ChatMessageDate from '../chat_message_date/chat_message_date.vue'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faTimes,
|
||||
faEllipsisH
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
|
||||
library.add(
|
||||
faTimes,
|
||||
faEllipsisH
|
||||
)
|
||||
import { useInstanceStore } from 'src/stores/instance.js'
|
||||
import { useInterfaceStore } from 'src/stores/interface'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faEllipsisH, faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faTimes, faEllipsisH)
|
||||
|
||||
const ChatMessage = {
|
||||
name: 'ChatMessage',
|
||||
|
|
@ -27,7 +26,7 @@ const ChatMessage = {
|
|||
'edited',
|
||||
'noHeading',
|
||||
'chatViewItem',
|
||||
'hoveredMessageChain'
|
||||
'hoveredMessageChain',
|
||||
],
|
||||
emits: ['hover'],
|
||||
components: {
|
||||
|
|
@ -38,73 +37,82 @@ const ChatMessage = {
|
|||
Gallery,
|
||||
LinkPreview,
|
||||
ChatMessageDate,
|
||||
UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
|
||||
UserPopover: defineAsyncComponent(
|
||||
() => import('../user_popover/user_popover.vue'),
|
||||
),
|
||||
},
|
||||
computed: {
|
||||
// Returns HH:MM (hours and minutes) in local time.
|
||||
createdAt () {
|
||||
createdAt() {
|
||||
const time = this.chatViewItem.data.created_at
|
||||
return time.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', hour12: false })
|
||||
return time.toLocaleTimeString('en', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
})
|
||||
},
|
||||
isCurrentUser () {
|
||||
isCurrentUser() {
|
||||
return this.message.account_id === this.currentUser.id
|
||||
},
|
||||
message () {
|
||||
message() {
|
||||
return this.chatViewItem.data
|
||||
},
|
||||
isMessage () {
|
||||
isMessage() {
|
||||
return this.chatViewItem.type === 'message'
|
||||
},
|
||||
messageForStatusContent () {
|
||||
messageForStatusContent() {
|
||||
return {
|
||||
summary: '',
|
||||
emojis: this.message.emojis,
|
||||
raw_html: this.message.content || '',
|
||||
text: this.message.content || '',
|
||||
attachments: this.message.attachments
|
||||
attachments: this.message.attachments,
|
||||
}
|
||||
},
|
||||
hasAttachment () {
|
||||
hasAttachment() {
|
||||
return this.message.attachments.length > 0
|
||||
},
|
||||
...mapPiniaState(useInterfaceStore, {
|
||||
betterShadow: store => store.browserSupport.cssFilter
|
||||
betterShadow: (store) => store.browserSupport.cssFilter,
|
||||
}),
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser,
|
||||
restrictedNicknames: state => state.instance.restrictedNicknames
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
restrictedNicknames: (state) => useInstanceStore().restrictedNicknames,
|
||||
}),
|
||||
popoverMarginStyle () {
|
||||
popoverMarginStyle() {
|
||||
if (this.isCurrentUser) {
|
||||
return {}
|
||||
} else {
|
||||
return { left: 50 }
|
||||
}
|
||||
},
|
||||
...mapGetters(['mergedConfig', 'findUser'])
|
||||
...mapPiniaState(useMergedConfigStore, ['mergedConfig', 'findUser']),
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
hovered: false,
|
||||
menuOpened: false
|
||||
menuOpened: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onHover (bool) {
|
||||
this.$emit('hover', { isHovered: bool, messageChainId: this.chatViewItem.messageChainId })
|
||||
onHover(bool) {
|
||||
this.$emit('hover', {
|
||||
isHovered: bool,
|
||||
messageChainId: this.chatViewItem.messageChainId,
|
||||
})
|
||||
},
|
||||
async deleteMessage () {
|
||||
async deleteMessage() {
|
||||
const confirmed = window.confirm(this.$t('chats.delete_confirm'))
|
||||
if (confirmed) {
|
||||
await this.$store.dispatch('deleteChatMessage', {
|
||||
messageId: this.chatViewItem.data.id,
|
||||
chatId: this.chatViewItem.data.chat_id
|
||||
chatId: this.chatViewItem.data.chat_id,
|
||||
})
|
||||
}
|
||||
this.hovered = false
|
||||
this.menuOpened = false
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default ChatMessage
|
||||
|
|
|
|||
|
|
@ -2,29 +2,21 @@ export default {
|
|||
name: 'ChatMessage',
|
||||
selector: '.chat-message',
|
||||
variants: {
|
||||
outgoing: '.outgoing'
|
||||
outgoing: '.outgoing',
|
||||
},
|
||||
validInnerComponents: [
|
||||
'Text',
|
||||
'Icon',
|
||||
'Border',
|
||||
'Button',
|
||||
'RichContent',
|
||||
'Attachment',
|
||||
'PollGraph'
|
||||
],
|
||||
validInnerComponents: ['Text', 'Icon', 'Border', 'PollGraph'],
|
||||
defaultRules: [
|
||||
{
|
||||
directives: {
|
||||
background: '--bg, 2',
|
||||
backgroundNoCssColor: 'yes'
|
||||
}
|
||||
backgroundNoCssColor: 'yes',
|
||||
},
|
||||
},
|
||||
{
|
||||
variant: 'outgoing',
|
||||
directives: {
|
||||
background: '--bg, 5'
|
||||
}
|
||||
}
|
||||
]
|
||||
background: '--bg, 5',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,16 +11,19 @@ export default {
|
|||
name: 'Timeago',
|
||||
props: ['date'],
|
||||
computed: {
|
||||
displayDate () {
|
||||
displayDate() {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
if (this.date.getTime() === today.getTime()) {
|
||||
return this.$t('display_date.today')
|
||||
} else {
|
||||
return this.date.toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale), { day: 'numeric', month: 'long' })
|
||||
return this.date.toLocaleDateString(
|
||||
localeService.internalToBrowserLocale(this.$i18n.locale),
|
||||
{ day: 'numeric', month: 'long' },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,39 +1,35 @@
|
|||
import { mapState, mapGetters } from 'vuex'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
|
||||
import BasicUserCard from '../basic_user_card/basic_user_card.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
|
||||
)
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faChevronLeft, faSearch } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faSearch, faChevronLeft)
|
||||
|
||||
const chatNew = {
|
||||
components: {
|
||||
BasicUserCard,
|
||||
UserAvatar
|
||||
UserAvatar,
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
suggestions: [],
|
||||
userIds: [],
|
||||
loading: false,
|
||||
query: ''
|
||||
query: '',
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
async created() {
|
||||
const { chats } = await this.backendInteractor.chats()
|
||||
chats.forEach(chat => this.suggestions.push(chat.account))
|
||||
chats.forEach((chat) => this.suggestions.push(chat.account))
|
||||
},
|
||||
computed: {
|
||||
users () {
|
||||
return this.userIds.map(userId => this.findUser(userId))
|
||||
users() {
|
||||
return this.userIds.map((userId) => this.findUser(userId))
|
||||
},
|
||||
availableUsers () {
|
||||
availableUsers() {
|
||||
if (this.query.length !== 0) {
|
||||
return this.users
|
||||
} else {
|
||||
|
|
@ -41,29 +37,29 @@ const chatNew = {
|
|||
}
|
||||
},
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser,
|
||||
backendInteractor: state => state.api.backendInteractor
|
||||
currentUser: (state) => state.users.currentUser,
|
||||
backendInteractor: (state) => state.api.backendInteractor,
|
||||
}),
|
||||
...mapGetters(['findUser'])
|
||||
...mapGetters(['findUser']),
|
||||
},
|
||||
methods: {
|
||||
goBack () {
|
||||
goBack() {
|
||||
this.$emit('cancel')
|
||||
},
|
||||
goToChat (user) {
|
||||
goToChat(user) {
|
||||
this.$router.push({ name: 'chat', params: { recipient_id: user.id } })
|
||||
},
|
||||
onInput () {
|
||||
onInput() {
|
||||
this.search(this.query)
|
||||
},
|
||||
addUser (user) {
|
||||
addUser(user) {
|
||||
this.selectedUserIds.push(user.id)
|
||||
this.query = ''
|
||||
},
|
||||
removeUser (userId) {
|
||||
this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId)
|
||||
removeUser(userId) {
|
||||
this.selectedUserIds = this.selectedUserIds.filter((id) => id !== userId)
|
||||
},
|
||||
search (query) {
|
||||
search(query) {
|
||||
if (!query) {
|
||||
this.loading = false
|
||||
return
|
||||
|
|
@ -71,13 +67,14 @@ const chatNew = {
|
|||
|
||||
this.loading = true
|
||||
this.userIds = []
|
||||
this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts' })
|
||||
.then(data => {
|
||||
this.$store
|
||||
.dispatch('search', { q: query, resolve: true, type: 'accounts' })
|
||||
.then((data) => {
|
||||
this.loading = false
|
||||
this.userIds = data.accounts.map(a => a.id)
|
||||
this.userIds = data.accounts.map((a) => a.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default chatNew
|
||||
|
|
|
|||
|
|
@ -1,23 +1,24 @@
|
|||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
|
||||
export default {
|
||||
name: 'ChatTitle',
|
||||
components: {
|
||||
UserAvatar,
|
||||
RichContent,
|
||||
UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue'))
|
||||
UserPopover: defineAsyncComponent(
|
||||
() => import('../user_popover/user_popover.vue'),
|
||||
),
|
||||
},
|
||||
props: [
|
||||
'user', 'withAvatar'
|
||||
],
|
||||
props: ['user', 'withAvatar'],
|
||||
computed: {
|
||||
title () {
|
||||
title() {
|
||||
return this.user ? this.user.screen_name_ui : ''
|
||||
},
|
||||
htmlTitle () {
|
||||
htmlTitle() {
|
||||
return this.user ? this.user.name_html : ''
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
:title="'@'+(user && user.screen_name_ui)"
|
||||
:html="htmlTitle"
|
||||
:emoji="user.emoji || []"
|
||||
:is-local="user.is_local"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<label
|
||||
class="checkbox"
|
||||
:class="[{ disabled, indeterminate, 'indeterminate-fix': indeterminateTransitionFix }, radio ? '-radio' : '-checkbox']"
|
||||
:class="[{ ['-disabled']: disabled, indeterminate, 'indeterminate-fix': indeterminateTransitionFix }, radio ? '-radio' : '-checkbox']"
|
||||
>
|
||||
<span
|
||||
v-if="!!$slots.before"
|
||||
|
|
@ -36,30 +36,25 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
props: [
|
||||
'radio',
|
||||
'modelValue',
|
||||
'indeterminate',
|
||||
'disabled'
|
||||
],
|
||||
props: ['radio', 'modelValue', 'indeterminate', 'disabled'],
|
||||
emits: ['update:modelValue'],
|
||||
data: (vm) => ({
|
||||
indeterminateTransitionFix: vm.indeterminate
|
||||
indeterminateTransitionFix: vm.indeterminate,
|
||||
}),
|
||||
watch: {
|
||||
indeterminate (e) {
|
||||
indeterminate(e) {
|
||||
if (e) {
|
||||
this.indeterminateTransitionFix = true
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onTransitionEnd () {
|
||||
onTransitionEnd() {
|
||||
if (!this.indeterminate) {
|
||||
this.indeterminateTransitionFix = false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -123,7 +118,7 @@ export default {
|
|||
|
||||
.disabled {
|
||||
.checkbox-indicator::before {
|
||||
background-color: var(--background);
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,27 @@
|
|||
.color-input {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
max-width: 10em;
|
||||
|
||||
&.-compact {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: 1 1 auto;
|
||||
grid-area: label;
|
||||
}
|
||||
|
||||
.opt {
|
||||
grid-area: checkbox;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
&-field.input {
|
||||
display: inline-flex;
|
||||
flex: 0 0 0;
|
||||
max-width: 9em;
|
||||
flex: 1 1 10em;
|
||||
max-width: 10em;
|
||||
grid-area: input;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
input {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
class="color-input style-control"
|
||||
:class="{ disabled: !present || disabled }"
|
||||
:class="{ disabled: !present || disabled, '-compact': compact }"
|
||||
>
|
||||
<label
|
||||
:for="name"
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
/>
|
||||
<div
|
||||
class="input color-input-field"
|
||||
:class="{ disabled: !present || disabled }"
|
||||
:class="{ disabled: !present || disabled, unstyled }"
|
||||
>
|
||||
<input
|
||||
:id="name + '-t'"
|
||||
|
|
@ -64,86 +64,95 @@
|
|||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||
import { throttle } from 'lodash'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faEyeDropper
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
|
||||
library.add(
|
||||
faEyeDropper
|
||||
)
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { faEyeDropper } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(faEyeDropper)
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Checkbox
|
||||
Checkbox,
|
||||
},
|
||||
props: {
|
||||
// Name of color, used for identifying
|
||||
name: {
|
||||
required: true,
|
||||
type: String
|
||||
type: String,
|
||||
},
|
||||
// Readable label
|
||||
label: {
|
||||
required: true,
|
||||
type: String
|
||||
required: false,
|
||||
type: String,
|
||||
},
|
||||
// use unstyled, uh, style
|
||||
unstyled: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
},
|
||||
// Color value, should be required but vue cannot tell the difference
|
||||
// between "property missing" and "property set to undefined"
|
||||
modelValue: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: undefined
|
||||
default: undefined,
|
||||
},
|
||||
// Color fallback to use when value is not defeind
|
||||
fallback: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: undefined
|
||||
default: undefined,
|
||||
},
|
||||
// Disable the control
|
||||
disabled: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: false,
|
||||
},
|
||||
// Show "optional" tickbox, for when value might become mandatory
|
||||
showOptionalCheckbox: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: true
|
||||
default: true,
|
||||
},
|
||||
// Force "optional" tickbox to hide
|
||||
hideOptionalCheckbox: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
default: false,
|
||||
},
|
||||
compact: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
computed: {
|
||||
present () {
|
||||
present() {
|
||||
return typeof this.modelValue !== 'undefined'
|
||||
},
|
||||
validColor () {
|
||||
validColor() {
|
||||
return hex2rgb(this.modelValue || this.fallback)
|
||||
},
|
||||
transparentColor () {
|
||||
transparentColor() {
|
||||
return this.modelValue === 'transparent'
|
||||
},
|
||||
computedColor () {
|
||||
return this.modelValue && (this.modelValue.startsWith('--') || this.modelValue.startsWith('$'))
|
||||
}
|
||||
computedColor() {
|
||||
return (
|
||||
this.modelValue &&
|
||||
(this.modelValue.startsWith('--') || this.modelValue.startsWith('$'))
|
||||
)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateValue: throttle(function (value) {
|
||||
this.$emit('update:modelValue', value)
|
||||
}, 100)
|
||||
}
|
||||
}, 100),
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" src="./color_input.scss"></style>
|
||||
|
|
|
|||
87
src/components/component_preview/component_preview.js
Normal file
87
src/components/component_preview/component_preview.js
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import ColorInput from 'src/components/color_input/color_input.vue'
|
||||
|
||||
import genRandomSeed from 'src/services/random_seed/random_seed.service.js'
|
||||
import {
|
||||
adoptStyleSheets,
|
||||
createStyleSheet,
|
||||
} from 'src/services/style_setter/style_setter.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Checkbox,
|
||||
ColorInput,
|
||||
},
|
||||
props: [
|
||||
'shadow',
|
||||
'shadowControl',
|
||||
'previewClass',
|
||||
'previewStyle',
|
||||
'previewCss',
|
||||
'disabled',
|
||||
'invalid',
|
||||
'noColorControl',
|
||||
],
|
||||
emits: ['update:shadow'],
|
||||
data() {
|
||||
return {
|
||||
colorOverride: undefined,
|
||||
lightGrid: false,
|
||||
zoom: 100,
|
||||
randomSeed: genRandomSeed(),
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.update()
|
||||
},
|
||||
computed: {
|
||||
hideControls() {
|
||||
return typeof this.shadow === 'string'
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
previewCss() {
|
||||
this.update()
|
||||
},
|
||||
previewStyle() {
|
||||
this.update()
|
||||
},
|
||||
zoom() {
|
||||
this.update()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateProperty(axis, value) {
|
||||
this.$emit('update:shadow', { axis, value: Number(value) })
|
||||
},
|
||||
update() {
|
||||
const sheet = createStyleSheet('style-component-preview', 90)
|
||||
|
||||
sheet.clear()
|
||||
|
||||
const result = [this.previewCss]
|
||||
if (this.colorOverride) result.push(`--background: ${this.colorOverride}`)
|
||||
|
||||
const styleRule = [
|
||||
'#component-preview-',
|
||||
this.randomSeed,
|
||||
' {\n',
|
||||
'.preview-block {\n',
|
||||
`zoom: ${this.zoom / 100};`,
|
||||
this.previewStyle,
|
||||
'\n}',
|
||||
'\n}',
|
||||
].join('')
|
||||
|
||||
sheet.addRule(styleRule)
|
||||
sheet.addRule(
|
||||
['#component-preview-', this.randomSeed, ' {\n', ...result, '\n}'].join(
|
||||
'',
|
||||
),
|
||||
)
|
||||
|
||||
sheet.ready = true
|
||||
adoptStyleSheets()
|
||||
},
|
||||
},
|
||||
}
|
||||
151
src/components/component_preview/component_preview.scss
Normal file
151
src/components/component_preview/component_preview.scss
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
.ComponentPreview {
|
||||
display: grid;
|
||||
grid-template-columns: 1em 1fr 1fr 1em;
|
||||
grid-template-rows: 2em 1fr 1fr 1fr 1em 2em max-content;
|
||||
grid-template-areas:
|
||||
"header header header header "
|
||||
"preview preview preview y-slide"
|
||||
"preview preview preview y-slide"
|
||||
"preview preview preview y-slide"
|
||||
"x-slide x-slide x-slide . "
|
||||
"x-num x-num y-num y-num "
|
||||
"assists assists assists assists";
|
||||
grid-gap: 0.5em;
|
||||
|
||||
&:not(.-shadow-controls) {
|
||||
grid-template-areas:
|
||||
"header header header header "
|
||||
"preview preview preview y-slide"
|
||||
"preview preview preview y-slide"
|
||||
"preview preview preview y-slide"
|
||||
"assists assists assists assists";
|
||||
grid-template-rows: 2em 1fr 1fr 1fr max-content;
|
||||
}
|
||||
|
||||
.header {
|
||||
grid-area: header;
|
||||
place-self: baseline center;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
.invalid-container {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center center;
|
||||
background-color: rgb(100 0 0 / 50%);
|
||||
|
||||
.alert {
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.assists {
|
||||
grid-area: assists;
|
||||
display: grid;
|
||||
grid-auto-flow: row;
|
||||
grid-auto-rows: 2em;
|
||||
grid-gap: 0.5em;
|
||||
}
|
||||
|
||||
.input-light-grid {
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.input-number {
|
||||
min-width: 2em;
|
||||
}
|
||||
|
||||
.x-shift-number {
|
||||
grid-area: x-num;
|
||||
justify-self: right;
|
||||
}
|
||||
|
||||
.y-shift-number {
|
||||
grid-area: y-num;
|
||||
justify-self: left;
|
||||
}
|
||||
|
||||
.x-shift-number,
|
||||
.y-shift-number {
|
||||
input {
|
||||
max-width: 4em;
|
||||
}
|
||||
}
|
||||
|
||||
.x-shift-slider {
|
||||
grid-area: x-slide;
|
||||
height: auto;
|
||||
align-self: start;
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
.y-shift-slider {
|
||||
grid-area: y-slide;
|
||||
writing-mode: vertical-lr;
|
||||
justify-self: left;
|
||||
min-height: 10em;
|
||||
}
|
||||
|
||||
.x-shift-slider,
|
||||
.y-shift-slider {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.preview-window {
|
||||
--__grid-color1: rgb(102 102 102);
|
||||
--__grid-color2: rgb(153 153 153);
|
||||
--__grid-color1-disabled: rgb(102 102 102 / 20%);
|
||||
--__grid-color2-disabled: rgb(153 153 153 / 20%);
|
||||
|
||||
&.-light-grid {
|
||||
--__grid-color1: rgb(205 205 205);
|
||||
--__grid-color2: rgb(255 255 255);
|
||||
--__grid-color1-disabled: rgb(205 205 205 / 20%);
|
||||
--__grid-color2-disabled: rgb(255 255 255 / 20%);
|
||||
}
|
||||
|
||||
position: relative;
|
||||
grid-area: preview;
|
||||
aspect-ratio: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 10em;
|
||||
min-height: 10em;
|
||||
background-color: var(--__grid-color2);
|
||||
background-image:
|
||||
linear-gradient(45deg, var(--__grid-color1) 25%, transparent 25%),
|
||||
linear-gradient(-45deg, var(--__grid-color1) 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, var(--__grid-color1) 75%),
|
||||
linear-gradient(-45deg, transparent 75%, var(--__grid-color1) 75%);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 0 10px, 10px -10px, -10px 0;
|
||||
border-radius: var(--roundness);
|
||||
|
||||
&.disabled {
|
||||
background-color: var(--__grid-color2-disabled);
|
||||
background-image:
|
||||
linear-gradient(45deg, var(--__grid-color1-disabled) 25%, transparent 25%),
|
||||
linear-gradient(-45deg, var(--__grid-color1-disabled) 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, var(--__grid-color1-disabled) 75%),
|
||||
linear-gradient(-45deg, transparent 75%, var(--__grid-color1-disabled) 75%);
|
||||
}
|
||||
|
||||
.preview-block {
|
||||
background: var(--background, var(--bg));
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 33%;
|
||||
min-height: 33%;
|
||||
max-width: 80%;
|
||||
max-height: 80%;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: var(--border);
|
||||
border-radius: var(--roundness);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue