mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-16 01:09:55 +00:00
Compare commits
779 Commits
v4.4.5
...
revert-320
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
855c3be3d7 | ||
|
|
805a19e288 | ||
|
|
e2a8b5c30f | ||
|
|
9db7ad670f | ||
|
|
df72a2dbbe | ||
|
|
ea3f6ce2e5 | ||
|
|
7430d399b5 | ||
|
|
26e59bec15 | ||
|
|
500619ba4c | ||
|
|
ee64228963 | ||
|
|
351dc60a2f | ||
|
|
b15b07606c | ||
|
|
7821634df9 | ||
|
|
28be5a199f | ||
|
|
4eb04e96d0 | ||
|
|
3a81ee8f5b | ||
|
|
29d9f81e42 | ||
|
|
059bf1e980 | ||
|
|
23a69e3bd7 | ||
|
|
e1f7847b64 | ||
|
|
6cbc857ee0 | ||
|
|
e097dbd25d | ||
|
|
ba724f80b1 | ||
|
|
4ba1ad75e0 | ||
|
|
986e6b1505 | ||
|
|
a548c5c29b | ||
|
|
37cec638df | ||
|
|
82f5901a3f | ||
|
|
6bd90940b6 | ||
|
|
24ddf80ff7 | ||
|
|
8e6c0fdf5a | ||
|
|
bdadfe60cb | ||
|
|
64895e5f6d | ||
|
|
dc808054d2 | ||
|
|
4b1d7490d2 | ||
|
|
107d2a9f93 | ||
|
|
6c2c485638 | ||
|
|
99d5af9914 | ||
|
|
45a044cad0 | ||
|
|
63a2ef6274 | ||
|
|
91e666bcaa | ||
|
|
da272d13e2 | ||
|
|
28264c5c86 | ||
|
|
0ac2d11ac8 | ||
|
|
5f2091d8d2 | ||
|
|
e473583da0 | ||
|
|
4a99025d02 | ||
|
|
5398dd9ee1 | ||
|
|
4abc442add | ||
|
|
1f2a84b3d0 | ||
|
|
00cc6c40eb | ||
|
|
187afeaee7 | ||
|
|
b1c91d885a | ||
|
|
66afc13b7f | ||
|
|
6689040fc6 | ||
|
|
9bd151808c | ||
|
|
a42258eca2 | ||
|
|
a95deddeab | ||
|
|
3b6624f97e | ||
|
|
67c332798e | ||
|
|
880e662a60 | ||
|
|
854aaec6fe | ||
|
|
f316cd51c8 | ||
|
|
ea7371c183 | ||
|
|
4ff215f19b | ||
|
|
938b54dff0 | ||
|
|
a0a56a4c7b | ||
|
|
e45fd85436 | ||
|
|
b6bc42aaa6 | ||
|
|
23efdf12cb | ||
|
|
1e3832f0ec | ||
|
|
98a148dced | ||
|
|
c9bb7e9b26 | ||
|
|
af360df59b | ||
|
|
48005c55ff | ||
|
|
e2171f5083 | ||
|
|
16a6e5c118 | ||
|
|
c368a16dc1 | ||
|
|
bb3f9ed13b | ||
|
|
d72ae0dd70 | ||
|
|
16e479e3ec | ||
|
|
90765342a3 | ||
|
|
1e70fbfc52 | ||
|
|
21cf2673f2 | ||
|
|
3a31ef11cc | ||
|
|
0a6895f8b9 | ||
|
|
210786efd8 | ||
|
|
085e9ea676 | ||
|
|
db0cd9489c | ||
|
|
fbf093a87f | ||
|
|
2664bb628b | ||
|
|
8a6ef2ebdf | ||
|
|
de3692ca00 | ||
|
|
72fff2e54f | ||
|
|
dfef7d9407 | ||
|
|
ff03938808 | ||
|
|
3055afd1d2 | ||
|
|
12d61e0d84 | ||
|
|
322b6d8dd5 | ||
|
|
f659190e18 | ||
|
|
26193a119b | ||
|
|
d9b70bbde1 | ||
|
|
d1402af0b2 | ||
|
|
41ab10f88c | ||
|
|
96d0f6f049 | ||
|
|
eeb84cbf76 | ||
|
|
882a0a1040 | ||
|
|
504f221ec8 | ||
|
|
bce8469c99 | ||
|
|
58016b9c33 | ||
|
|
cc52833296 | ||
|
|
6c5a4702d9 | ||
|
|
38fa0102c1 | ||
|
|
943cdc5b21 | ||
|
|
887e982aa2 | ||
|
|
e4bb0fc43a | ||
|
|
f5591346cc | ||
|
|
94dcf8c96c | ||
|
|
4702e369e9 | ||
|
|
681a9cfda1 | ||
|
|
38c0c9ba3b | ||
|
|
93c977eb3d | ||
|
|
8e46252862 | ||
|
|
85f414da8a | ||
|
|
5da295fa4d | ||
|
|
06803422da | ||
|
|
82b26603fe | ||
|
|
30b31a89e6 | ||
|
|
b59e06fba7 | ||
|
|
c708f06621 | ||
|
|
7cb56c7bc7 | ||
|
|
268ede0391 | ||
|
|
d471514231 | ||
|
|
9f97df84da | ||
|
|
45a996f12b | ||
|
|
946721fd0b | ||
|
|
84d1ba980b | ||
|
|
872044484c | ||
|
|
e35dfbfdef | ||
|
|
aacc829dc6 | ||
|
|
c198d04a62 | ||
|
|
261edc8b87 | ||
|
|
5e0d346e14 | ||
|
|
128a69c1f4 | ||
|
|
f7289b251f | ||
|
|
75f78244d5 | ||
|
|
b60ee191ac | ||
|
|
8a0d0025ff | ||
|
|
2314583606 | ||
|
|
cf7d1822f4 | ||
|
|
3f7f766e47 | ||
|
|
c799d73484 | ||
|
|
1bb92520d6 | ||
|
|
4b85fcabff | ||
|
|
838d88cb8f | ||
|
|
4388fa1495 | ||
|
|
435f536375 | ||
|
|
50acfc48b9 | ||
|
|
acb2c1a635 | ||
|
|
8ce1c8ab7c | ||
|
|
28d479ceb3 | ||
|
|
1d6226f66e | ||
|
|
06f45a2046 | ||
|
|
841212710b | ||
|
|
ffaa672fd6 | ||
|
|
eb118d8523 | ||
|
|
e1dc960219 | ||
|
|
377e870348 | ||
|
|
66d73fc213 | ||
|
|
e2cbef7edb | ||
|
|
beca0faed0 | ||
|
|
cf20c5db9c | ||
|
|
3c79c512fe | ||
|
|
1c8990927a | ||
|
|
13d970b5ae | ||
|
|
e7d7da9511 | ||
|
|
2347354bba | ||
|
|
9d0e73d308 | ||
|
|
3ca4793e68 | ||
|
|
e9e107c5a5 | ||
|
|
c0af4581aa | ||
|
|
0153b49ef7 | ||
|
|
24fb862a65 | ||
|
|
b99c94537b | ||
|
|
a5fbe2f5c1 | ||
|
|
65b4a0a6f1 | ||
|
|
9463a31107 | ||
|
|
7ddb8814e1 | ||
|
|
a317bde6b8 | ||
|
|
31df462f51 | ||
|
|
6335f3263d | ||
|
|
cda2119af8 | ||
|
|
fc3b4d9cc9 | ||
|
|
7441f21b18 | ||
|
|
6fc9febb47 | ||
|
|
1b664cf20d | ||
|
|
05a655f33e | ||
|
|
3efba15b3c | ||
|
|
350a802851 | ||
|
|
de09e33c92 | ||
|
|
9a2be25199 | ||
|
|
279405f2a7 | ||
|
|
497bfbc483 | ||
|
|
dd6bd681ea | ||
|
|
0d93801bde | ||
|
|
e7c30cd072 | ||
|
|
42be0ca0eb | ||
|
|
d7d83d44e6 | ||
|
|
5cb85acf2b | ||
|
|
27969c3077 | ||
|
|
4c2a2c27c1 | ||
|
|
14cb5ff881 | ||
|
|
372b746dbf | ||
|
|
705ea99136 | ||
|
|
bc952ebde9 | ||
|
|
c1542643f5 | ||
|
|
624c024766 | ||
|
|
927468bce5 | ||
|
|
eeb80a926c | ||
|
|
6a71fffadf | ||
|
|
a06e96f99e | ||
|
|
160c29b073 | ||
|
|
a5662c71db | ||
|
|
c5867cc98d | ||
|
|
ae6613db55 | ||
|
|
aadbfbb315 | ||
|
|
1db5d85d20 | ||
|
|
d5b959a2ca | ||
|
|
75fca715e9 | ||
|
|
5d9a9c76fb | ||
|
|
0e5cac8c8b | ||
|
|
e665cc68f4 | ||
|
|
bdff970a5e | ||
|
|
bf860c39c2 | ||
|
|
b829d89757 | ||
|
|
5844fd3e3d | ||
|
|
f51b733109 | ||
|
|
2138f3e40f | ||
|
|
fb736eaed5 | ||
|
|
786891c333 | ||
|
|
7fc884ba00 | ||
|
|
6e09dd10a7 | ||
|
|
314d5f0d7a | ||
|
|
15401e6988 | ||
|
|
6f8187e595 | ||
|
|
ab698ff521 | ||
|
|
f21d9f64db | ||
|
|
e95e6064ae | ||
|
|
70da14871f | ||
|
|
c3b1dc4b1c | ||
|
|
65723e791f | ||
|
|
a0763483b0 | ||
|
|
bf3c53661a | ||
|
|
6abe6cdef5 | ||
|
|
40242fafee | ||
|
|
ee4b0a223c | ||
|
|
3c578dbdcd | ||
|
|
4fa203e69e | ||
|
|
8b7685d956 | ||
|
|
4ecbaea8bb | ||
|
|
5f1e6a5886 | ||
|
|
97d80265b4 | ||
|
|
d93572ea90 | ||
|
|
3b6afadf55 | ||
|
|
5db1bbef69 | ||
|
|
c1159fa712 | ||
|
|
03e07d88d7 | ||
|
|
d880f397df | ||
|
|
3ab2d14782 | ||
|
|
229cbc6a24 | ||
|
|
dccd29fe25 | ||
|
|
e109917119 | ||
|
|
b6cd32281f | ||
|
|
971d8af8e3 | ||
|
|
9c662f83d2 | ||
|
|
a68dc4ab7a | ||
|
|
e7890c0153 | ||
|
|
9f710449db | ||
|
|
884d7996b1 | ||
|
|
02de05dc27 | ||
|
|
9c55b2fbe4 | ||
|
|
a6a0d982ef | ||
|
|
c2b7b28919 | ||
|
|
5cb7dfafcc | ||
|
|
35a06319fa | ||
|
|
6f34db9bb7 | ||
|
|
3f1d78f3c6 | ||
|
|
c00ed9c913 | ||
|
|
b1e2f70934 | ||
|
|
66f5ad42e2 | ||
|
|
8dcd195527 | ||
|
|
f825b0cfa4 | ||
|
|
0710138578 | ||
|
|
7e25f6b606 | ||
|
|
4180f754d0 | ||
|
|
6ad0ddebe4 | ||
|
|
676300789e | ||
|
|
3c9bde31f7 | ||
|
|
a8166d28ed | ||
|
|
460043a969 | ||
|
|
4f41b7c089 | ||
|
|
33085d9cf5 | ||
|
|
a17db258ea | ||
|
|
c2c0b4f347 | ||
|
|
24e2bd2d6c | ||
|
|
7fcd9184d2 | ||
|
|
2b00895179 | ||
|
|
e8fe33f273 | ||
|
|
4738a18e52 | ||
|
|
3c17ccab37 | ||
|
|
496370801a | ||
|
|
f3a932d8a1 | ||
|
|
7aba79ade9 | ||
|
|
7a76f71d99 | ||
|
|
012450e87d | ||
|
|
d8c07be021 | ||
|
|
2560242972 | ||
|
|
94ad088482 | ||
|
|
b771fc0880 | ||
|
|
61a21d6a36 | ||
|
|
d85743576c | ||
|
|
a1ca52ed8f | ||
|
|
95709ce88b | ||
|
|
a44b4e220e | ||
|
|
c0a48f082c | ||
|
|
f6e822e1f5 | ||
|
|
3fd629cf84 | ||
|
|
20bc34ca52 | ||
|
|
3cc6f8813a | ||
|
|
f7cd193390 | ||
|
|
e9c1ffeb9a | ||
|
|
bae04bd303 | ||
|
|
b52b744e6d | ||
|
|
be78b64948 | ||
|
|
5cac4dfb5c | ||
|
|
0a0f57f3ec | ||
|
|
4e7b270365 | ||
|
|
02134172f1 | ||
|
|
48a71e6ad2 | ||
|
|
abe5413638 | ||
|
|
4df50b9c7e | ||
|
|
a1c7b853ec | ||
|
|
118c30fbc7 | ||
|
|
511e10df34 | ||
|
|
4f494781c1 | ||
|
|
f85f0eee1b | ||
|
|
e770303968 | ||
|
|
23de9c7e6c | ||
|
|
f696f794cf | ||
|
|
83f151947e | ||
|
|
20a71a5479 | ||
|
|
181134153d | ||
|
|
45ec4c93c0 | ||
|
|
50cf327819 | ||
|
|
8268323d7f | ||
|
|
54da7ff12b | ||
|
|
e28fe4199d | ||
|
|
183e9d953b | ||
|
|
b7c9b881e3 | ||
|
|
c0aa21f4ad | ||
|
|
72035d5faa | ||
|
|
43034790a6 | ||
|
|
8d3bca3bb8 | ||
|
|
71cee3e472 | ||
|
|
f16f8b51b8 | ||
|
|
831a24ae15 | ||
|
|
736751e5de | ||
|
|
421dbf9a7f | ||
|
|
7ac16582be | ||
|
|
5d24cb7514 | ||
|
|
a81b6beeca | ||
|
|
10dc66be64 | ||
|
|
ce813ad144 | ||
|
|
76b7092062 | ||
|
|
903eefd420 | ||
|
|
9871aeed20 | ||
|
|
4ffcf7f7ed | ||
|
|
d4b2e7f771 | ||
|
|
28bf811a07 | ||
|
|
255d8f3f8c | ||
|
|
95111e88e3 | ||
|
|
c2fcf4183c | ||
|
|
f5754f2a36 | ||
|
|
7ea5f622d3 | ||
|
|
8d602df786 | ||
|
|
59dbb17bfa | ||
|
|
72bd1ed4b3 | ||
|
|
9d15b85d3b | ||
|
|
a18e6199ef | ||
|
|
83fcd1cf4f | ||
|
|
1904d9617d | ||
|
|
2a593bd89e | ||
|
|
8a7324d114 | ||
|
|
6c8a478273 | ||
|
|
80bc5ab5ef | ||
|
|
fd2420891c | ||
|
|
4d9150735a | ||
|
|
add059205f | ||
|
|
25f1a515f8 | ||
|
|
4de21056ff | ||
|
|
651e51a82e | ||
|
|
a2cddb9eac | ||
|
|
b0ce1ce49d | ||
|
|
ac0581fd22 | ||
|
|
93923a4af2 | ||
|
|
9fc81adc7b | ||
|
|
734dbbcb14 | ||
|
|
2112416761 | ||
|
|
c0f64a6603 | ||
|
|
e2e19544ae | ||
|
|
2648bbdc51 | ||
|
|
49a6e4cbb5 | ||
|
|
7cf53dbf63 | ||
|
|
613cbf720c | ||
|
|
bce4a572cd | ||
|
|
c893b82ace | ||
|
|
eda8ddddd6 | ||
|
|
63d3f28b20 | ||
|
|
783b33e2da | ||
|
|
5847117573 | ||
|
|
258e5c4938 | ||
|
|
69ee043f9d | ||
|
|
28b5477c6f | ||
|
|
5ee83a680b | ||
|
|
d9d7914a8d | ||
|
|
ca3d67e88d | ||
|
|
61f0ce654f | ||
|
|
379f12ea00 | ||
|
|
bf15b1d65d | ||
|
|
4cabc031e6 | ||
|
|
9c5b4b2639 | ||
|
|
8a7e84a475 | ||
|
|
0e99d428b2 | ||
|
|
dbab3912bd | ||
|
|
3c6c9d650d | ||
|
|
b827a0a6a8 | ||
|
|
ab0a206d43 | ||
|
|
11902d958f | ||
|
|
ce1680e6f9 | ||
|
|
b8982cb881 | ||
|
|
5d934c2835 | ||
|
|
868c46bc76 | ||
|
|
1fd147bf2b | ||
|
|
8ee4b3f906 | ||
|
|
54134a30a7 | ||
|
|
e68de292b4 | ||
|
|
a485f97d21 | ||
|
|
496a5f423e | ||
|
|
836a2bfee0 | ||
|
|
39a3ffaf2f | ||
|
|
d4e0784182 | ||
|
|
e615d2f069 | ||
|
|
9ddb9739d1 | ||
|
|
9da8c9e07c | ||
|
|
4b507c0f05 | ||
|
|
dbb2908eeb | ||
|
|
235037a3b2 | ||
|
|
ac59772dc6 | ||
|
|
4838085d66 | ||
|
|
9ec99ffef1 | ||
|
|
6e48322055 | ||
|
|
55a98580aa | ||
|
|
c8f263c419 | ||
|
|
6f6e7d8d49 | ||
|
|
fcbd4b7afb | ||
|
|
4c2ddbf2c4 | ||
|
|
a4c05c694f | ||
|
|
a968849e9c | ||
|
|
5c1456c97f | ||
|
|
3f166bb068 | ||
|
|
ffeb5da991 | ||
|
|
edece2a197 | ||
|
|
1fd3510b32 | ||
|
|
9c0a10f662 | ||
|
|
54fd1c1f9b | ||
|
|
8131268256 | ||
|
|
ad499db397 | ||
|
|
5318957ab3 | ||
|
|
98685f744d | ||
|
|
3fc07508ee | ||
|
|
b8f66e1d33 | ||
|
|
a6f0ec160e | ||
|
|
53185f5381 | ||
|
|
081d38679f | ||
|
|
570c9d16be | ||
|
|
28b0e5ee78 | ||
|
|
cb0b608fa7 | ||
|
|
32791c9745 | ||
|
|
0153a239db | ||
|
|
49dcbd22d6 | ||
|
|
5ed9410de0 | ||
|
|
eb273f904f | ||
|
|
d8397040d7 | ||
|
|
c8ec649830 | ||
|
|
80aadc55df | ||
|
|
f68bd21600 | ||
|
|
59e729e3fe | ||
|
|
895975e2ab | ||
|
|
1228e000a1 | ||
|
|
3d3d2c93d6 | ||
|
|
3caa318dfe | ||
|
|
927cfea5ae | ||
|
|
05cdd3f6eb | ||
|
|
bf46cffd9e | ||
|
|
ab1a5b4822 | ||
|
|
9a8bc41552 | ||
|
|
0d54a47ec3 | ||
|
|
c5144c0c7d | ||
|
|
dbab707d64 | ||
|
|
591df1f205 | ||
|
|
483da67204 | ||
|
|
fba24cc4eb | ||
|
|
bcab6a9318 | ||
|
|
6268321316 | ||
|
|
29a5f059d2 | ||
|
|
d09f866daa | ||
|
|
1d86df685b | ||
|
|
6bca52453a | ||
|
|
0e249cba4b | ||
|
|
19db4cb7c1 | ||
|
|
b81670776f | ||
|
|
8452ec6f3b | ||
|
|
f7388af721 | ||
|
|
2dfdcc7dcb | ||
|
|
572a0e128d | ||
|
|
2131d1ff23 | ||
|
|
fc1abed0dc | ||
|
|
6dc0677dfc | ||
|
|
27fdc1258b | ||
|
|
2f8fa59b85 | ||
|
|
0f54e51777 | ||
|
|
6f7ab952c8 | ||
|
|
2e6ed97416 | ||
|
|
e53421b548 | ||
|
|
e5826777b6 | ||
|
|
b80e95b2aa | ||
|
|
2257612deb | ||
|
|
92bf55afd0 | ||
|
|
39250ab961 | ||
|
|
efc0d237af | ||
|
|
31ba52a57b | ||
|
|
e8e6cf9510 | ||
|
|
139025fce0 | ||
|
|
3146109b08 | ||
|
|
8896d6c1b1 | ||
|
|
25add0af31 | ||
|
|
027657b590 | ||
|
|
7e6b134222 | ||
|
|
4042bc959b | ||
|
|
6dc55a2f4e | ||
|
|
15b72591d4 | ||
|
|
fd779c25b9 | ||
|
|
ece49baa38 | ||
|
|
ba9fa54f9c | ||
|
|
1c89309db0 | ||
|
|
a368b29e27 | ||
|
|
20bbd20ef1 | ||
|
|
8cf7a77808 | ||
|
|
d121007927 | ||
|
|
3eca8cce1c | ||
|
|
d299b0d576 | ||
|
|
ea976a5ffb | ||
|
|
bedbab74b9 | ||
|
|
c587c44975 | ||
|
|
f1b9868980 | ||
|
|
8d6f033326 | ||
|
|
b5cebf45ea | ||
|
|
513b6289d6 | ||
|
|
040a638ab9 | ||
|
|
eb73ae2f86 | ||
|
|
916cc1365e | ||
|
|
86ef4d4884 | ||
|
|
456c3bda0b | ||
|
|
63daf6b317 | ||
|
|
e183d7dd9a | ||
|
|
2acc942bb4 | ||
|
|
038de44110 | ||
|
|
3b01f98c11 | ||
|
|
7cd3738c19 | ||
|
|
018e5e303f | ||
|
|
a57a9505d4 | ||
|
|
720ee96969 | ||
|
|
73f72ec8fe | ||
|
|
5d69157e62 | ||
|
|
b464b87c2b | ||
|
|
9d0d6f011c | ||
|
|
f3786e0816 | ||
|
|
3c2cbbb4a6 | ||
|
|
8649681c51 | ||
|
|
e93efe0e13 | ||
|
|
5a88b7f683 | ||
|
|
81da377d8e | ||
|
|
d950298d29 | ||
|
|
2e35defeec | ||
|
|
960f693219 | ||
|
|
e5e977c24f | ||
|
|
cac693d0eb | ||
|
|
ad0f2c5e02 | ||
|
|
3f980d6751 | ||
|
|
d235b2ce62 | ||
|
|
ec412e1748 | ||
|
|
cee41da8d8 | ||
|
|
72affd6b45 | ||
|
|
a863e68d17 | ||
|
|
847b37552a | ||
|
|
dfaca794bf | ||
|
|
6fc77a545b | ||
|
|
c871c7398e | ||
|
|
8baed8b90e | ||
|
|
8a1c43bf3b | ||
|
|
5c01ccc31f | ||
|
|
67be8208db | ||
|
|
7d136feccf | ||
|
|
e54e96d61f | ||
|
|
469304359a | ||
|
|
290e36d7e8 | ||
|
|
4241ce9888 | ||
|
|
7f9ad7eabf | ||
|
|
a6794c066d | ||
|
|
dfdd844882 | ||
|
|
afa217da97 | ||
|
|
8a8453e3b1 | ||
|
|
a8432560ba | ||
|
|
d9c1e45739 | ||
|
|
63af7ecb1f | ||
|
|
7d3ef27a8d | ||
|
|
14a781fa24 | ||
|
|
a93a67a4d9 | ||
|
|
d69c5f1a6e | ||
|
|
26ba2db53f | ||
|
|
0ae7c7e406 | ||
|
|
105315a2e3 | ||
|
|
45d79427c4 | ||
|
|
cec26d58c8 | ||
|
|
593cdae404 | ||
|
|
d2ef9ac04a | ||
|
|
d065ec9298 | ||
|
|
b19131202f | ||
|
|
70058ae49d | ||
|
|
62a23b1985 | ||
|
|
6917cd2f40 | ||
|
|
d36236cbcd | ||
|
|
b3809ffa31 | ||
|
|
82f1bb4191 | ||
|
|
40de31ddad | ||
|
|
696ad9d414 | ||
|
|
5ff0039d7b | ||
|
|
889c0b3a1f | ||
|
|
760d00b7f7 | ||
|
|
0af2c4829f | ||
|
|
be3dc5b508 | ||
|
|
9c9bca713b | ||
|
|
9c918e31e3 | ||
|
|
4bc361546e | ||
|
|
ae13063460 | ||
|
|
1ed58aaaf2 | ||
|
|
bf17895d19 | ||
|
|
20b3c43dde | ||
|
|
ee21f72211 | ||
|
|
4de5cbd6f5 | ||
|
|
fab95b8dae | ||
|
|
4d2655490c | ||
|
|
6bb4113d0a | ||
|
|
3e76f01db4 | ||
|
|
cf580d8c90 | ||
|
|
dbd0c3cbd9 | ||
|
|
3771f9e04b | ||
|
|
a842b14c84 | ||
|
|
138746bdcc | ||
|
|
9e6a9efe10 | ||
|
|
19626ad89f | ||
|
|
7e2d92284c | ||
|
|
20fb6bd788 | ||
|
|
faffb73cbd | ||
|
|
02a4e30594 | ||
|
|
f10b522f0c | ||
|
|
e4a22e8068 | ||
|
|
cfd2acfdd2 | ||
|
|
6628faaa6a | ||
|
|
d6ce7de2d3 | ||
|
|
331599fa2b | ||
|
|
558b9c90a6 | ||
|
|
7d2dda97b3 | ||
|
|
5c6133c20b | ||
|
|
16caa6e387 | ||
|
|
dc5693fde3 | ||
|
|
eeffe580d8 | ||
|
|
74fc4dbacf | ||
|
|
07912a1cb7 | ||
|
|
d36bf3b6fb | ||
|
|
594976a538 | ||
|
|
0efb889a9c | ||
|
|
c0eabe289b | ||
|
|
5bbc3c5ebb | ||
|
|
d5e2cf5d3c | ||
|
|
82a6ff091f | ||
|
|
4b8e60682d | ||
|
|
6c2db9b1cf | ||
|
|
30344d6abf | ||
|
|
1637297085 | ||
|
|
dec1fb71f4 | ||
|
|
6206e6756a | ||
|
|
ad38168b77 | ||
|
|
7273f6c03c | ||
|
|
a3ffd2edf8 | ||
|
|
a2c5eace88 | ||
|
|
a643d9d498 | ||
|
|
70b5ec4349 | ||
|
|
500e7eccc6 | ||
|
|
1dcf6a3937 | ||
|
|
1716f8da71 | ||
|
|
3b52dca405 | ||
|
|
853a0c466e | ||
|
|
94bceb8683 | ||
|
|
88b0f3a172 | ||
|
|
b69b5ba775 | ||
|
|
c442589593 | ||
|
|
28633a504a | ||
|
|
ad78701b6f | ||
|
|
1496488771 | ||
|
|
dd3d958e75 | ||
|
|
b363a3651d | ||
|
|
86645fc14c | ||
|
|
f9beecb343 | ||
|
|
4ecfbd3920 | ||
|
|
a315934314 | ||
|
|
4dbdc436ed | ||
|
|
55914c6434 | ||
|
|
c2d76e8994 | ||
|
|
e2a867507c | ||
|
|
e9170e2de1 | ||
|
|
5cfc1fabcf | ||
|
|
786b12e379 | ||
|
|
e7c5c25de8 | ||
|
|
a1e8813522 | ||
|
|
76c1446416 | ||
|
|
8bd2c87399 | ||
|
|
1e2d77f2c7 | ||
|
|
fb6c22f5c2 | ||
|
|
f7259f625f | ||
|
|
e783f796c9 | ||
|
|
f7d1d73073 | ||
|
|
5c5f30ed7e | ||
|
|
615896c8ea | ||
|
|
d9380857db | ||
|
|
1c89eb2250 | ||
|
|
3efc857fbc | ||
|
|
b628a98d32 | ||
|
|
d8fa807998 | ||
|
|
ef66d8379c | ||
|
|
8ee6cee36e | ||
|
|
71b2120e5c | ||
|
|
b10078633c | ||
|
|
b5eebd4d2b | ||
|
|
fdefc4d2b4 | ||
|
|
f6b2609353 | ||
|
|
bdffdcb12f | ||
|
|
1ebb87a6a8 | ||
|
|
83660ee381 | ||
|
|
1fa72d6c44 | ||
|
|
5a7c0d42f7 | ||
|
|
e8d2432e6a | ||
|
|
2af17adc34 | ||
|
|
e97f43399b | ||
|
|
c66c5fd73d | ||
|
|
3c0767f543 | ||
|
|
70cd1fdc63 | ||
|
|
39028dde40 | ||
|
|
6e39b5ef04 | ||
|
|
49db8a9662 | ||
|
|
2cfa6cb0e0 | ||
|
|
1ae3510ede | ||
|
|
6f1135d763 | ||
|
|
52bc2f64f4 | ||
|
|
b1375328e1 | ||
|
|
9443e2cc4b | ||
|
|
3a533c6c8d | ||
|
|
c047014214 | ||
|
|
68b05e994f |
@@ -5,6 +5,7 @@
|
|||||||
.gitattributes
|
.gitattributes
|
||||||
.gitignore
|
.gitignore
|
||||||
.github
|
.github
|
||||||
|
.vscode
|
||||||
public/system
|
public/system
|
||||||
public/assets
|
public/assets
|
||||||
public/packs
|
public/packs
|
||||||
@@ -20,6 +21,7 @@ postgres14
|
|||||||
redis
|
redis
|
||||||
elasticsearch
|
elasticsearch
|
||||||
chart
|
chart
|
||||||
|
storybook-static
|
||||||
.yarn/
|
.yarn/
|
||||||
!.yarn/patches
|
!.yarn/patches
|
||||||
!.yarn/plugins
|
!.yarn/plugins
|
||||||
|
|||||||
21
.github/renovate.json5
vendored
21
.github/renovate.json5
vendored
@@ -6,6 +6,7 @@
|
|||||||
':labels(dependencies)',
|
':labels(dependencies)',
|
||||||
':prConcurrentLimitNone', // Remove limit for open PRs at any time.
|
':prConcurrentLimitNone', // Remove limit for open PRs at any time.
|
||||||
':prHourlyLimit2', // Rate limit PR creation to a maximum of two per hour.
|
':prHourlyLimit2', // Rate limit PR creation to a maximum of two per hour.
|
||||||
|
':enableVulnerabilityAlertsWithLabel(security)',
|
||||||
],
|
],
|
||||||
rebaseWhen: 'conflicted',
|
rebaseWhen: 'conflicted',
|
||||||
minimumReleaseAge: '3', // Wait 3 days after the package has been published before upgrading it
|
minimumReleaseAge: '3', // Wait 3 days after the package has been published before upgrading it
|
||||||
@@ -23,7 +24,6 @@
|
|||||||
matchManagers: ['npm'],
|
matchManagers: ['npm'],
|
||||||
matchPackageNames: [
|
matchPackageNames: [
|
||||||
'tesseract.js', // Requires code changes
|
'tesseract.js', // Requires code changes
|
||||||
'react-hotkeys', // Requires code changes
|
|
||||||
|
|
||||||
// react-router: Requires manual upgrade
|
// react-router: Requires manual upgrade
|
||||||
'history',
|
'history',
|
||||||
@@ -94,6 +94,19 @@
|
|||||||
matchUpdateTypes: ['patch', 'minor'],
|
matchUpdateTypes: ['patch', 'minor'],
|
||||||
groupName: 'eslint (non-major)',
|
groupName: 'eslint (non-major)',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Group all Storybook-related packages in the same PR
|
||||||
|
matchManagers: ['npm'],
|
||||||
|
matchPackageNames: [
|
||||||
|
'chromatic',
|
||||||
|
'storybook',
|
||||||
|
'@storybook/*',
|
||||||
|
'msw',
|
||||||
|
'msw-storybook-addon',
|
||||||
|
],
|
||||||
|
matchUpdateTypes: ['patch', 'minor'],
|
||||||
|
groupName: 'storybook (non-major)',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// Group actions/*-artifact in the same PR
|
// Group actions/*-artifact in the same PR
|
||||||
matchManagers: ['github-actions'],
|
matchManagers: ['github-actions'],
|
||||||
@@ -142,6 +155,12 @@
|
|||||||
matchUpdateTypes: ['patch', 'minor'],
|
matchUpdateTypes: ['patch', 'minor'],
|
||||||
groupName: 'opentelemetry-ruby (non-major)',
|
groupName: 'opentelemetry-ruby (non-major)',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Group Playwright Ruby & JS deps in the same PR, as they need to be in sync
|
||||||
|
matchManagers: ['bundler', 'npm'],
|
||||||
|
matchPackageNames: ['playwright-ruby-client', 'playwright'],
|
||||||
|
groupName: 'Playwright',
|
||||||
|
},
|
||||||
// Add labels depending on package manager
|
// Add labels depending on package manager
|
||||||
{ matchManagers: ['npm', 'nvm'], addLabels: ['javascript'] },
|
{ matchManagers: ['npm', 'nvm'], addLabels: ['javascript'] },
|
||||||
{ matchManagers: ['bundler', 'ruby-version'], addLabels: ['ruby'] },
|
{ matchManagers: ['bundler', 'ruby-version'], addLabels: ['ruby'] },
|
||||||
|
|||||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -25,8 +25,8 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
language: ['javascript', 'ruby']
|
language: ['actions', 'javascript', 'ruby']
|
||||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
# CodeQL supports [ 'actions', 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
|
|
||||||
# Create or update the pull request
|
# Create or update the pull request
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@v7.0.6
|
uses: peter-evans/create-pull-request@v7.0.8
|
||||||
with:
|
with:
|
||||||
commit-message: 'New Crowdin translations'
|
commit-message: 'New Crowdin translations'
|
||||||
title: 'New Crowdin Translations for ${{ github.base_ref || github.ref_name }} (automated)'
|
title: 'New Crowdin Translations for ${{ github.base_ref || github.ref_name }} (automated)'
|
||||||
|
|||||||
1
.github/workflows/crowdin-upload.yml
vendored
1
.github/workflows/crowdin-upload.yml
vendored
@@ -14,6 +14,7 @@ on:
|
|||||||
- config/locales-glitch/devise.en.yml
|
- config/locales-glitch/devise.en.yml
|
||||||
- config/locales-glitch/doorkeeper.en.yml
|
- config/locales-glitch/doorkeeper.en.yml
|
||||||
- .github/workflows/crowdin-upload.yml
|
- .github/workflows/crowdin-upload.yml
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
upload-translations:
|
upload-translations:
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
---
|
---
|
||||||
Metrics/AbcSize:
|
Metrics/AbcSize:
|
||||||
Exclude:
|
Enabled: false
|
||||||
- lib/mastodon/cli/*.rb
|
|
||||||
|
|
||||||
Metrics/BlockLength:
|
Metrics/BlockLength:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Metrics/BlockNesting:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Metrics/ClassLength:
|
Metrics/ClassLength:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Metrics/CollectionLiteralLength:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Metrics/CyclomaticComplexity:
|
Metrics/CyclomaticComplexity:
|
||||||
Exclude:
|
Enabled: false
|
||||||
- lib/mastodon/cli/*.rb
|
|
||||||
|
|
||||||
Metrics/MethodLength:
|
Metrics/MethodLength:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
@@ -20,4 +24,7 @@ Metrics/ModuleLength:
|
|||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Metrics/ParameterLists:
|
Metrics/ParameterLists:
|
||||||
CountKeywordArgs: false
|
Enabled: false
|
||||||
|
|
||||||
|
Metrics/PerceivedComplexity:
|
||||||
|
Enabled: false
|
||||||
|
|||||||
@@ -1,32 +1,11 @@
|
|||||||
# This configuration was generated by
|
# This configuration was generated by
|
||||||
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
|
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
|
||||||
# using RuboCop version 1.77.0.
|
# using RuboCop version 1.80.2.
|
||||||
# The point is for the user to remove these configuration records
|
# The point is for the user to remove these configuration records
|
||||||
# one by one as the offenses are removed from the code base.
|
# one by one as the offenses are removed from the code base.
|
||||||
# Note that changes in the inspected code, or installation of new
|
# Note that changes in the inspected code, or installation of new
|
||||||
# versions of RuboCop, may require this file to be generated again.
|
# versions of RuboCop, may require this file to be generated again.
|
||||||
|
|
||||||
Lint/NonLocalExitFromIterator:
|
|
||||||
Exclude:
|
|
||||||
- 'app/helpers/json_ld_helper.rb'
|
|
||||||
|
|
||||||
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
|
|
||||||
Metrics/AbcSize:
|
|
||||||
Max: 90
|
|
||||||
|
|
||||||
# Configuration parameters: CountBlocks, CountModifierForms, Max.
|
|
||||||
Metrics/BlockNesting:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/tasks/mastodon.rake'
|
|
||||||
|
|
||||||
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
|
||||||
Metrics/CyclomaticComplexity:
|
|
||||||
Max: 25
|
|
||||||
|
|
||||||
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
|
||||||
Metrics/PerceivedComplexity:
|
|
||||||
Max: 27
|
|
||||||
|
|
||||||
# This cop supports safe autocorrection (--autocorrect).
|
# This cop supports safe autocorrection (--autocorrect).
|
||||||
# Configuration parameters: AllowedVars, DefaultToNil.
|
# Configuration parameters: AllowedVars, DefaultToNil.
|
||||||
Style/FetchEnvVar:
|
Style/FetchEnvVar:
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
3.4.4
|
3.4.6
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
import type { StorybookConfig } from '@storybook/react-vite';
|
import type { StorybookConfig } from '@storybook/react-vite';
|
||||||
|
|
||||||
const config: StorybookConfig = {
|
const config: StorybookConfig = {
|
||||||
@@ -26,6 +28,12 @@ const config: StorybookConfig = {
|
|||||||
'oops.png',
|
'oops.png',
|
||||||
].map((path) => ({ from: `../public/${path}`, to: `/${path}` })),
|
].map((path) => ({ from: `../public/${path}`, to: `/${path}` })),
|
||||||
],
|
],
|
||||||
|
viteFinal(config) {
|
||||||
|
// For an unknown reason, Storybook does not use the root
|
||||||
|
// from the Vite config so we need to set it manually.
|
||||||
|
config.root = resolve(__dirname, '../app/javascript');
|
||||||
|
return config;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
2
.storybook/preview-body.html
Normal file
2
.storybook/preview-body.html
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<html class="no-reduce-motion">
|
||||||
|
</html>
|
||||||
@@ -12,13 +12,14 @@ import { initialize, mswLoader } from 'msw-storybook-addon';
|
|||||||
import { action } from 'storybook/actions';
|
import { action } from 'storybook/actions';
|
||||||
|
|
||||||
import type { LocaleData } from '@/mastodon/locales';
|
import type { LocaleData } from '@/mastodon/locales';
|
||||||
import { reducerWithInitialState, rootReducer } from '@/mastodon/reducers';
|
import { reducerWithInitialState } from '@/mastodon/reducers';
|
||||||
import { defaultMiddleware } from '@/mastodon/store/store';
|
import { defaultMiddleware } from '@/mastodon/store/store';
|
||||||
import { mockHandlers, unhandledRequestHandler } from '@/testing/api';
|
import { mockHandlers, unhandledRequestHandler } from '@/testing/api';
|
||||||
|
|
||||||
// If you want to run the dark theme during development,
|
// If you want to run the dark theme during development,
|
||||||
// you can change the below to `/application.scss`
|
// you can change the below to `/application.scss`
|
||||||
import '../app/javascript/styles/mastodon-light.scss';
|
import '../app/javascript/styles/mastodon-light.scss';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
const localeFiles = import.meta.glob('@/mastodon/locales/*.json', {
|
const localeFiles = import.meta.glob('@/mastodon/locales/*.json', {
|
||||||
query: { as: 'json' },
|
query: { as: 'json' },
|
||||||
@@ -49,12 +50,17 @@ const preview: Preview = {
|
|||||||
locale: 'en',
|
locale: 'en',
|
||||||
},
|
},
|
||||||
decorators: [
|
decorators: [
|
||||||
(Story, { parameters }) => {
|
(Story, { parameters, globals }) => {
|
||||||
|
const { locale } = globals as { locale: string };
|
||||||
const { state = {} } = parameters;
|
const { state = {} } = parameters;
|
||||||
let reducer = rootReducer;
|
const reducer = reducerWithInitialState(
|
||||||
if (typeof state === 'object' && state) {
|
{
|
||||||
reducer = reducerWithInitialState(state as Record<string, unknown>);
|
meta: {
|
||||||
}
|
locale,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
state as Record<string, unknown>,
|
||||||
|
);
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
reducer,
|
reducer,
|
||||||
middleware(getDefaultMiddleware) {
|
middleware(getDefaultMiddleware) {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
* - Please do NOT modify this file.
|
* - Please do NOT modify this file.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const PACKAGE_VERSION = '2.10.2'
|
const PACKAGE_VERSION = '2.11.3'
|
||||||
const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
|
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
|
||||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||||
const activeClientIds = new Set()
|
const activeClientIds = new Set()
|
||||||
|
|
||||||
@@ -71,11 +71,6 @@ addEventListener('message', async function (event) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'MOCK_DEACTIVATE': {
|
|
||||||
activeClientIds.delete(clientId)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'CLIENT_CLOSED': {
|
case 'CLIENT_CLOSED': {
|
||||||
activeClientIds.delete(clientId)
|
activeClientIds.delete(clientId)
|
||||||
|
|
||||||
@@ -94,6 +89,8 @@ addEventListener('message', async function (event) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
addEventListener('fetch', function (event) {
|
addEventListener('fetch', function (event) {
|
||||||
|
const requestInterceptedAt = Date.now()
|
||||||
|
|
||||||
// Bypass navigation requests.
|
// Bypass navigation requests.
|
||||||
if (event.request.mode === 'navigate') {
|
if (event.request.mode === 'navigate') {
|
||||||
return
|
return
|
||||||
@@ -110,23 +107,29 @@ addEventListener('fetch', function (event) {
|
|||||||
|
|
||||||
// Bypass all requests when there are no active clients.
|
// Bypass all requests when there are no active clients.
|
||||||
// Prevents the self-unregistered worked from handling requests
|
// Prevents the self-unregistered worked from handling requests
|
||||||
// after it's been deleted (still remains active until the next reload).
|
// after it's been terminated (still remains active until the next reload).
|
||||||
if (activeClientIds.size === 0) {
|
if (activeClientIds.size === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestId = crypto.randomUUID()
|
const requestId = crypto.randomUUID()
|
||||||
event.respondWith(handleRequest(event, requestId))
|
event.respondWith(handleRequest(event, requestId, requestInterceptedAt))
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {FetchEvent} event
|
* @param {FetchEvent} event
|
||||||
* @param {string} requestId
|
* @param {string} requestId
|
||||||
|
* @param {number} requestInterceptedAt
|
||||||
*/
|
*/
|
||||||
async function handleRequest(event, requestId) {
|
async function handleRequest(event, requestId, requestInterceptedAt) {
|
||||||
const client = await resolveMainClient(event)
|
const client = await resolveMainClient(event)
|
||||||
const requestCloneForEvents = event.request.clone()
|
const requestCloneForEvents = event.request.clone()
|
||||||
const response = await getResponse(event, client, requestId)
|
const response = await getResponse(
|
||||||
|
event,
|
||||||
|
client,
|
||||||
|
requestId,
|
||||||
|
requestInterceptedAt,
|
||||||
|
)
|
||||||
|
|
||||||
// Send back the response clone for the "response:*" life-cycle events.
|
// Send back the response clone for the "response:*" life-cycle events.
|
||||||
// Ensure MSW is active and ready to handle the message, otherwise
|
// Ensure MSW is active and ready to handle the message, otherwise
|
||||||
@@ -204,7 +207,7 @@ async function resolveMainClient(event) {
|
|||||||
* @param {string} requestId
|
* @param {string} requestId
|
||||||
* @returns {Promise<Response>}
|
* @returns {Promise<Response>}
|
||||||
*/
|
*/
|
||||||
async function getResponse(event, client, requestId) {
|
async function getResponse(event, client, requestId, requestInterceptedAt) {
|
||||||
// Clone the request because it might've been already used
|
// Clone the request because it might've been already used
|
||||||
// (i.e. its body has been read and sent to the client).
|
// (i.e. its body has been read and sent to the client).
|
||||||
const requestClone = event.request.clone()
|
const requestClone = event.request.clone()
|
||||||
@@ -255,6 +258,7 @@ async function getResponse(event, client, requestId) {
|
|||||||
type: 'REQUEST',
|
type: 'REQUEST',
|
||||||
payload: {
|
payload: {
|
||||||
id: requestId,
|
id: requestId,
|
||||||
|
interceptedAt: requestInterceptedAt,
|
||||||
...serializedRequest,
|
...serializedRequest,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
8
.storybook/styles.css
Normal file
8
.storybook/styles.css
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
93
CHANGELOG.md
93
CHANGELOG.md
@@ -2,7 +2,87 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
## [4.4.0] - UNRELEASED
|
## [4.4.4] - 2025-09-16
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Update dependencies
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix missing memoization in `Web::PushNotificationWorker` (#36085 by @ClearlyClaire)
|
||||||
|
- Fix unresponsive areas around GIFV modals in some cases (#36059 by @ClearlyClaire)
|
||||||
|
- Fix missing `beforeUnload` confirmation when a poll is being authored (#36030 by @ClearlyClaire)
|
||||||
|
- Fix processing of remote edited statuses with new media and no text (#35970 by @unfokus)
|
||||||
|
- Fix polls not being displayed in moderation interface (#35644 and #35933 by @ThisIsMissEm)
|
||||||
|
- Fix WebUI handling of deleted quoted posts (#35909 and #35918 by @ClearlyClaire and @diondiondion)
|
||||||
|
- Fix “Edit” and “Delete & Redraft” on a poll not inserting empty option (#35892 by @ClearlyClaire)
|
||||||
|
- Fix loading of some compatibility CSS on some configurations (#35876 by @shleeable)
|
||||||
|
- Fix HttpLog not being enabled with `RAILS_LOG_LEVEL=debug` (#35833 by @mjankowski)
|
||||||
|
- Fix self-destruct scheduler behavior on some Redis setups (#35823 by @ClearlyClaire)
|
||||||
|
- Fix `tootctl admin create` not bypassing reserved username checks (#35779 by @ClearlyClaire)
|
||||||
|
- Fix interaction policy changes in implicit updates not being saved (#35751 by @ClearlyClaire)
|
||||||
|
- Fix quote revocation not being streamed (#35710 by @ClearlyClaire)
|
||||||
|
- Fix export of large user archives by enabling Zip64 (#35850 by @ClearlyClaire)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change labels for quote policy settings (#35893 by @ClearlyClaire)
|
||||||
|
- Change standalone “Share” page to redirect to web interface after posting (#35763 by @ChaosExAnima)
|
||||||
|
|
||||||
|
## [4.4.3] - 2025-08-05
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Update dependencies
|
||||||
|
- Fix incorrect rate-limit handling [GHSA-84ch-6436-c7mg](https://github.com/mastodon/mastodon/security/advisories/GHSA-84ch-6436-c7mg)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix race condition caused by ActiveRecord query cache in `Create` critical path (#35662 by @ClearlyClaire)
|
||||||
|
- Fix race condition caused by quote post processing (#35657 by @ClearlyClaire)
|
||||||
|
- Fix WebUI crashing for accounts with `null` URL (#35651 by @ClearlyClaire)
|
||||||
|
- Fix friends-of-friends recommendations suggesting already-requested accounts (#35604 by @ClearlyClaire)
|
||||||
|
- Fix synchronous recursive fetching of deeply-nested quoted posts (#35600 by @ClearlyClaire)
|
||||||
|
- Fix “Expand this post” link including user `@undefined` (#35478 by @ClearlyClaire)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change `StatusReachFinder` to consider quotes as well as reblogs (#35601 by @ClearlyClaire)
|
||||||
|
- Add restrictions on which quote posts can trend (#35507 by @ClearlyClaire)
|
||||||
|
- Change quote verification to not bypass authorization flow for mentions (#35528 by @ClearlyClaire)
|
||||||
|
|
||||||
|
## [4.4.2] - 2025-07-23
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Update dependencies
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix menu not clickable in Firefox (#35390 and #35414 by @diondiondion)
|
||||||
|
- Add `lang` attribute to current composer language in alt text modal (#35412 by @diondiondion)
|
||||||
|
- Fix quote posts styling on notifications page (#35411 by @diondiondion)
|
||||||
|
- Improve a11y of custom select menus in notifications settings (#35403 by @diondiondion)
|
||||||
|
- Fix selected item in poll select menus is unreadable in Firefox (#35402 by @diondiondion)
|
||||||
|
- Update age limit wording (#35387 by @diondiondion)
|
||||||
|
- Fix support for quote verification in implicit status updates (#35384 by @ClearlyClaire)
|
||||||
|
- Improve `Dropdown` component accessibility (#35373 by @diondiondion)
|
||||||
|
- Fix processing some incoming quotes failing because of missing JSON-LD context (#35354 and #35380 by @ClearlyClaire)
|
||||||
|
- Make bio hashtags open the local page instead of the remote instance (#35349 by @ChaosExAnima)
|
||||||
|
- Fix styling of external log-in button (#35320 by @ClearlyClaire)
|
||||||
|
|
||||||
|
## [4.4.1] - 2025-07-09
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix nearly every sub-directory being crawled as part of Vite build (#35323 by @ClearlyClaire)
|
||||||
|
- Fix assets not building when Redis is unavailable (#35321 by @oneiros)
|
||||||
|
- Fix replying from media modal or pop-in-player tagging user `@undefined` (#35317 by @ClearlyClaire)
|
||||||
|
- Fix support for special characters in various environment variables (#35314 by @mjankowski and @ClearlyClaire)
|
||||||
|
- Fix some database migrations failing for indexes manually removed by admins (#35309 by @mjankowski)
|
||||||
|
|
||||||
|
## [4.4.0] - 2025-07-08
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
@@ -38,7 +118,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
Server administrators can now chose to opt in to transmit referrer information when following an external link. Only the domain name is transmitted, not the referrer path.
|
Server administrators can now chose to opt in to transmit referrer information when following an external link. Only the domain name is transmitted, not the referrer path.
|
||||||
- Add double tap to zoom and swipe to dismiss to media modal in web UI (#34210 by @Gargron)
|
- Add double tap to zoom and swipe to dismiss to media modal in web UI (#34210 by @Gargron)
|
||||||
- Add link from Web UI for Hashtags to the Moderation UI (#31448 by @ThisIsMissEm)
|
- Add link from Web UI for Hashtags to the Moderation UI (#31448 by @ThisIsMissEm)
|
||||||
- **Add terms of service** (#33055, #33233, #33230, #33703, #33699, #33994, #33993, #34105, #34122, #34200, #34527, #35053, #35115, #35126 and #35127 by @ClearlyClaire, @Gargron, @mjankowski, and @oneiros)\
|
- **Add terms of service** (#33055, #33233, #33230, #33703, #33699, #33994, #33993, #34105, #34122, #34200, #34527, #35053, #35115, #35126, #35127 and #35233 by @ClearlyClaire, @Gargron, @mjankowski, and @oneiros)\
|
||||||
Server administrators can now fill in Terms of Service and notify their users of upcoming changes.
|
Server administrators can now fill in Terms of Service and notify their users of upcoming changes.
|
||||||
- Add optional bulk mailer settings (#35191 and #35203 by @oneiros)\
|
- Add optional bulk mailer settings (#35191 and #35203 by @oneiros)\
|
||||||
This adds the optional environment variables `BULK_SMTP_PORT`, `BULK_SMTP_SERVER`, `BULK_SMTP_LOGIN` and so on analogous to `SMTP_PORT`, `SMTP_SERVER`, `SMTP_LOGIN` and related SMTP configuration environment variables.\
|
This adds the optional environment variables `BULK_SMTP_PORT`, `BULK_SMTP_SERVER`, `BULK_SMTP_LOGIN` and so on analogous to `SMTP_PORT`, `SMTP_SERVER`, `SMTP_LOGIN` and related SMTP configuration environment variables.\
|
||||||
@@ -51,7 +131,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Add ability to dismiss alt text badge by tapping it in web UI (#33737 by @Gargron)
|
- Add ability to dismiss alt text badge by tapping it in web UI (#33737 by @Gargron)
|
||||||
- Add loading indicator to timeline gap indicators in web UI (#33762 by @Gargron)
|
- Add loading indicator to timeline gap indicators in web UI (#33762 by @Gargron)
|
||||||
- Add interaction modal when trying to interact with a poll while logged out (#32609 by @ThisIsMissEm)
|
- Add interaction modal when trying to interact with a poll while logged out (#32609 by @ThisIsMissEm)
|
||||||
- **Add experimental FASP support** (#34031, #34415, #34765, #34965, #34964, #34033 and #35218 by @oneiros)\
|
- **Add experimental FASP support** (#34031, #34415, #34765, #34965, #34964, #34033, #35218, #35262 and #35263 by @oneiros)\
|
||||||
This is a first step towards supporting “Fediverse Auxiliary Service Providers” (https://github.com/mastodon/fediverse_auxiliary_service_provider_specifications). This is mostly interesting to developers who would like to implement their own FASP, but also includes the capability to share data with a discovery provider (see https://www.fediscovery.org).
|
This is a first step towards supporting “Fediverse Auxiliary Service Providers” (https://github.com/mastodon/fediverse_auxiliary_service_provider_specifications). This is mostly interesting to developers who would like to implement their own FASP, but also includes the capability to share data with a discovery provider (see https://www.fediscovery.org).
|
||||||
- Add ability for admins to send announcements to all users via email (#33928 and #34411 by @ClearlyClaire)\
|
- Add ability for admins to send announcements to all users via email (#33928 and #34411 by @ClearlyClaire)\
|
||||||
This is meant for critical announcements only, as this will potentially send a lot of emails and cannot be opted out of by users.
|
This is meant for critical announcements only, as this will potentially send a lot of emails and cannot be opted out of by users.
|
||||||
@@ -64,7 +144,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Add dropdown menu with quick actions to lists of accounts in web UI (#34391, #34709, and #34767 by @Gargron, @diondiondion, and @mkljczk)
|
- Add dropdown menu with quick actions to lists of accounts in web UI (#34391, #34709, and #34767 by @Gargron, @diondiondion, and @mkljczk)
|
||||||
- Add support for displaying “year in review” notification in web UI (#32710, #32765, #32709, #32807, #32914, #33148, and #33882 by @Gargron and @mjankowski)\
|
- Add support for displaying “year in review” notification in web UI (#32710, #32765, #32709, #32807, #32914, #33148, and #33882 by @Gargron and @mjankowski)\
|
||||||
Note that the notification is currently not generated automatically, and at the moment requires a manual undocumented administrator action.
|
Note that the notification is currently not generated automatically, and at the moment requires a manual undocumented administrator action.
|
||||||
- Add experimental support for receiving HTTP Message Signatures (RFC9421) (#34814, #35033 and #35109 by @oneiros)\
|
- Add experimental support for receiving HTTP Message Signatures (RFC9421) (#34814, #35033, #35109 and #35278 by @oneiros)\
|
||||||
For now, this needs to be explicitly enabled through the `http_message_signatures` feature flag (`EXPERIMENTAL_FEATURES=http_message_signatures`). This currently only covers verifying such signatures (inbound HTTP requests), not issuing them (outbound HTTP requests).
|
For now, this needs to be explicitly enabled through the `http_message_signatures` feature flag (`EXPERIMENTAL_FEATURES=http_message_signatures`). This currently only covers verifying such signatures (inbound HTTP requests), not issuing them (outbound HTTP requests).
|
||||||
- Add experimental Async Refreshes API (#34918 by @oneiros)
|
- Add experimental Async Refreshes API (#34918 by @oneiros)
|
||||||
- Add experimental server-side feature to fetch remote replies (#32615, #34147, #34149, #34151, #34615, #34682, and #34702 by @ClearlyClaire and @sneakers-the-rat)\
|
- Add experimental server-side feature to fetch remote replies (#32615, #34147, #34149, #34151, #34615, #34682, and #34702 by @ClearlyClaire and @sneakers-the-rat)\
|
||||||
@@ -218,6 +298,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Fix admin dashboard crash on specific Elasticsearch connection errors (#34683 by @ClearlyClaire)
|
- Fix admin dashboard crash on specific Elasticsearch connection errors (#34683 by @ClearlyClaire)
|
||||||
- Fix OIDC account creation failing for long display names (#34639 by @defnull)
|
- Fix OIDC account creation failing for long display names (#34639 by @defnull)
|
||||||
- Fix use of the deprecated `/api/v1/instance` endpoint in the moderation interface (#34613 by @renchap)
|
- Fix use of the deprecated `/api/v1/instance` endpoint in the moderation interface (#34613 by @renchap)
|
||||||
|
- Fix inaccessible “Clear search” button (#35152 and #35281 by @diondiondion)
|
||||||
- Fix search operators sometimes getting lost (#35190 by @ClearlyClaire)
|
- Fix search operators sometimes getting lost (#35190 by @ClearlyClaire)
|
||||||
- Fix directory scroll position reset (#34560 by @przucidlo)
|
- Fix directory scroll position reset (#34560 by @przucidlo)
|
||||||
- Fix needlessly complex SVG paths for oEmbed and logo (#34538 by @edent)
|
- Fix needlessly complex SVG paths for oEmbed and logo (#34538 by @edent)
|
||||||
@@ -232,7 +313,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Fix extra space under left-indented vertical videos (#34313 by @ClearlyClaire)
|
- Fix extra space under left-indented vertical videos (#34313 by @ClearlyClaire)
|
||||||
- Fix glitchy iOS media attachment drag interactions (#35057 by @diondiondion)
|
- Fix glitchy iOS media attachment drag interactions (#35057 by @diondiondion)
|
||||||
- Fix zoomed images being blurry in Safari (#35052 by @diondiondion)
|
- Fix zoomed images being blurry in Safari (#35052 by @diondiondion)
|
||||||
- Fix redundant focus stop within status component in Web UI and make focus style more noticeable (#35037, #35051, #35096 and #35150 by @diondiondion)
|
- Fix redundant focus stop within status component in Web UI and make focus style more noticeable (#35037, #35051, #35096, #35150 and #35251 by @diondiondion)
|
||||||
- Fix digits in media player time readout not having a consistent width (#35038 by @diondiondion)
|
- Fix digits in media player time readout not having a consistent width (#35038 by @diondiondion)
|
||||||
- Fix wrong text color for “Open in advanced web interface” banner in high-contrast theme (#35032 by @diondiondion)
|
- Fix wrong text color for “Open in advanced web interface” banner in high-contrast theme (#35032 by @diondiondion)
|
||||||
- Fix hover card for limited accounts not hiding information as expected (#35024 by @diondiondion)
|
- Fix hover card for limited accounts not hiding information as expected (#35024 by @diondiondion)
|
||||||
@@ -530,7 +611,6 @@ The following changelog entries focus on changes visible to users, administrator
|
|||||||
You can now separately filter or drop notifications from people you don't follow, people who don't follow you, accounts created within the past 30 days, as well as unsolicited private mentions, and accounts limited by the moderation.\
|
You can now separately filter or drop notifications from people you don't follow, people who don't follow you, accounts created within the past 30 days, as well as unsolicited private mentions, and accounts limited by the moderation.\
|
||||||
Instead of being outright dropped, notifications that you chose to filter are put in a separate “Filtered notifications” box that you can review separately without it clogging your main notifications.\
|
Instead of being outright dropped, notifications that you chose to filter are put in a separate “Filtered notifications” box that you can review separately without it clogging your main notifications.\
|
||||||
This adds the following REST API endpoints:
|
This adds the following REST API endpoints:
|
||||||
|
|
||||||
- `GET /api/v2/notifications/policy`: https://docs.joinmastodon.org/methods/notifications/#get-policy
|
- `GET /api/v2/notifications/policy`: https://docs.joinmastodon.org/methods/notifications/#get-policy
|
||||||
- `PATCH /api/v2/notifications/policy`: https://docs.joinmastodon.org/methods/notifications/#update-the-filtering-policy-for-notifications
|
- `PATCH /api/v2/notifications/policy`: https://docs.joinmastodon.org/methods/notifications/#update-the-filtering-policy-for-notifications
|
||||||
- `GET /api/v1/notifications/requests`: https://docs.joinmastodon.org/methods/notifications/#get-requests
|
- `GET /api/v1/notifications/requests`: https://docs.joinmastodon.org/methods/notifications/#get-requests
|
||||||
@@ -542,7 +622,6 @@ The following changelog entries focus on changes visible to users, administrator
|
|||||||
- `GET /api/v1/notifications/requests/merged`: https://docs.joinmastodon.org/methods/notifications/#requests-merged
|
- `GET /api/v1/notifications/requests/merged`: https://docs.joinmastodon.org/methods/notifications/#requests-merged
|
||||||
|
|
||||||
In addition, accepting one or more notification requests generates a new streaming event:
|
In addition, accepting one or more notification requests generates a new streaming event:
|
||||||
|
|
||||||
- `notifications_merged`: an event of this type indicates accepted notification requests have finished merging, and the notifications list should be refreshed
|
- `notifications_merged`: an event of this type indicates accepted notification requests have finished merging, and the notifications list should be refreshed
|
||||||
|
|
||||||
- **Add notifications of severed relationships** (#27511, #29665, #29668, #29670, #29700, #29714, #29712, and #29731 by @ClearlyClaire and @Gargron)\
|
- **Add notifications of severed relationships** (#27511, #29665, #29668, #29670, #29700, #29714, #29712, and #29731 by @ClearlyClaire and @Gargron)\
|
||||||
|
|||||||
41
Dockerfile
41
Dockerfile
@@ -1,4 +1,4 @@
|
|||||||
# syntax=docker/dockerfile:1.12
|
# syntax=docker/dockerfile:1.18
|
||||||
|
|
||||||
# This file is designed for production server deployment, not local development work
|
# This file is designed for production server deployment, not local development work
|
||||||
# For a containerized local dev environment, see: https://github.com/mastodon/mastodon/blob/main/docs/DEVELOPMENT.md#docker
|
# For a containerized local dev environment, see: https://github.com/mastodon/mastodon/blob/main/docs/DEVELOPMENT.md#docker
|
||||||
@@ -13,15 +13,15 @@ ARG BASE_REGISTRY="docker.io"
|
|||||||
|
|
||||||
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"]
|
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"]
|
||||||
# renovate: datasource=docker depName=docker.io/ruby
|
# renovate: datasource=docker depName=docker.io/ruby
|
||||||
ARG RUBY_VERSION="3.4.4"
|
ARG RUBY_VERSION="3.4.6"
|
||||||
# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
|
# # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
|
||||||
# renovate: datasource=node-version depName=node
|
# renovate: datasource=node-version depName=node
|
||||||
ARG NODE_MAJOR_VERSION="22"
|
ARG NODE_MAJOR_VERSION="22"
|
||||||
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"]
|
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="trixie"]
|
||||||
ARG DEBIAN_VERSION="bookworm"
|
ARG DEBIAN_VERSION="trixie"
|
||||||
# Node.js image to use for base image based on combined variables (ex: 20-bookworm-slim)
|
# Node.js image to use for base image based on combined variables (ex: 20-trixie-slim)
|
||||||
FROM ${BASE_REGISTRY}/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim AS node
|
FROM ${BASE_REGISTRY}/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim AS node
|
||||||
# Ruby image to use for base image based on combined variables (ex: 3.4.x-slim-bookworm)
|
# Ruby image to use for base image based on combined variables (ex: 3.4.x-slim-trixie)
|
||||||
FROM ${BASE_REGISTRY}/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} AS ruby
|
FROM ${BASE_REGISTRY}/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} AS ruby
|
||||||
|
|
||||||
# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
|
# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
|
||||||
@@ -96,9 +96,6 @@ RUN \
|
|||||||
# Set /opt/mastodon as working directory
|
# Set /opt/mastodon as working directory
|
||||||
WORKDIR /opt/mastodon
|
WORKDIR /opt/mastodon
|
||||||
|
|
||||||
# Add backport repository for some specific packages where we need the latest version
|
|
||||||
RUN echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list
|
|
||||||
|
|
||||||
# hadolint ignore=DL3008,DL3005
|
# hadolint ignore=DL3008,DL3005
|
||||||
RUN \
|
RUN \
|
||||||
# Mount Apt cache and lib directories from Docker buildx caches
|
# Mount Apt cache and lib directories from Docker buildx caches
|
||||||
@@ -161,11 +158,11 @@ RUN \
|
|||||||
libexif-dev \
|
libexif-dev \
|
||||||
libexpat1-dev \
|
libexpat1-dev \
|
||||||
libgirepository1.0-dev \
|
libgirepository1.0-dev \
|
||||||
libheif-dev/bookworm-backports \
|
libheif-dev \
|
||||||
|
libhwy-dev \
|
||||||
libimagequant-dev \
|
libimagequant-dev \
|
||||||
libjpeg62-turbo-dev \
|
libjpeg62-turbo-dev \
|
||||||
liblcms2-dev \
|
liblcms2-dev \
|
||||||
liborc-dev \
|
|
||||||
libspng-dev \
|
libspng-dev \
|
||||||
libtiff-dev \
|
libtiff-dev \
|
||||||
libwebp-dev \
|
libwebp-dev \
|
||||||
@@ -186,7 +183,7 @@ FROM build AS libvips
|
|||||||
|
|
||||||
# libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"]
|
# libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"]
|
||||||
# renovate: datasource=github-releases depName=libvips packageName=libvips/libvips
|
# renovate: datasource=github-releases depName=libvips packageName=libvips/libvips
|
||||||
ARG VIPS_VERSION=8.17.0
|
ARG VIPS_VERSION=8.17.2
|
||||||
# libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"]
|
# libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"]
|
||||||
ARG VIPS_URL=https://github.com/libvips/libvips/releases/download
|
ARG VIPS_URL=https://github.com/libvips/libvips/releases/download
|
||||||
|
|
||||||
@@ -209,7 +206,7 @@ FROM build AS ffmpeg
|
|||||||
|
|
||||||
# ffmpeg version to compile, change with [--build-arg FFMPEG_VERSION="7.0.x"]
|
# ffmpeg version to compile, change with [--build-arg FFMPEG_VERSION="7.0.x"]
|
||||||
# renovate: datasource=repology depName=ffmpeg packageName=openpkg_current/ffmpeg
|
# renovate: datasource=repology depName=ffmpeg packageName=openpkg_current/ffmpeg
|
||||||
ARG FFMPEG_VERSION=7.1
|
ARG FFMPEG_VERSION=8.0
|
||||||
# ffmpeg download URL, change with [--build-arg FFMPEG_URL="https://ffmpeg.org/releases"]
|
# ffmpeg download URL, change with [--build-arg FFMPEG_URL="https://ffmpeg.org/releases"]
|
||||||
ARG FFMPEG_URL=https://ffmpeg.org/releases
|
ARG FFMPEG_URL=https://ffmpeg.org/releases
|
||||||
|
|
||||||
@@ -327,28 +324,28 @@ RUN \
|
|||||||
# Apt update install non-dev versions of necessary components
|
# Apt update install non-dev versions of necessary components
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
libexpat1 \
|
libexpat1 \
|
||||||
libglib2.0-0 \
|
libglib2.0-0t64 \
|
||||||
libicu72 \
|
libicu76 \
|
||||||
libidn12 \
|
libidn12 \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
libreadline8 \
|
libreadline8t64 \
|
||||||
libssl3 \
|
libssl3t64 \
|
||||||
libyaml-0-2 \
|
libyaml-0-2 \
|
||||||
# libvips components
|
# libvips components
|
||||||
libcgif0 \
|
libcgif0 \
|
||||||
libexif12 \
|
libexif12 \
|
||||||
libheif1/bookworm-backports \
|
libheif1 \
|
||||||
|
libhwy1t64 \
|
||||||
libimagequant0 \
|
libimagequant0 \
|
||||||
libjpeg62-turbo \
|
libjpeg62-turbo \
|
||||||
liblcms2-2 \
|
liblcms2-2 \
|
||||||
liborc-0.4-0 \
|
|
||||||
libspng0 \
|
libspng0 \
|
||||||
libtiff6 \
|
libtiff6 \
|
||||||
libwebp7 \
|
libwebp7 \
|
||||||
libwebpdemux2 \
|
libwebpdemux2 \
|
||||||
libwebpmux3 \
|
libwebpmux3 \
|
||||||
# ffmpeg components
|
# ffmpeg components
|
||||||
libdav1d6 \
|
libdav1d7 \
|
||||||
libmp3lame0 \
|
libmp3lame0 \
|
||||||
libopencore-amrnb0 \
|
libopencore-amrnb0 \
|
||||||
libopencore-amrwb0 \
|
libopencore-amrwb0 \
|
||||||
@@ -358,9 +355,9 @@ RUN \
|
|||||||
libvorbis0a \
|
libvorbis0a \
|
||||||
libvorbisenc2 \
|
libvorbisenc2 \
|
||||||
libvorbisfile3 \
|
libvorbisfile3 \
|
||||||
libvpx7 \
|
libvpx9 \
|
||||||
libx264-164 \
|
libx264-164 \
|
||||||
libx265-199 \
|
libx265-215 \
|
||||||
;
|
;
|
||||||
|
|
||||||
# Copy Mastodon sources into final layer
|
# Copy Mastodon sources into final layer
|
||||||
|
|||||||
27
Gemfile
27
Gemfile
@@ -62,7 +62,7 @@ gem 'inline_svg'
|
|||||||
gem 'irb', '~> 1.8'
|
gem 'irb', '~> 1.8'
|
||||||
gem 'kaminari', '~> 1.2'
|
gem 'kaminari', '~> 1.2'
|
||||||
gem 'link_header', '~> 0.0'
|
gem 'link_header', '~> 0.0'
|
||||||
gem 'linzer', '~> 0.7.2'
|
gem 'linzer', '~> 0.7.7'
|
||||||
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
||||||
gem 'mime-types', '~> 3.7.0', require: 'mime/types/columnar'
|
gem 'mime-types', '~> 3.7.0', require: 'mime/types/columnar'
|
||||||
gem 'mutex_m'
|
gem 'mutex_m'
|
||||||
@@ -82,13 +82,13 @@ gem 'rqrcode', '~> 3.0'
|
|||||||
gem 'ruby-progressbar', '~> 1.13'
|
gem 'ruby-progressbar', '~> 1.13'
|
||||||
gem 'sanitize', '~> 7.0'
|
gem 'sanitize', '~> 7.0'
|
||||||
gem 'scenic', '~> 1.7'
|
gem 'scenic', '~> 1.7'
|
||||||
gem 'sidekiq', '< 8'
|
gem 'sidekiq', '< 9'
|
||||||
gem 'sidekiq-bulk', '~> 0.2.0'
|
gem 'sidekiq-bulk', '~> 0.2.0'
|
||||||
gem 'sidekiq-scheduler', '~> 5.0'
|
gem 'sidekiq-scheduler', '~> 6.0'
|
||||||
gem 'sidekiq-unique-jobs', '> 8'
|
gem 'sidekiq-unique-jobs', '> 8'
|
||||||
gem 'simple_form', '~> 5.2'
|
gem 'simple_form', '~> 5.2'
|
||||||
gem 'simple-navigation', '~> 4.4'
|
gem 'simple-navigation', '~> 4.4'
|
||||||
gem 'stoplight', '~> 4.1'
|
gem 'stoplight'
|
||||||
gem 'strong_migrations'
|
gem 'strong_migrations'
|
||||||
gem 'tty-prompt', '~> 0.23', require: false
|
gem 'tty-prompt', '~> 0.23', require: false
|
||||||
gem 'twitter-text', '~> 3.1.0'
|
gem 'twitter-text', '~> 3.1.0'
|
||||||
@@ -102,21 +102,21 @@ gem 'rdf-normalize', '~> 0.5'
|
|||||||
|
|
||||||
gem 'prometheus_exporter', '~> 2.2', require: false
|
gem 'prometheus_exporter', '~> 2.2', require: false
|
||||||
|
|
||||||
gem 'opentelemetry-api', '~> 1.5.0'
|
gem 'opentelemetry-api', '~> 1.7.0'
|
||||||
|
|
||||||
group :opentelemetry do
|
group :opentelemetry do
|
||||||
gem 'opentelemetry-exporter-otlp', '~> 0.30.0', require: false
|
gem 'opentelemetry-exporter-otlp', '~> 0.30.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-active_job', '~> 0.8.0', require: false
|
gem 'opentelemetry-instrumentation-active_job', '~> 0.8.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.22.0', require: false
|
gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.22.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.22.0', require: false
|
gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.22.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-excon', '~> 0.23.0', require: false
|
gem 'opentelemetry-instrumentation-excon', '~> 0.24.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-faraday', '~> 0.27.0', require: false
|
gem 'opentelemetry-instrumentation-faraday', '~> 0.28.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-http', '~> 0.25.0', require: false
|
gem 'opentelemetry-instrumentation-http', '~> 0.25.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-http_client', '~> 0.23.0', require: false
|
gem 'opentelemetry-instrumentation-http_client', '~> 0.24.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-net_http', '~> 0.23.0', require: false
|
gem 'opentelemetry-instrumentation-net_http', '~> 0.24.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-pg', '~> 0.30.0', require: false
|
gem 'opentelemetry-instrumentation-pg', '~> 0.30.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-rack', '~> 0.26.0', require: false
|
gem 'opentelemetry-instrumentation-rack', '~> 0.27.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-rails', '~> 0.36.0', require: false
|
gem 'opentelemetry-instrumentation-rails', '~> 0.37.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-redis', '~> 0.26.0', require: false
|
gem 'opentelemetry-instrumentation-redis', '~> 0.26.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.26.0', require: false
|
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.26.0', require: false
|
||||||
gem 'opentelemetry-sdk', '~> 1.4', require: false
|
gem 'opentelemetry-sdk', '~> 1.4', require: false
|
||||||
@@ -138,6 +138,7 @@ group :test do
|
|||||||
# Browser integration testing
|
# Browser integration testing
|
||||||
gem 'capybara', '~> 3.39'
|
gem 'capybara', '~> 3.39'
|
||||||
gem 'capybara-playwright-driver'
|
gem 'capybara-playwright-driver'
|
||||||
|
gem 'playwright-ruby-client', '1.55.0', require: false # Pinning the exact version as it needs to be kept in sync with the installed npm package
|
||||||
|
|
||||||
# Used to reset the database between system tests
|
# Used to reset the database between system tests
|
||||||
gem 'database_cleaner-active_record'
|
gem 'database_cleaner-active_record'
|
||||||
@@ -146,7 +147,7 @@ group :test do
|
|||||||
gem 'climate_control'
|
gem 'climate_control'
|
||||||
|
|
||||||
# Validate schemas in specs
|
# Validate schemas in specs
|
||||||
gem 'json-schema', '~> 5.0'
|
gem 'json-schema', '~> 6.0'
|
||||||
|
|
||||||
# Test harness fo rack components
|
# Test harness fo rack components
|
||||||
gem 'rack-test', '~> 2.1'
|
gem 'rack-test', '~> 2.1'
|
||||||
@@ -223,7 +224,7 @@ gem 'connection_pool', require: false
|
|||||||
gem 'xorcist', '~> 1.1'
|
gem 'xorcist', '~> 1.1'
|
||||||
|
|
||||||
gem 'net-http', '~> 0.6.0'
|
gem 'net-http', '~> 0.6.0'
|
||||||
gem 'rubyzip', '~> 2.3'
|
gem 'rubyzip', '~> 3.0'
|
||||||
|
|
||||||
gem 'hcaptcha', '~> 7.1'
|
gem 'hcaptcha', '~> 7.1'
|
||||||
|
|
||||||
|
|||||||
342
Gemfile.lock
342
Gemfile.lock
@@ -10,29 +10,29 @@ GIT
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (8.0.2)
|
actioncable (8.0.2.1)
|
||||||
actionpack (= 8.0.2)
|
actionpack (= 8.0.2.1)
|
||||||
activesupport (= 8.0.2)
|
activesupport (= 8.0.2.1)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
actionmailbox (8.0.2)
|
actionmailbox (8.0.2.1)
|
||||||
actionpack (= 8.0.2)
|
actionpack (= 8.0.2.1)
|
||||||
activejob (= 8.0.2)
|
activejob (= 8.0.2.1)
|
||||||
activerecord (= 8.0.2)
|
activerecord (= 8.0.2.1)
|
||||||
activestorage (= 8.0.2)
|
activestorage (= 8.0.2.1)
|
||||||
activesupport (= 8.0.2)
|
activesupport (= 8.0.2.1)
|
||||||
mail (>= 2.8.0)
|
mail (>= 2.8.0)
|
||||||
actionmailer (8.0.2)
|
actionmailer (8.0.2.1)
|
||||||
actionpack (= 8.0.2)
|
actionpack (= 8.0.2.1)
|
||||||
actionview (= 8.0.2)
|
actionview (= 8.0.2.1)
|
||||||
activejob (= 8.0.2)
|
activejob (= 8.0.2.1)
|
||||||
activesupport (= 8.0.2)
|
activesupport (= 8.0.2.1)
|
||||||
mail (>= 2.8.0)
|
mail (>= 2.8.0)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
actionpack (8.0.2)
|
actionpack (8.0.2.1)
|
||||||
actionview (= 8.0.2)
|
actionview (= 8.0.2.1)
|
||||||
activesupport (= 8.0.2)
|
activesupport (= 8.0.2.1)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
rack (>= 2.2.4)
|
rack (>= 2.2.4)
|
||||||
rack-session (>= 1.0.1)
|
rack-session (>= 1.0.1)
|
||||||
@@ -40,15 +40,15 @@ GEM
|
|||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.6)
|
rails-html-sanitizer (~> 1.6)
|
||||||
useragent (~> 0.16)
|
useragent (~> 0.16)
|
||||||
actiontext (8.0.2)
|
actiontext (8.0.2.1)
|
||||||
actionpack (= 8.0.2)
|
actionpack (= 8.0.2.1)
|
||||||
activerecord (= 8.0.2)
|
activerecord (= 8.0.2.1)
|
||||||
activestorage (= 8.0.2)
|
activestorage (= 8.0.2.1)
|
||||||
activesupport (= 8.0.2)
|
activesupport (= 8.0.2.1)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (8.0.2)
|
actionview (8.0.2.1)
|
||||||
activesupport (= 8.0.2)
|
activesupport (= 8.0.2.1)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.11)
|
erubi (~> 1.11)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
@@ -58,22 +58,22 @@ GEM
|
|||||||
activemodel (>= 4.1)
|
activemodel (>= 4.1)
|
||||||
case_transform (>= 0.2)
|
case_transform (>= 0.2)
|
||||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||||
activejob (8.0.2)
|
activejob (8.0.2.1)
|
||||||
activesupport (= 8.0.2)
|
activesupport (= 8.0.2.1)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (8.0.2)
|
activemodel (8.0.2.1)
|
||||||
activesupport (= 8.0.2)
|
activesupport (= 8.0.2.1)
|
||||||
activerecord (8.0.2)
|
activerecord (8.0.2.1)
|
||||||
activemodel (= 8.0.2)
|
activemodel (= 8.0.2.1)
|
||||||
activesupport (= 8.0.2)
|
activesupport (= 8.0.2.1)
|
||||||
timeout (>= 0.4.0)
|
timeout (>= 0.4.0)
|
||||||
activestorage (8.0.2)
|
activestorage (8.0.2.1)
|
||||||
actionpack (= 8.0.2)
|
actionpack (= 8.0.2.1)
|
||||||
activejob (= 8.0.2)
|
activejob (= 8.0.2.1)
|
||||||
activerecord (= 8.0.2)
|
activerecord (= 8.0.2.1)
|
||||||
activesupport (= 8.0.2)
|
activesupport (= 8.0.2.1)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
activesupport (8.0.2)
|
activesupport (8.0.2.1)
|
||||||
base64
|
base64
|
||||||
benchmark (>= 0.3)
|
benchmark (>= 0.3)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
@@ -90,13 +90,13 @@ GEM
|
|||||||
public_suffix (>= 2.0.2, < 7.0)
|
public_suffix (>= 2.0.2, < 7.0)
|
||||||
aes_key_wrap (1.1.0)
|
aes_key_wrap (1.1.0)
|
||||||
android_key_attestation (0.3.0)
|
android_key_attestation (0.3.0)
|
||||||
annotaterb (4.16.0)
|
annotaterb (4.19.0)
|
||||||
activerecord (>= 6.0.0)
|
activerecord (>= 6.0.0)
|
||||||
activesupport (>= 6.0.0)
|
activesupport (>= 6.0.0)
|
||||||
ast (2.4.3)
|
ast (2.4.3)
|
||||||
attr_required (1.0.2)
|
attr_required (1.0.2)
|
||||||
aws-eventstream (1.3.2)
|
aws-eventstream (1.4.0)
|
||||||
aws-partitions (1.1103.0)
|
aws-partitions (1.1135.0)
|
||||||
aws-sdk-core (3.215.1)
|
aws-sdk-core (3.215.1)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.992.0)
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
@@ -109,9 +109,9 @@ GEM
|
|||||||
aws-sdk-core (~> 3, >= 3.210.0)
|
aws-sdk-core (~> 3, >= 3.210.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sigv4 (1.11.0)
|
aws-sigv4 (1.12.1)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
azure-blob (0.5.8)
|
azure-blob (0.5.9.1)
|
||||||
rexml
|
rexml
|
||||||
base64 (0.3.0)
|
base64 (0.3.0)
|
||||||
bcp47_spec (0.2.1)
|
bcp47_spec (0.2.1)
|
||||||
@@ -121,7 +121,7 @@ GEM
|
|||||||
erubi (>= 1.0.0)
|
erubi (>= 1.0.0)
|
||||||
rack (>= 0.9.0)
|
rack (>= 0.9.0)
|
||||||
rouge (>= 1.0.0)
|
rouge (>= 1.0.0)
|
||||||
bigdecimal (3.2.2)
|
bigdecimal (3.2.3)
|
||||||
bindata (2.5.1)
|
bindata (2.5.1)
|
||||||
binding_of_caller (1.0.1)
|
binding_of_caller (1.0.1)
|
||||||
debug_inspector (>= 1.2.0)
|
debug_inspector (>= 1.2.0)
|
||||||
@@ -144,7 +144,7 @@ GEM
|
|||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
regexp_parser (>= 1.5, < 3.0)
|
regexp_parser (>= 1.5, < 3.0)
|
||||||
xpath (~> 3.2)
|
xpath (~> 3.2)
|
||||||
capybara-playwright-driver (0.5.6)
|
capybara-playwright-driver (0.5.7)
|
||||||
addressable
|
addressable
|
||||||
capybara
|
capybara
|
||||||
playwright-ruby-client (>= 1.16.0)
|
playwright-ruby-client (>= 1.16.0)
|
||||||
@@ -164,7 +164,7 @@ GEM
|
|||||||
cocoon (1.2.15)
|
cocoon (1.2.15)
|
||||||
color_diff (0.1)
|
color_diff (0.1)
|
||||||
concurrent-ruby (1.3.5)
|
concurrent-ruby (1.3.5)
|
||||||
connection_pool (2.5.3)
|
connection_pool (2.5.4)
|
||||||
cose (1.3.1)
|
cose (1.3.1)
|
||||||
cbor (~> 0.5.9)
|
cbor (~> 0.5.9)
|
||||||
openssl-signature_algorithm (~> 1.0)
|
openssl-signature_algorithm (~> 1.0)
|
||||||
@@ -175,9 +175,9 @@ GEM
|
|||||||
css_parser (1.21.1)
|
css_parser (1.21.1)
|
||||||
addressable
|
addressable
|
||||||
csv (3.3.5)
|
csv (3.3.5)
|
||||||
database_cleaner-active_record (2.2.1)
|
database_cleaner-active_record (2.2.2)
|
||||||
activerecord (>= 5.a)
|
activerecord (>= 5.a)
|
||||||
database_cleaner-core (~> 2.0.0)
|
database_cleaner-core (~> 2.0)
|
||||||
database_cleaner-core (2.0.1)
|
database_cleaner-core (2.0.1)
|
||||||
date (3.4.1)
|
date (3.4.1)
|
||||||
debug (1.11.0)
|
debug (1.11.0)
|
||||||
@@ -224,16 +224,16 @@ GEM
|
|||||||
mail (~> 2.7)
|
mail (~> 2.7)
|
||||||
email_validator (2.2.4)
|
email_validator (2.2.4)
|
||||||
activemodel
|
activemodel
|
||||||
erb (5.0.1)
|
erb (5.0.2)
|
||||||
erubi (1.13.1)
|
erubi (1.13.1)
|
||||||
et-orbi (1.2.11)
|
et-orbi (1.2.11)
|
||||||
tzinfo
|
tzinfo
|
||||||
excon (1.2.5)
|
excon (1.2.8)
|
||||||
logger
|
logger
|
||||||
fabrication (3.0.0)
|
fabrication (3.0.0)
|
||||||
faker (3.5.1)
|
faker (3.5.2)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (2.13.1)
|
faraday (2.13.4)
|
||||||
faraday-net_http (>= 2.0, < 3.5)
|
faraday-net_http (>= 2.0, < 3.5)
|
||||||
json
|
json
|
||||||
logger
|
logger
|
||||||
@@ -241,7 +241,7 @@ GEM
|
|||||||
faraday (>= 1, < 3)
|
faraday (>= 1, < 3)
|
||||||
faraday-httpclient (2.0.2)
|
faraday-httpclient (2.0.2)
|
||||||
httpclient (>= 2.2)
|
httpclient (>= 2.2)
|
||||||
faraday-net_http (3.4.0)
|
faraday-net_http (3.4.1)
|
||||||
net-http (>= 0.5.0)
|
net-http (>= 0.5.0)
|
||||||
fast_blank (1.0.1)
|
fast_blank (1.0.1)
|
||||||
fastimage (2.4.0)
|
fastimage (2.4.0)
|
||||||
@@ -266,14 +266,14 @@ GEM
|
|||||||
fog-openstack (1.1.5)
|
fog-openstack (1.1.5)
|
||||||
fog-core (~> 2.1)
|
fog-core (~> 2.1)
|
||||||
fog-json (>= 1.0)
|
fog-json (>= 1.0)
|
||||||
formatador (1.1.0)
|
formatador (1.1.1)
|
||||||
forwardable (1.3.3)
|
forwardable (1.3.3)
|
||||||
fugit (1.11.1)
|
fugit (1.11.1)
|
||||||
et-orbi (~> 1, >= 1.2.11)
|
et-orbi (~> 1, >= 1.2.11)
|
||||||
raabro (~> 1.4)
|
raabro (~> 1.4)
|
||||||
globalid (1.2.1)
|
globalid (1.2.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
google-protobuf (4.31.0)
|
google-protobuf (4.31.1)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
rake (>= 13)
|
rake (>= 13)
|
||||||
googleapis-common-protos-types (1.20.0)
|
googleapis-common-protos-types (1.20.0)
|
||||||
@@ -287,21 +287,21 @@ GEM
|
|||||||
activesupport (>= 5.1)
|
activesupport (>= 5.1)
|
||||||
haml (>= 4.0.6)
|
haml (>= 4.0.6)
|
||||||
railties (>= 5.1)
|
railties (>= 5.1)
|
||||||
haml_lint (0.64.0)
|
haml_lint (0.66.0)
|
||||||
haml (>= 5.0)
|
haml (>= 5.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
rainbow
|
rainbow
|
||||||
rubocop (>= 1.0)
|
rubocop (>= 1.0)
|
||||||
sysexits (~> 1.1)
|
sysexits (~> 1.1)
|
||||||
hashdiff (1.1.2)
|
hashdiff (1.2.0)
|
||||||
hashie (5.0.0)
|
hashie (5.0.0)
|
||||||
hcaptcha (7.1.0)
|
hcaptcha (7.1.0)
|
||||||
json
|
json
|
||||||
highline (3.1.2)
|
highline (3.1.2)
|
||||||
reline
|
reline
|
||||||
hiredis (0.6.3)
|
hiredis (0.6.3)
|
||||||
hiredis-client (0.24.0)
|
hiredis-client (0.25.3)
|
||||||
redis-client (= 0.24.0)
|
redis-client (= 0.25.3)
|
||||||
hkdf (0.3.0)
|
hkdf (0.3.0)
|
||||||
htmlentities (4.3.4)
|
htmlentities (4.3.4)
|
||||||
http (5.3.1)
|
http (5.3.1)
|
||||||
@@ -315,7 +315,7 @@ GEM
|
|||||||
http_accept_language (2.1.1)
|
http_accept_language (2.1.1)
|
||||||
httpclient (2.9.0)
|
httpclient (2.9.0)
|
||||||
mutex_m
|
mutex_m
|
||||||
httplog (1.7.0)
|
httplog (1.7.3)
|
||||||
rack (>= 2.0)
|
rack (>= 2.0)
|
||||||
rainbow (>= 2.0.0)
|
rainbow (>= 2.0.0)
|
||||||
i18n (1.14.7)
|
i18n (1.14.7)
|
||||||
@@ -335,7 +335,7 @@ GEM
|
|||||||
inline_svg (1.10.0)
|
inline_svg (1.10.0)
|
||||||
activesupport (>= 3.0)
|
activesupport (>= 3.0)
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
io-console (0.8.0)
|
io-console (0.8.1)
|
||||||
irb (1.15.2)
|
irb (1.15.2)
|
||||||
pp (>= 0.6.0)
|
pp (>= 0.6.0)
|
||||||
rdoc (>= 4.0.0)
|
rdoc (>= 4.0.0)
|
||||||
@@ -345,7 +345,7 @@ GEM
|
|||||||
azure-blob (~> 0.5.2)
|
azure-blob (~> 0.5.2)
|
||||||
hashie (~> 5.0)
|
hashie (~> 5.0)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.12.2)
|
json (2.13.2)
|
||||||
json-canonicalization (1.0.0)
|
json-canonicalization (1.0.0)
|
||||||
json-jwt (1.16.7)
|
json-jwt (1.16.7)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 4.2)
|
||||||
@@ -362,14 +362,14 @@ GEM
|
|||||||
rack (>= 2.2, < 4)
|
rack (>= 2.2, < 4)
|
||||||
rdf (~> 3.3)
|
rdf (~> 3.3)
|
||||||
rexml (~> 3.2)
|
rexml (~> 3.2)
|
||||||
json-ld-preloaded (3.3.1)
|
json-ld-preloaded (3.3.2)
|
||||||
json-ld (~> 3.3)
|
json-ld (~> 3.3)
|
||||||
rdf (~> 3.3)
|
rdf (~> 3.3)
|
||||||
json-schema (5.1.1)
|
json-schema (6.0.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
bigdecimal (~> 3.1)
|
bigdecimal (~> 3.1)
|
||||||
jsonapi-renderer (0.2.2)
|
jsonapi-renderer (0.2.2)
|
||||||
jwt (2.10.1)
|
jwt (2.10.2)
|
||||||
base64
|
base64
|
||||||
kaminari (1.2.2)
|
kaminari (1.2.2)
|
||||||
activesupport (>= 4.1.0)
|
activesupport (>= 4.1.0)
|
||||||
@@ -403,7 +403,7 @@ GEM
|
|||||||
rexml
|
rexml
|
||||||
link_header (0.0.8)
|
link_header (0.0.8)
|
||||||
lint_roller (1.1.0)
|
lint_roller (1.1.0)
|
||||||
linzer (0.7.3)
|
linzer (0.7.7)
|
||||||
cgi (~> 0.4.2)
|
cgi (~> 0.4.2)
|
||||||
forwardable (~> 1.3, >= 1.3.3)
|
forwardable (~> 1.3, >= 1.3.3)
|
||||||
logger (~> 1.7, >= 1.7.0)
|
logger (~> 1.7, >= 1.7.0)
|
||||||
@@ -433,24 +433,26 @@ GEM
|
|||||||
marcel (1.0.4)
|
marcel (1.0.4)
|
||||||
mario-redis-lock (1.2.1)
|
mario-redis-lock (1.2.1)
|
||||||
redis (>= 3.0.5)
|
redis (>= 3.0.5)
|
||||||
matrix (0.4.2)
|
matrix (0.4.3)
|
||||||
memory_profiler (1.1.0)
|
memory_profiler (1.1.0)
|
||||||
mime-types (3.7.0)
|
mime-types (3.7.0)
|
||||||
logger
|
logger
|
||||||
mime-types-data (~> 3.2025, >= 3.2025.0507)
|
mime-types-data (~> 3.2025, >= 3.2025.0507)
|
||||||
mime-types-data (3.2025.0514)
|
mime-types-data (3.2025.0916)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.9)
|
mini_portile2 (2.8.9)
|
||||||
minitest (5.25.5)
|
minitest (5.25.5)
|
||||||
msgpack (1.8.0)
|
msgpack (1.8.0)
|
||||||
multi_json (1.15.0)
|
multi_json (1.17.0)
|
||||||
mutex_m (0.3.0)
|
mutex_m (0.3.0)
|
||||||
net-http (0.6.0)
|
net-http (0.6.0)
|
||||||
uri
|
uri
|
||||||
net-imap (0.5.8)
|
net-imap (0.5.9)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-ldap (0.19.0)
|
net-ldap (0.20.0)
|
||||||
|
base64
|
||||||
|
ostruct
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
net-protocol
|
net-protocol
|
||||||
net-protocol (0.2.2)
|
net-protocol (0.2.2)
|
||||||
@@ -458,7 +460,7 @@ GEM
|
|||||||
net-smtp (0.5.1)
|
net-smtp (0.5.1)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.7.4)
|
nio4r (2.7.4)
|
||||||
nokogiri (1.18.8)
|
nokogiri (1.18.10)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oj (3.16.11)
|
oj (3.16.11)
|
||||||
@@ -468,7 +470,7 @@ GEM
|
|||||||
hashie (>= 3.4.6)
|
hashie (>= 3.4.6)
|
||||||
rack (>= 2.2.3)
|
rack (>= 2.2.3)
|
||||||
rack-protection
|
rack-protection
|
||||||
omniauth-cas (3.0.1)
|
omniauth-cas (3.0.2)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
nokogiri (~> 1.12)
|
nokogiri (~> 1.12)
|
||||||
omniauth (~> 2.1)
|
omniauth (~> 2.1)
|
||||||
@@ -497,7 +499,7 @@ GEM
|
|||||||
openssl (3.3.0)
|
openssl (3.3.0)
|
||||||
openssl-signature_algorithm (1.3.0)
|
openssl-signature_algorithm (1.3.0)
|
||||||
openssl (> 2.0)
|
openssl (> 2.0)
|
||||||
opentelemetry-api (1.5.0)
|
opentelemetry-api (1.7.0)
|
||||||
opentelemetry-common (0.22.0)
|
opentelemetry-common (0.22.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-exporter-otlp (0.30.0)
|
opentelemetry-exporter-otlp (0.30.0)
|
||||||
@@ -515,7 +517,7 @@ GEM
|
|||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-active_support (~> 0.7)
|
opentelemetry-instrumentation-active_support (~> 0.7)
|
||||||
opentelemetry-instrumentation-base (~> 0.23.0)
|
opentelemetry-instrumentation-base (~> 0.23.0)
|
||||||
opentelemetry-instrumentation-action_pack (0.12.1)
|
opentelemetry-instrumentation-action_pack (0.13.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.23.0)
|
opentelemetry-instrumentation-base (~> 0.23.0)
|
||||||
opentelemetry-instrumentation-rack (~> 0.21)
|
opentelemetry-instrumentation-rack (~> 0.21)
|
||||||
@@ -547,19 +549,19 @@ GEM
|
|||||||
opentelemetry-instrumentation-concurrent_ruby (0.22.0)
|
opentelemetry-instrumentation-concurrent_ruby (0.22.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.23.0)
|
opentelemetry-instrumentation-base (~> 0.23.0)
|
||||||
opentelemetry-instrumentation-excon (0.23.0)
|
opentelemetry-instrumentation-excon (0.24.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.23.0)
|
opentelemetry-instrumentation-base (~> 0.23.0)
|
||||||
opentelemetry-instrumentation-faraday (0.27.0)
|
opentelemetry-instrumentation-faraday (0.28.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.23.0)
|
opentelemetry-instrumentation-base (~> 0.23.0)
|
||||||
opentelemetry-instrumentation-http (0.25.0)
|
opentelemetry-instrumentation-http (0.25.1)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.23.0)
|
opentelemetry-instrumentation-base (~> 0.23.0)
|
||||||
opentelemetry-instrumentation-http_client (0.23.0)
|
opentelemetry-instrumentation-http_client (0.24.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.23.0)
|
opentelemetry-instrumentation-base (~> 0.23.0)
|
||||||
opentelemetry-instrumentation-net_http (0.23.0)
|
opentelemetry-instrumentation-net_http (0.24.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.23.0)
|
opentelemetry-instrumentation-base (~> 0.23.0)
|
||||||
opentelemetry-instrumentation-pg (0.30.1)
|
opentelemetry-instrumentation-pg (0.30.1)
|
||||||
@@ -567,13 +569,13 @@ GEM
|
|||||||
opentelemetry-helpers-sql
|
opentelemetry-helpers-sql
|
||||||
opentelemetry-helpers-sql-obfuscation
|
opentelemetry-helpers-sql-obfuscation
|
||||||
opentelemetry-instrumentation-base (~> 0.23.0)
|
opentelemetry-instrumentation-base (~> 0.23.0)
|
||||||
opentelemetry-instrumentation-rack (0.26.0)
|
opentelemetry-instrumentation-rack (0.27.1)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.23.0)
|
opentelemetry-instrumentation-base (~> 0.23.0)
|
||||||
opentelemetry-instrumentation-rails (0.36.0)
|
opentelemetry-instrumentation-rails (0.37.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-action_mailer (~> 0.4.0)
|
opentelemetry-instrumentation-action_mailer (~> 0.4.0)
|
||||||
opentelemetry-instrumentation-action_pack (~> 0.12.0)
|
opentelemetry-instrumentation-action_pack (~> 0.13.0)
|
||||||
opentelemetry-instrumentation-action_view (~> 0.9.0)
|
opentelemetry-instrumentation-action_view (~> 0.9.0)
|
||||||
opentelemetry-instrumentation-active_job (~> 0.8.0)
|
opentelemetry-instrumentation-active_job (~> 0.8.0)
|
||||||
opentelemetry-instrumentation-active_record (~> 0.9.0)
|
opentelemetry-instrumentation-active_record (~> 0.9.0)
|
||||||
@@ -589,28 +591,28 @@ GEM
|
|||||||
opentelemetry-instrumentation-base (~> 0.23.0)
|
opentelemetry-instrumentation-base (~> 0.23.0)
|
||||||
opentelemetry-registry (0.4.0)
|
opentelemetry-registry (0.4.0)
|
||||||
opentelemetry-api (~> 1.1)
|
opentelemetry-api (~> 1.1)
|
||||||
opentelemetry-sdk (1.8.0)
|
opentelemetry-sdk (1.9.0)
|
||||||
opentelemetry-api (~> 1.1)
|
opentelemetry-api (~> 1.1)
|
||||||
opentelemetry-common (~> 0.20)
|
opentelemetry-common (~> 0.20)
|
||||||
opentelemetry-registry (~> 0.2)
|
opentelemetry-registry (~> 0.2)
|
||||||
opentelemetry-semantic_conventions
|
opentelemetry-semantic_conventions
|
||||||
opentelemetry-semantic_conventions (1.11.0)
|
opentelemetry-semantic_conventions (1.36.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ostruct (0.6.1)
|
ostruct (0.6.3)
|
||||||
ox (2.14.23)
|
ox (2.14.23)
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.0)
|
||||||
parallel (1.27.0)
|
parallel (1.27.0)
|
||||||
parser (3.3.8.0)
|
parser (3.3.9.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
parslet (2.0.0)
|
parslet (2.0.0)
|
||||||
pastel (0.8.0)
|
pastel (0.8.0)
|
||||||
tty-color (~> 0.5)
|
tty-color (~> 0.5)
|
||||||
pg (1.5.9)
|
pg (1.6.2)
|
||||||
pghero (3.7.0)
|
pghero (3.7.0)
|
||||||
activerecord (>= 7.1)
|
activerecord (>= 7.1)
|
||||||
playwright-ruby-client (1.52.0)
|
playwright-ruby-client (1.55.0)
|
||||||
concurrent-ruby (>= 1.1.6)
|
concurrent-ruby (>= 1.1.6)
|
||||||
mime-types (>= 3.0)
|
mime-types (>= 3.0)
|
||||||
pp (0.6.2)
|
pp (0.6.2)
|
||||||
@@ -625,20 +627,19 @@ GEM
|
|||||||
premailer (~> 1.7, >= 1.7.9)
|
premailer (~> 1.7, >= 1.7.9)
|
||||||
prettyprint (0.2.0)
|
prettyprint (0.2.0)
|
||||||
prism (1.4.0)
|
prism (1.4.0)
|
||||||
prometheus_exporter (2.2.0)
|
prometheus_exporter (2.3.0)
|
||||||
webrick
|
webrick
|
||||||
propshaft (1.1.0)
|
propshaft (1.2.1)
|
||||||
actionpack (>= 7.0.0)
|
actionpack (>= 7.0.0)
|
||||||
activesupport (>= 7.0.0)
|
activesupport (>= 7.0.0)
|
||||||
rack
|
rack
|
||||||
railties (>= 7.0.0)
|
|
||||||
psych (5.2.6)
|
psych (5.2.6)
|
||||||
date
|
date
|
||||||
stringio
|
stringio
|
||||||
public_suffix (6.0.2)
|
public_suffix (6.0.2)
|
||||||
puma (6.6.0)
|
puma (6.6.1)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.5.0)
|
pundit (2.5.1)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
@@ -668,33 +669,33 @@ GEM
|
|||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rackup (2.2.1)
|
rackup (2.2.1)
|
||||||
rack (>= 3)
|
rack (>= 3)
|
||||||
rails (8.0.2)
|
rails (8.0.2.1)
|
||||||
actioncable (= 8.0.2)
|
actioncable (= 8.0.2.1)
|
||||||
actionmailbox (= 8.0.2)
|
actionmailbox (= 8.0.2.1)
|
||||||
actionmailer (= 8.0.2)
|
actionmailer (= 8.0.2.1)
|
||||||
actionpack (= 8.0.2)
|
actionpack (= 8.0.2.1)
|
||||||
actiontext (= 8.0.2)
|
actiontext (= 8.0.2.1)
|
||||||
actionview (= 8.0.2)
|
actionview (= 8.0.2.1)
|
||||||
activejob (= 8.0.2)
|
activejob (= 8.0.2.1)
|
||||||
activemodel (= 8.0.2)
|
activemodel (= 8.0.2.1)
|
||||||
activerecord (= 8.0.2)
|
activerecord (= 8.0.2.1)
|
||||||
activestorage (= 8.0.2)
|
activestorage (= 8.0.2.1)
|
||||||
activesupport (= 8.0.2)
|
activesupport (= 8.0.2.1)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 8.0.2)
|
railties (= 8.0.2.1)
|
||||||
rails-dom-testing (2.2.0)
|
rails-dom-testing (2.3.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.6.2)
|
rails-html-sanitizer (1.6.2)
|
||||||
loofah (~> 2.21)
|
loofah (~> 2.21)
|
||||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||||
rails-i18n (8.0.1)
|
rails-i18n (8.0.2)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 8.0.0, < 9)
|
railties (>= 8.0.0, < 9)
|
||||||
railties (8.0.2)
|
railties (8.0.2.1)
|
||||||
actionpack (= 8.0.2)
|
actionpack (= 8.0.2.1)
|
||||||
activesupport (= 8.0.2)
|
activesupport (= 8.0.2.1)
|
||||||
irb (~> 1.13)
|
irb (~> 1.13)
|
||||||
rackup (>= 1.0.0)
|
rackup (>= 1.0.0)
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
@@ -702,42 +703,45 @@ GEM
|
|||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.3.0)
|
rake (13.3.0)
|
||||||
rdf (3.3.2)
|
rdf (3.3.4)
|
||||||
bcp47_spec (~> 0.2)
|
bcp47_spec (~> 0.2)
|
||||||
bigdecimal (~> 3.1, >= 3.1.5)
|
bigdecimal (~> 3.1, >= 3.1.5)
|
||||||
link_header (~> 0.0, >= 0.0.8)
|
link_header (~> 0.0, >= 0.0.8)
|
||||||
|
logger (~> 1.5)
|
||||||
|
ostruct (~> 0.6)
|
||||||
|
readline (~> 0.0)
|
||||||
rdf-normalize (0.7.0)
|
rdf-normalize (0.7.0)
|
||||||
rdf (~> 3.3)
|
rdf (~> 3.3)
|
||||||
rdoc (6.14.1)
|
rdoc (6.14.2)
|
||||||
erb
|
erb
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
|
readline (0.0.4)
|
||||||
|
reline
|
||||||
redcarpet (3.6.1)
|
redcarpet (3.6.1)
|
||||||
redis (4.8.1)
|
redis (4.8.1)
|
||||||
redis-client (0.24.0)
|
redis-client (0.25.3)
|
||||||
connection_pool
|
connection_pool
|
||||||
redlock (1.3.2)
|
regexp_parser (2.11.2)
|
||||||
redis (>= 3.0.0, < 6.0)
|
reline (0.6.2)
|
||||||
regexp_parser (2.10.0)
|
|
||||||
reline (0.6.1)
|
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
request_store (1.7.0)
|
request_store (1.7.0)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.1.1)
|
responders (3.1.1)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rexml (3.4.1)
|
rexml (3.4.4)
|
||||||
rotp (6.3.0)
|
rotp (6.3.0)
|
||||||
rouge (4.5.2)
|
rouge (4.6.0)
|
||||||
rpam2 (4.0.2)
|
rpam2 (4.0.2)
|
||||||
rqrcode (3.1.0)
|
rqrcode (3.1.0)
|
||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
rqrcode_core (~> 2.0)
|
rqrcode_core (~> 2.0)
|
||||||
rqrcode_core (2.0.0)
|
rqrcode_core (2.0.0)
|
||||||
rspec (3.13.0)
|
rspec (3.13.1)
|
||||||
rspec-core (~> 3.13.0)
|
rspec-core (~> 3.13.0)
|
||||||
rspec-expectations (~> 3.13.0)
|
rspec-expectations (~> 3.13.0)
|
||||||
rspec-mocks (~> 3.13.0)
|
rspec-mocks (~> 3.13.0)
|
||||||
rspec-core (3.13.4)
|
rspec-core (3.13.5)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-expectations (3.13.5)
|
rspec-expectations (3.13.5)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
@@ -747,7 +751,7 @@ GEM
|
|||||||
rspec-mocks (3.13.5)
|
rspec-mocks (3.13.5)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-rails (8.0.1)
|
rspec-rails (8.0.2)
|
||||||
actionpack (>= 7.2)
|
actionpack (>= 7.2)
|
||||||
activesupport (>= 7.2)
|
activesupport (>= 7.2)
|
||||||
railties (>= 7.2)
|
railties (>= 7.2)
|
||||||
@@ -755,13 +759,13 @@ GEM
|
|||||||
rspec-expectations (~> 3.13)
|
rspec-expectations (~> 3.13)
|
||||||
rspec-mocks (~> 3.13)
|
rspec-mocks (~> 3.13)
|
||||||
rspec-support (~> 3.13)
|
rspec-support (~> 3.13)
|
||||||
rspec-sidekiq (5.1.0)
|
rspec-sidekiq (5.2.0)
|
||||||
rspec-core (~> 3.0)
|
rspec-core (~> 3.0)
|
||||||
rspec-expectations (~> 3.0)
|
rspec-expectations (~> 3.0)
|
||||||
rspec-mocks (~> 3.0)
|
rspec-mocks (~> 3.0)
|
||||||
sidekiq (>= 5, < 9)
|
sidekiq (>= 5, < 9)
|
||||||
rspec-support (3.13.4)
|
rspec-support (3.13.4)
|
||||||
rubocop (1.77.0)
|
rubocop (1.80.2)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (~> 3.17.0.2)
|
language_server-protocol (~> 3.17.0.2)
|
||||||
lint_roller (~> 1.1.0)
|
lint_roller (~> 1.1.0)
|
||||||
@@ -769,10 +773,10 @@ GEM
|
|||||||
parser (>= 3.3.0.2)
|
parser (>= 3.3.0.2)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 2.9.3, < 3.0)
|
regexp_parser (>= 2.9.3, < 3.0)
|
||||||
rubocop-ast (>= 1.45.1, < 2.0)
|
rubocop-ast (>= 1.46.0, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 4.0)
|
unicode-display_width (>= 2.4.0, < 4.0)
|
||||||
rubocop-ast (1.45.1)
|
rubocop-ast (1.46.0)
|
||||||
parser (>= 3.3.7.2)
|
parser (>= 3.3.7.2)
|
||||||
prism (~> 1.4)
|
prism (~> 1.4)
|
||||||
rubocop-capybara (2.22.1)
|
rubocop-capybara (2.22.1)
|
||||||
@@ -781,17 +785,17 @@ GEM
|
|||||||
rubocop-i18n (3.2.3)
|
rubocop-i18n (3.2.3)
|
||||||
lint_roller (~> 1.1)
|
lint_roller (~> 1.1)
|
||||||
rubocop (>= 1.72.1)
|
rubocop (>= 1.72.1)
|
||||||
rubocop-performance (1.25.0)
|
rubocop-performance (1.26.0)
|
||||||
lint_roller (~> 1.1)
|
lint_roller (~> 1.1)
|
||||||
rubocop (>= 1.75.0, < 2.0)
|
rubocop (>= 1.75.0, < 2.0)
|
||||||
rubocop-ast (>= 1.38.0, < 2.0)
|
rubocop-ast (>= 1.44.0, < 2.0)
|
||||||
rubocop-rails (2.32.0)
|
rubocop-rails (2.33.3)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
lint_roller (~> 1.1)
|
lint_roller (~> 1.1)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.75.0, < 2.0)
|
rubocop (>= 1.75.0, < 2.0)
|
||||||
rubocop-ast (>= 1.44.0, < 2.0)
|
rubocop-ast (>= 1.44.0, < 2.0)
|
||||||
rubocop-rspec (3.6.0)
|
rubocop-rspec (3.7.0)
|
||||||
lint_roller (~> 1.1)
|
lint_roller (~> 1.1)
|
||||||
rubocop (~> 1.72, >= 1.72.1)
|
rubocop (~> 1.72, >= 1.72.1)
|
||||||
rubocop-rspec_rails (2.31.0)
|
rubocop-rspec_rails (2.31.0)
|
||||||
@@ -801,13 +805,13 @@ GEM
|
|||||||
ruby-prof (1.7.2)
|
ruby-prof (1.7.2)
|
||||||
base64
|
base64
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
ruby-saml (1.18.0)
|
ruby-saml (1.18.1)
|
||||||
nokogiri (>= 1.13.10)
|
nokogiri (>= 1.13.10)
|
||||||
rexml
|
rexml
|
||||||
ruby-vips (2.2.4)
|
ruby-vips (2.2.5)
|
||||||
ffi (~> 1.12)
|
ffi (~> 1.12)
|
||||||
logger
|
logger
|
||||||
rubyzip (2.4.1)
|
rubyzip (3.1.0)
|
||||||
rufus-scheduler (3.9.2)
|
rufus-scheduler (3.9.2)
|
||||||
fugit (~> 1.1, >= 1.11.1)
|
fugit (~> 1.1, >= 1.11.1)
|
||||||
safety_net_attestation (0.4.0)
|
safety_net_attestation (0.4.0)
|
||||||
@@ -815,24 +819,23 @@ GEM
|
|||||||
sanitize (7.0.0)
|
sanitize (7.0.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.16.8)
|
nokogiri (>= 1.16.8)
|
||||||
scenic (1.8.0)
|
scenic (1.9.0)
|
||||||
activerecord (>= 4.0.0)
|
activerecord (>= 4.0.0)
|
||||||
railties (>= 4.0.0)
|
railties (>= 4.0.0)
|
||||||
securerandom (0.4.1)
|
securerandom (0.4.1)
|
||||||
shoulda-matchers (6.5.0)
|
shoulda-matchers (6.5.0)
|
||||||
activesupport (>= 5.2.0)
|
activesupport (>= 5.2.0)
|
||||||
sidekiq (7.3.9)
|
sidekiq (8.0.7)
|
||||||
base64
|
connection_pool (>= 2.5.0)
|
||||||
connection_pool (>= 2.3.0)
|
json (>= 2.9.0)
|
||||||
logger
|
logger (>= 1.6.2)
|
||||||
rack (>= 2.2.4)
|
rack (>= 3.1.0)
|
||||||
redis-client (>= 0.22.2)
|
redis-client (>= 0.23.2)
|
||||||
sidekiq-bulk (0.2.0)
|
sidekiq-bulk (0.2.0)
|
||||||
sidekiq
|
sidekiq
|
||||||
sidekiq-scheduler (5.0.6)
|
sidekiq-scheduler (6.0.1)
|
||||||
rufus-scheduler (~> 3.2)
|
rufus-scheduler (~> 3.2)
|
||||||
sidekiq (>= 6, < 8)
|
sidekiq (>= 7.3, < 9)
|
||||||
tilt (>= 1.4.0, < 3)
|
|
||||||
sidekiq-unique-jobs (8.0.11)
|
sidekiq-unique-jobs (8.0.11)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||||
sidekiq (>= 7.0.0, < 9.0.0)
|
sidekiq (>= 7.0.0, < 9.0.0)
|
||||||
@@ -846,16 +849,16 @@ GEM
|
|||||||
docile (~> 1.1)
|
docile (~> 1.1)
|
||||||
simplecov-html (~> 0.11)
|
simplecov-html (~> 0.11)
|
||||||
simplecov_json_formatter (~> 0.1)
|
simplecov_json_formatter (~> 0.1)
|
||||||
simplecov-html (0.13.1)
|
simplecov-html (0.13.2)
|
||||||
simplecov-lcov (0.8.0)
|
simplecov-lcov (0.9.0)
|
||||||
simplecov_json_formatter (0.1.4)
|
simplecov_json_formatter (0.1.4)
|
||||||
stackprof (0.2.27)
|
stackprof (0.2.27)
|
||||||
starry (0.2.0)
|
starry (0.2.0)
|
||||||
base64
|
base64
|
||||||
stoplight (4.1.1)
|
stoplight (5.3.8)
|
||||||
redlock (~> 1.0)
|
zeitwerk
|
||||||
stringio (3.1.7)
|
stringio (3.1.7)
|
||||||
strong_migrations (2.4.0)
|
strong_migrations (2.5.0)
|
||||||
activerecord (>= 7.1)
|
activerecord (>= 7.1)
|
||||||
swd (2.0.3)
|
swd (2.0.3)
|
||||||
activesupport (>= 3)
|
activesupport (>= 3)
|
||||||
@@ -863,14 +866,14 @@ GEM
|
|||||||
faraday (~> 2.0)
|
faraday (~> 2.0)
|
||||||
faraday-follow_redirects
|
faraday-follow_redirects
|
||||||
sysexits (1.2.0)
|
sysexits (1.2.0)
|
||||||
temple (0.10.3)
|
temple (0.10.4)
|
||||||
terminal-table (4.0.0)
|
terminal-table (4.0.0)
|
||||||
unicode-display_width (>= 1.1.1, < 4)
|
unicode-display_width (>= 1.1.1, < 4)
|
||||||
terrapin (1.1.0)
|
terrapin (1.1.1)
|
||||||
climate_control
|
climate_control
|
||||||
test-prof (1.4.4)
|
test-prof (1.4.4)
|
||||||
thor (1.3.2)
|
thor (1.4.0)
|
||||||
tilt (2.6.0)
|
tilt (2.6.1)
|
||||||
timeout (0.4.3)
|
timeout (0.4.3)
|
||||||
tpm-key_attestation (0.14.1)
|
tpm-key_attestation (0.14.1)
|
||||||
bindata (~> 2.4)
|
bindata (~> 2.4)
|
||||||
@@ -896,7 +899,7 @@ GEM
|
|||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.9.1)
|
unf_ext (0.0.9.1)
|
||||||
unicode-display_width (3.1.4)
|
unicode-display_width (3.1.5)
|
||||||
unicode-emoji (~> 4.0, >= 4.0.4)
|
unicode-emoji (~> 4.0, >= 4.0.4)
|
||||||
unicode-emoji (4.0.4)
|
unicode-emoji (4.0.4)
|
||||||
uri (1.0.3)
|
uri (1.0.3)
|
||||||
@@ -932,7 +935,7 @@ GEM
|
|||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
webrick (1.9.1)
|
webrick (1.9.1)
|
||||||
websocket-driver (0.7.7)
|
websocket-driver (0.8.0)
|
||||||
base64
|
base64
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
@@ -1002,13 +1005,13 @@ DEPENDENCIES
|
|||||||
jd-paperclip-azure (~> 3.0)
|
jd-paperclip-azure (~> 3.0)
|
||||||
json-ld
|
json-ld
|
||||||
json-ld-preloaded (~> 3.2)
|
json-ld-preloaded (~> 3.2)
|
||||||
json-schema (~> 5.0)
|
json-schema (~> 6.0)
|
||||||
kaminari (~> 1.2)
|
kaminari (~> 1.2)
|
||||||
kt-paperclip (~> 7.2)
|
kt-paperclip (~> 7.2)
|
||||||
letter_opener (~> 1.8)
|
letter_opener (~> 1.8)
|
||||||
letter_opener_web (~> 3.0)
|
letter_opener_web (~> 3.0)
|
||||||
link_header (~> 0.0)
|
link_header (~> 0.0)
|
||||||
linzer (~> 0.7.2)
|
linzer (~> 0.7.7)
|
||||||
lograge (~> 0.12)
|
lograge (~> 0.12)
|
||||||
mail (~> 2.8)
|
mail (~> 2.8)
|
||||||
mario-redis-lock (~> 1.2)
|
mario-redis-lock (~> 1.2)
|
||||||
@@ -1024,19 +1027,19 @@ DEPENDENCIES
|
|||||||
omniauth-rails_csrf_protection (~> 1.0)
|
omniauth-rails_csrf_protection (~> 1.0)
|
||||||
omniauth-saml (~> 2.0)
|
omniauth-saml (~> 2.0)
|
||||||
omniauth_openid_connect (~> 0.8.0)
|
omniauth_openid_connect (~> 0.8.0)
|
||||||
opentelemetry-api (~> 1.5.0)
|
opentelemetry-api (~> 1.7.0)
|
||||||
opentelemetry-exporter-otlp (~> 0.30.0)
|
opentelemetry-exporter-otlp (~> 0.30.0)
|
||||||
opentelemetry-instrumentation-active_job (~> 0.8.0)
|
opentelemetry-instrumentation-active_job (~> 0.8.0)
|
||||||
opentelemetry-instrumentation-active_model_serializers (~> 0.22.0)
|
opentelemetry-instrumentation-active_model_serializers (~> 0.22.0)
|
||||||
opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0)
|
opentelemetry-instrumentation-concurrent_ruby (~> 0.22.0)
|
||||||
opentelemetry-instrumentation-excon (~> 0.23.0)
|
opentelemetry-instrumentation-excon (~> 0.24.0)
|
||||||
opentelemetry-instrumentation-faraday (~> 0.27.0)
|
opentelemetry-instrumentation-faraday (~> 0.28.0)
|
||||||
opentelemetry-instrumentation-http (~> 0.25.0)
|
opentelemetry-instrumentation-http (~> 0.25.0)
|
||||||
opentelemetry-instrumentation-http_client (~> 0.23.0)
|
opentelemetry-instrumentation-http_client (~> 0.24.0)
|
||||||
opentelemetry-instrumentation-net_http (~> 0.23.0)
|
opentelemetry-instrumentation-net_http (~> 0.24.0)
|
||||||
opentelemetry-instrumentation-pg (~> 0.30.0)
|
opentelemetry-instrumentation-pg (~> 0.30.0)
|
||||||
opentelemetry-instrumentation-rack (~> 0.26.0)
|
opentelemetry-instrumentation-rack (~> 0.27.0)
|
||||||
opentelemetry-instrumentation-rails (~> 0.36.0)
|
opentelemetry-instrumentation-rails (~> 0.37.0)
|
||||||
opentelemetry-instrumentation-redis (~> 0.26.0)
|
opentelemetry-instrumentation-redis (~> 0.26.0)
|
||||||
opentelemetry-instrumentation-sidekiq (~> 0.26.0)
|
opentelemetry-instrumentation-sidekiq (~> 0.26.0)
|
||||||
opentelemetry-sdk (~> 1.4)
|
opentelemetry-sdk (~> 1.4)
|
||||||
@@ -1044,6 +1047,7 @@ DEPENDENCIES
|
|||||||
parslet
|
parslet
|
||||||
pg (~> 1.5)
|
pg (~> 1.5)
|
||||||
pghero
|
pghero
|
||||||
|
playwright-ruby-client (= 1.55.0)
|
||||||
premailer-rails
|
premailer-rails
|
||||||
prometheus_exporter (~> 2.2)
|
prometheus_exporter (~> 2.2)
|
||||||
propshaft
|
propshaft
|
||||||
@@ -1072,20 +1076,20 @@ DEPENDENCIES
|
|||||||
ruby-prof
|
ruby-prof
|
||||||
ruby-progressbar (~> 1.13)
|
ruby-progressbar (~> 1.13)
|
||||||
ruby-vips (~> 2.2)
|
ruby-vips (~> 2.2)
|
||||||
rubyzip (~> 2.3)
|
rubyzip (~> 3.0)
|
||||||
sanitize (~> 7.0)
|
sanitize (~> 7.0)
|
||||||
scenic (~> 1.7)
|
scenic (~> 1.7)
|
||||||
shoulda-matchers
|
shoulda-matchers
|
||||||
sidekiq (< 8)
|
sidekiq (< 9)
|
||||||
sidekiq-bulk (~> 0.2.0)
|
sidekiq-bulk (~> 0.2.0)
|
||||||
sidekiq-scheduler (~> 5.0)
|
sidekiq-scheduler (~> 6.0)
|
||||||
sidekiq-unique-jobs (> 8)
|
sidekiq-unique-jobs (> 8)
|
||||||
simple-navigation (~> 4.4)
|
simple-navigation (~> 4.4)
|
||||||
simple_form (~> 5.2)
|
simple_form (~> 5.2)
|
||||||
simplecov (~> 0.22)
|
simplecov (~> 0.22)
|
||||||
simplecov-lcov (~> 0.8)
|
simplecov-lcov (~> 0.8)
|
||||||
stackprof
|
stackprof
|
||||||
stoplight (~> 4.1)
|
stoplight
|
||||||
strong_migrations
|
strong_migrations
|
||||||
test-prof
|
test-prof
|
||||||
thor (~> 1.2)
|
thor (~> 1.2)
|
||||||
@@ -1102,4 +1106,4 @@ RUBY VERSION
|
|||||||
ruby 3.4.1p0
|
ruby 3.4.1p0
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.6.9
|
2.7.1
|
||||||
|
|||||||
64
README.md
64
README.md
@@ -33,71 +33,71 @@ Mastodon Glitch Edition is a fork of [Mastodon](https://github.com/mastodon/mast
|
|||||||
<img src="https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg" alt="Crowdin" /></a>
|
<img src="https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg" alt="Crowdin" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Mastodon is a **free, open-source social network server** based on ActivityPub where users can follow friends and discover new ones. On Mastodon, users can publish anything they want: links, pictures, text, and video. All Mastodon servers are interoperable as a federated network (users on one server can seamlessly communicate with users from another one, including non-Mastodon software that implements ActivityPub!)
|
Mastodon is a **free, open-source social network server** based on [ActivityPub](https://www.w3.org/TR/activitypub/) where users can follow friends and discover new ones. On Mastodon, users can publish anything they want: links, pictures, text, and video. All Mastodon servers are interoperable as a federated network (users on one server can seamlessly communicate with users from another one, including non-Mastodon software that implements ActivityPub!)
|
||||||
|
|
||||||
## Navigation
|
## Navigation
|
||||||
|
|
||||||
- [Project homepage 🐘](https://joinmastodon.org)
|
- [Project homepage 🐘](https://joinmastodon.org)
|
||||||
- [Support the development via Patreon][patreon]
|
- [Donate to support development 🎁](https://joinmastodon.org/sponsors#donate)
|
||||||
- [View sponsors](https://joinmastodon.org/sponsors)
|
- [View sponsors](https://joinmastodon.org/sponsors)
|
||||||
- [Blog](https://blog.joinmastodon.org)
|
- [Blog 📰](https://blog.joinmastodon.org)
|
||||||
- [Documentation](https://docs.joinmastodon.org)
|
- [Documentation 📚](https://docs.joinmastodon.org)
|
||||||
- [Roadmap](https://joinmastodon.org/roadmap)
|
- [Official container image 🚢](https://github.com/mastodon/mastodon/pkgs/container/mastodon)
|
||||||
- [Official Docker image](https://github.com/mastodon/mastodon/pkgs/container/mastodon)
|
|
||||||
- [Browse Mastodon servers](https://joinmastodon.org/communities)
|
|
||||||
- [Browse Mastodon apps](https://joinmastodon.org/apps)
|
|
||||||
|
|
||||||
[patreon]: https://www.patreon.com/mastodon
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
<img src="/app/javascript/images/elephant_ui_working.svg?raw=true" align="right" width="30%" />
|
<img src="./app/javascript/images/elephant_ui_working.svg?raw=true" align="right" width="30%" />
|
||||||
|
|
||||||
**No vendor lock-in: Fully interoperable with any conforming platform** - It doesn't have to be Mastodon; whatever implements ActivityPub is part of the social network! [Learn more](https://blog.joinmastodon.org/2018/06/why-activitypub-is-the-future/)
|
**Part of the Fediverse. Based on open standards, with no vendor lock-in.** - the network goes beyond just Mastodon; anything that implements ActivityPub is part of a broader social network known as [the Fediverse](https://jointhefediverse.net/). You can follow and interact with users on other servers (including those running different software), and they can follow you back.
|
||||||
|
|
||||||
**Real-time, chronological timeline updates** - updates of people you're following appear in real-time in the UI via WebSockets. There's a firehose view as well!
|
**Real-time, chronological timeline updates** - updates of people you're following appear in real-time in the UI.
|
||||||
|
|
||||||
**Media attachments like images and short videos** - upload and view images and WebM/MP4 videos attached to the updates. Videos with no audio track are treated like GIFs; normal videos loop continuously!
|
**Media attachments** - upload and view images and videos attached to the updates. Videos with no audio track are treated like animated GIFs; normal videos loop continuously.
|
||||||
|
|
||||||
**Safety and moderation tools** - Mastodon includes private posts, locked accounts, phrase filtering, muting, blocking, and all sorts of other features, along with a reporting and moderation system. [Learn more](https://blog.joinmastodon.org/2018/07/cage-the-mastodon/)
|
**Safety and moderation tools** - Mastodon includes private posts, locked accounts, phrase filtering, muting, blocking, and many other features, along with a reporting and moderation system.
|
||||||
|
|
||||||
**OAuth2 and a straightforward REST API** - Mastodon acts as an OAuth2 provider, so 3rd party apps can use the REST and Streaming APIs. This results in a rich app ecosystem with a lot of choices!
|
**OAuth2 and a straightforward REST API** - Mastodon acts as an OAuth2 provider, and third party apps can use the REST and Streaming APIs. This results in a [rich app ecosystem](https://joinmastodon.org/apps) with a variety of choices!
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
### Tech stack
|
### Tech stack
|
||||||
|
|
||||||
- **Ruby on Rails** powers the REST API and other web pages
|
- [Ruby on Rails](https://github.com/rails/rails) powers the REST API and other web pages.
|
||||||
- **React.js** and **Redux** are used for the dynamic parts of the interface
|
- [PostgreSQL](https://www.postgresql.org/) is the main database.
|
||||||
- **Node.js** powers the streaming API
|
- [Redis](https://redis.io/) and [Sidekiq](https://sidekiq.org/) are used for caching and queueing.
|
||||||
|
- [Node.js](https://nodejs.org/) powers the streaming API.
|
||||||
|
- [React.js](https://reactjs.org/) and [Redux](https://redux.js.org/) are used for the dynamic parts of the interface.
|
||||||
|
- [BrowserStack](https://www.browserstack.com/) supports testing on real devices and browsers. (This project is tested with BrowserStack)
|
||||||
|
- [Chromatic](https://www.chromatic.com/) provides visual regression testing. (This project is tested with Chromatic)
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- **PostgreSQL** 13+
|
|
||||||
- **Redis** 6.2+
|
|
||||||
- **Ruby** 3.2+
|
- **Ruby** 3.2+
|
||||||
|
- **PostgreSQL** 13+
|
||||||
|
- **Redis** 7.0+
|
||||||
- **Node.js** 20+
|
- **Node.js** 20+
|
||||||
|
|
||||||
The repository includes deployment configurations for **Docker and docker-compose** as well as specific platforms like **Heroku**, and **Scalingo**. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). The [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation.
|
This repository includes deployment configurations for **Docker and docker-compose**, as well as for other environments like Heroku and Scalingo. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). A [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the main documentation.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Mastodon is **free, open-source software** licensed under **AGPLv3**.
|
Mastodon is **free, open-source software** licensed under **AGPLv3**. We welcome contributions and help from anyone who wants to improve the project.
|
||||||
|
|
||||||
You can open issues for bugs you've found or features you think are missing. You
|
You should read the overall [CONTRIBUTING](https://github.com/mastodon/.github/blob/main/CONTRIBUTING.md) guide, which covers our development processes.
|
||||||
can also submit pull requests to this repository or translations via Crowdin. To
|
|
||||||
get started, look at the [CONTRIBUTING] and [DEVELOPMENT] guides. For changes
|
|
||||||
accepted into Mastodon, you can request to be paid through our [OpenCollective].
|
|
||||||
|
|
||||||
**IRC channel**: #mastodon on [`irc.libera.chat`](https://libera.chat)
|
You should also read and understand the [CODE OF CONDUCT](https://github.com/mastodon/.github/blob/main/CODE_OF_CONDUCT.md) that enables us to maintain a welcoming and inclusive community. Collaboration begins with mutual respect and understanding.
|
||||||
|
|
||||||
## License
|
You can learn about setting up a development environment in the [DEVELOPMENT](docs/DEVELOPMENT.md) documentation.
|
||||||
|
|
||||||
|
If you would like to help with translations 🌐 you can do so on [Crowdin](https://crowdin.com/project/mastodon).
|
||||||
|
|
||||||
|
## LICENSE
|
||||||
|
|
||||||
Copyright (c) 2016-2025 Eugen Rochko (+ [`mastodon authors`](AUTHORS.md))
|
Copyright (c) 2016-2025 Eugen Rochko (+ [`mastodon authors`](AUTHORS.md))
|
||||||
|
|
||||||
Licensed under GNU Affero General Public License as stated in the [LICENSE](LICENSE):
|
Licensed under GNU Affero General Public License as stated in the [LICENSE](LICENSE):
|
||||||
|
|
||||||
```
|
```text
|
||||||
Copyright (c) 2016-2025 Eugen Rochko & other Mastodon contributors
|
Copyright (c) 2016-2025 Eugen Rochko & other Mastodon contributors
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify it under
|
This program is free software: you can redistribute it and/or modify it under
|
||||||
@@ -113,7 +113,3 @@ details.
|
|||||||
You should have received a copy of the GNU Affero General Public License along
|
You should have received a copy of the GNU Affero General Public License along
|
||||||
with this program. If not, see https://www.gnu.org/licenses/
|
with this program. If not, see https://www.gnu.org/licenses/
|
||||||
```
|
```
|
||||||
|
|
||||||
[CONTRIBUTING]: CONTRIBUTING.md
|
|
||||||
[DEVELOPMENT]: docs/DEVELOPMENT.md
|
|
||||||
[OpenCollective]: https://opencollective.com/mastodon
|
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
|
|||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ------- | --------- |
|
| ------- | ---------------- |
|
||||||
|
| 4.4.x | Yes |
|
||||||
| 4.3.x | Yes |
|
| 4.3.x | Yes |
|
||||||
| 4.2.x | Yes |
|
| 4.2.x | Until 2026-01-08 |
|
||||||
| < 4.2 | No |
|
| < 4.2 | No |
|
||||||
|
|||||||
3
Vagrantfile
vendored
3
Vagrantfile
vendored
@@ -54,6 +54,7 @@ sudo apt-get install \
|
|||||||
pkg-config \
|
pkg-config \
|
||||||
protobuf-compiler \
|
protobuf-compiler \
|
||||||
zlib1g-dev \
|
zlib1g-dev \
|
||||||
|
libvips42t64 \
|
||||||
-y
|
-y
|
||||||
|
|
||||||
# Install rvm
|
# Install rvm
|
||||||
@@ -134,7 +135,7 @@ VAGRANTFILE_API_VERSION = "2"
|
|||||||
|
|
||||||
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
||||||
|
|
||||||
config.vm.box = "ubuntu/focal64"
|
config.vm.box = "bento/ubuntu-24.04"
|
||||||
|
|
||||||
config.vm.provider :virtualbox do |vb|
|
config.vm.provider :virtualbox do |vb|
|
||||||
vb.name = "mastodon"
|
vb.name = "mastodon"
|
||||||
|
|||||||
82
app/controllers/activitypub/contexts_controller.rb
Normal file
82
app/controllers/activitypub/contexts_controller.rb
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::ContextsController < ActivityPub::BaseController
|
||||||
|
vary_by -> { 'Signature' if authorized_fetch_mode? }
|
||||||
|
|
||||||
|
before_action :require_account_signature!, if: :authorized_fetch_mode?
|
||||||
|
before_action :set_conversation
|
||||||
|
before_action :set_items
|
||||||
|
|
||||||
|
DESCENDANTS_LIMIT = 60
|
||||||
|
|
||||||
|
def show
|
||||||
|
expires_in 3.minutes, public: public_fetch_mode?
|
||||||
|
render_with_cache json: context_presenter, serializer: ActivityPub::ContextSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
||||||
|
end
|
||||||
|
|
||||||
|
def items
|
||||||
|
expires_in 3.minutes, public: public_fetch_mode?
|
||||||
|
render_with_cache json: items_collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def account_required?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_conversation
|
||||||
|
account_id, status_id = params[:id].split('-')
|
||||||
|
@conversation = Conversation.local.find_by(parent_account_id: account_id, parent_status_id: status_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_items
|
||||||
|
@items = @conversation.statuses.distributable_visibility.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def context_presenter
|
||||||
|
first_page = ActivityPub::CollectionPresenter.new(
|
||||||
|
id: items_context_url(@conversation, page_params),
|
||||||
|
type: :unordered,
|
||||||
|
part_of: items_context_url(@conversation),
|
||||||
|
next: next_page,
|
||||||
|
items: @items.map { |status| status.local? ? ActivityPub::TagManager.instance.uri_for(status) : status.uri }
|
||||||
|
)
|
||||||
|
|
||||||
|
ActivityPub::ContextPresenter.from_conversation(@conversation).tap do |presenter|
|
||||||
|
presenter.first = first_page
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def items_collection_presenter
|
||||||
|
page = ActivityPub::CollectionPresenter.new(
|
||||||
|
id: items_context_url(@conversation, page_params),
|
||||||
|
type: :unordered,
|
||||||
|
part_of: items_context_url(@conversation),
|
||||||
|
next: next_page,
|
||||||
|
items: @items.map { |status| status.local? ? ActivityPub::TagManager.instance.uri_for(status) : status.uri }
|
||||||
|
)
|
||||||
|
|
||||||
|
return page if page_requested?
|
||||||
|
|
||||||
|
ActivityPub::CollectionPresenter.new(
|
||||||
|
id: items_context_url(@conversation),
|
||||||
|
type: :unordered,
|
||||||
|
first: page
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def page_requested?
|
||||||
|
truthy_param?(:page)
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_page
|
||||||
|
return nil if @items.size < DESCENDANTS_LIMIT
|
||||||
|
|
||||||
|
items_context_url(@conversation, page: true, min_id: @items.last.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def page_params
|
||||||
|
params.permit(:page, :min_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::QuoteAuthorizationsController < ActivityPub::BaseController
|
||||||
|
include Authorization
|
||||||
|
|
||||||
|
vary_by -> { 'Signature' if authorized_fetch_mode? }
|
||||||
|
|
||||||
|
before_action :require_account_signature!, if: :authorized_fetch_mode?
|
||||||
|
before_action :set_quote_authorization
|
||||||
|
|
||||||
|
def show
|
||||||
|
expires_in 30.seconds, public: true if @quote.status.distributable? && public_fetch_mode?
|
||||||
|
render json: @quote, serializer: ActivityPub::QuoteAuthorizationSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def pundit_user
|
||||||
|
signed_request_account
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_quote_authorization
|
||||||
|
@quote = Quote.accepted.where(quoted_account: @account).find(params[:id])
|
||||||
|
return not_found unless @quote.status.present? && @quote.quoted_status.present?
|
||||||
|
|
||||||
|
authorize @quote.status, :show?
|
||||||
|
rescue Mastodon::NotPermittedError
|
||||||
|
not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -14,17 +14,21 @@ module Admin
|
|||||||
def create
|
def create
|
||||||
authorize @account, :show?
|
authorize @account, :show?
|
||||||
|
|
||||||
account_action = Admin::AccountAction.new(resource_params)
|
@account_action = Admin::AccountAction.new(resource_params)
|
||||||
account_action.target_account = @account
|
@account_action.target_account = @account
|
||||||
account_action.current_account = current_account
|
@account_action.current_account = current_account
|
||||||
|
|
||||||
account_action.save!
|
if @account_action.save
|
||||||
|
if @account_action.with_report?
|
||||||
if account_action.with_report?
|
|
||||||
redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: resource_params[:report_id])
|
redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: resource_params[:report_id])
|
||||||
else
|
else
|
||||||
redirect_to admin_account_path(@account.id)
|
redirect_to admin_account_path(@account.id)
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
@warning_presets = AccountWarningPreset.all
|
||||||
|
|
||||||
|
render :new
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -16,11 +16,14 @@ module Admin
|
|||||||
def batch
|
def batch
|
||||||
authorize :account, :index?
|
authorize :account, :index?
|
||||||
|
|
||||||
@form = Form::AccountBatch.new(form_account_batch_params)
|
@form = Form::AccountBatch.new(
|
||||||
@form.current_account = current_account
|
form_account_batch_params.merge(
|
||||||
@form.action = action_from_button
|
action: action_from_button,
|
||||||
@form.select_all_matching = params[:select_all_matching]
|
current_account:,
|
||||||
@form.query = filtered_accounts
|
query: filtered_accounts,
|
||||||
|
select_all_matching: params[:select_all_matching]
|
||||||
|
)
|
||||||
|
)
|
||||||
@form.save
|
@form.save
|
||||||
rescue ActionController::ParameterMissing
|
rescue ActionController::ParameterMissing
|
||||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ module Admin
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
authorize :audit_log, :index?
|
authorize :audit_log, :index?
|
||||||
@auditable_accounts = Account.auditable.select(:id, :username)
|
@auditable_accounts = Account.auditable.select(:id, :username).order(username: :asc)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -19,15 +19,13 @@ module Admin
|
|||||||
|
|
||||||
log_action :resend, @user
|
log_action :resend, @user
|
||||||
|
|
||||||
flash[:notice] = I18n.t('admin.accounts.resend_confirmation.success')
|
redirect_to admin_accounts_path, notice: t('admin.accounts.resend_confirmation.success')
|
||||||
redirect_to admin_accounts_path
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def redirect_confirmed_user
|
def redirect_confirmed_user
|
||||||
flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed')
|
redirect_to admin_accounts_path, flash: { error: t('admin.accounts.resend_confirmation.already_confirmed') }
|
||||||
redirect_to admin_accounts_path
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_confirmed?
|
def user_confirmed?
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class Admin::Disputes::AppealsController < Admin::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def reject
|
def reject
|
||||||
authorize @appeal, :approve?
|
authorize @appeal, :reject?
|
||||||
log_action :reject, @appeal
|
log_action :reject, @appeal
|
||||||
@appeal.reject!(current_account)
|
@appeal.reject!(current_account)
|
||||||
UserMailer.appeal_rejected(@appeal.account.user, @appeal).deliver_later
|
UserMailer.appeal_rejected(@appeal.account.user, @appeal).deliver_later
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ module Admin
|
|||||||
end
|
end
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
authorize :domain_block, :create?
|
authorize :domain_block, :update?
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@@ -129,7 +129,7 @@ module Admin
|
|||||||
end
|
end
|
||||||
|
|
||||||
def requires_confirmation?
|
def requires_confirmation?
|
||||||
@domain_block.valid? && (@domain_block.new_record? || @domain_block.severity_changed?) && @domain_block.severity.to_s == 'suspend' && !params[:confirm]
|
@domain_block.valid? && (@domain_block.new_record? || @domain_block.severity_changed?) && @domain_block.suspend? && !params[:confirm]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ module Admin
|
|||||||
|
|
||||||
def export_data
|
def export_data
|
||||||
CSV.generate(headers: export_headers, write_headers: true) do |content|
|
CSV.generate(headers: export_headers, write_headers: true) do |content|
|
||||||
DomainAllow.allowed_domains.each do |instance|
|
DomainAllow.allowed_domains.each do |domain|
|
||||||
content << [instance.domain]
|
content << [domain]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -13,27 +13,9 @@ class Admin::Reports::ActionsController < Admin::BaseController
|
|||||||
|
|
||||||
case action_from_button
|
case action_from_button
|
||||||
when 'delete', 'mark_as_sensitive'
|
when 'delete', 'mark_as_sensitive'
|
||||||
status_batch_action = Admin::StatusBatchAction.new(
|
Admin::StatusBatchAction.new(status_batch_action_params).save!
|
||||||
type: action_from_button,
|
|
||||||
status_ids: @report.status_ids,
|
|
||||||
current_account: current_account,
|
|
||||||
report_id: @report.id,
|
|
||||||
send_email_notification: !@report.spam?,
|
|
||||||
text: params[:text]
|
|
||||||
)
|
|
||||||
|
|
||||||
status_batch_action.save!
|
|
||||||
when 'silence', 'suspend'
|
when 'silence', 'suspend'
|
||||||
account_action = Admin::AccountAction.new(
|
Admin::AccountAction.new(account_action_params).save!
|
||||||
type: action_from_button,
|
|
||||||
report_id: @report.id,
|
|
||||||
target_account: @report.target_account,
|
|
||||||
current_account: current_account,
|
|
||||||
send_email_notification: !@report.spam?,
|
|
||||||
text: params[:text]
|
|
||||||
)
|
|
||||||
|
|
||||||
account_action.save!
|
|
||||||
else
|
else
|
||||||
return redirect_to admin_report_path(@report), alert: I18n.t('admin.reports.unknown_action_msg', action: action_from_button)
|
return redirect_to admin_report_path(@report), alert: I18n.t('admin.reports.unknown_action_msg', action: action_from_button)
|
||||||
end
|
end
|
||||||
@@ -43,6 +25,26 @@ class Admin::Reports::ActionsController < Admin::BaseController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def status_batch_action_params
|
||||||
|
shared_params
|
||||||
|
.merge(status_ids: @report.status_ids)
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_action_params
|
||||||
|
shared_params
|
||||||
|
.merge(target_account: @report.target_account)
|
||||||
|
end
|
||||||
|
|
||||||
|
def shared_params
|
||||||
|
{
|
||||||
|
current_account: current_account,
|
||||||
|
report_id: @report.id,
|
||||||
|
send_email_notification: !@report.spam?,
|
||||||
|
text: params[:text],
|
||||||
|
type: action_from_button,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def set_report
|
def set_report
|
||||||
@report = Report.find(params[:report_id])
|
@report = Report.find(params[:report_id])
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ module Admin
|
|||||||
@admin_settings = Form::AdminSettings.new(settings_params)
|
@admin_settings = Form::AdminSettings.new(settings_params)
|
||||||
|
|
||||||
if @admin_settings.save
|
if @admin_settings.save
|
||||||
flash[:notice] = I18n.t('generic.changes_saved_msg')
|
redirect_to after_update_redirect_path, notice: t('generic.changes_saved_msg')
|
||||||
redirect_to after_update_redirect_path
|
|
||||||
else
|
else
|
||||||
render :show
|
render :show
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ module Admin
|
|||||||
before_action :set_tag, except: [:index]
|
before_action :set_tag, except: [:index]
|
||||||
|
|
||||||
PER_PAGE = 20
|
PER_PAGE = 20
|
||||||
|
PERIOD_DAYS = 6.days
|
||||||
|
|
||||||
def index
|
def index
|
||||||
authorize :tag, :index?
|
authorize :tag, :index?
|
||||||
@@ -15,7 +16,7 @@ module Admin
|
|||||||
def show
|
def show
|
||||||
authorize @tag, :show?
|
authorize @tag, :show?
|
||||||
|
|
||||||
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
|
@time_period = report_range
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@@ -24,7 +25,7 @@ module Admin
|
|||||||
if @tag.update(tag_params.merge(reviewed_at: Time.now.utc))
|
if @tag.update(tag_params.merge(reviewed_at: Time.now.utc))
|
||||||
redirect_to admin_tag_path(@tag.id), notice: I18n.t('admin.tags.updated_msg')
|
redirect_to admin_tag_path(@tag.id), notice: I18n.t('admin.tags.updated_msg')
|
||||||
else
|
else
|
||||||
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
|
@time_period = report_range
|
||||||
|
|
||||||
render :show
|
render :show
|
||||||
end
|
end
|
||||||
@@ -36,6 +37,10 @@ module Admin
|
|||||||
@tag = Tag.find(params[:id])
|
@tag = Tag.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def report_range
|
||||||
|
(PERIOD_DAYS.ago.to_date...Time.now.utc.to_date)
|
||||||
|
end
|
||||||
|
|
||||||
def tag_params
|
def tag_params
|
||||||
params
|
params
|
||||||
.expect(tag: [:name, :display_name, :trendable, :usable, :listable])
|
.expect(tag: [:name, :display_name, :trendable, :usable, :listable])
|
||||||
|
|||||||
77
app/controllers/admin/username_blocks_controller.rb
Normal file
77
app/controllers/admin/username_blocks_controller.rb
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::UsernameBlocksController < Admin::BaseController
|
||||||
|
before_action :set_username_block, only: [:edit, :update]
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize :username_block, :index?
|
||||||
|
@username_blocks = UsernameBlock.order(username: :asc).page(params[:page])
|
||||||
|
@form = Form::UsernameBlockBatch.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def batch
|
||||||
|
authorize :username_block, :index?
|
||||||
|
|
||||||
|
@form = Form::UsernameBlockBatch.new(form_username_block_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||||
|
@form.save
|
||||||
|
rescue ActionController::ParameterMissing
|
||||||
|
flash[:alert] = I18n.t('admin.username_blocks.no_username_block_selected')
|
||||||
|
rescue Mastodon::NotPermittedError
|
||||||
|
flash[:alert] = I18n.t('admin.username_blocks.not_permitted')
|
||||||
|
ensure
|
||||||
|
redirect_to admin_username_blocks_path
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
authorize :username_block, :create?
|
||||||
|
@username_block = UsernameBlock.new(exact: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
authorize @username_block, :update?
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize :username_block, :create?
|
||||||
|
|
||||||
|
@username_block = UsernameBlock.new(resource_params)
|
||||||
|
|
||||||
|
if @username_block.save
|
||||||
|
log_action :create, @username_block
|
||||||
|
redirect_to admin_username_blocks_path, notice: I18n.t('admin.username_blocks.created_msg')
|
||||||
|
else
|
||||||
|
render :new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
authorize @username_block, :update?
|
||||||
|
|
||||||
|
if @username_block.update(resource_params)
|
||||||
|
log_action :update, @username_block
|
||||||
|
redirect_to admin_username_blocks_path, notice: I18n.t('admin.username_blocks.updated_msg')
|
||||||
|
else
|
||||||
|
render :new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_username_block
|
||||||
|
@username_block = UsernameBlock.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def form_username_block_batch_params
|
||||||
|
params
|
||||||
|
.expect(form_username_block_batch: [username_block_ids: []])
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params
|
||||||
|
.expect(username_block: [:username, :comparison, :allow_with_approval])
|
||||||
|
end
|
||||||
|
|
||||||
|
def action_from_button
|
||||||
|
'delete' if params[:delete]
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -48,6 +48,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
|
|||||||
default_privacy: source_params.fetch(:privacy, @account.user.setting_default_privacy),
|
default_privacy: source_params.fetch(:privacy, @account.user.setting_default_privacy),
|
||||||
default_sensitive: source_params.fetch(:sensitive, @account.user.setting_default_sensitive),
|
default_sensitive: source_params.fetch(:sensitive, @account.user.setting_default_sensitive),
|
||||||
default_language: source_params.fetch(:language, @account.user.setting_default_language),
|
default_language: source_params.fetch(:language, @account.user.setting_default_language),
|
||||||
|
default_quote_policy: source_params.fetch(:quote_policy, @account.user.setting_default_quote_policy),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
class Api::V1::Admin::TagsController < Api::BaseController
|
class Api::V1::Admin::TagsController < Api::BaseController
|
||||||
include Authorization
|
include Authorization
|
||||||
|
|
||||||
before_action -> { authorize_if_got_token! :'admin:read' }, only: [:index, :show]
|
before_action -> { authorize_if_got_token! :'admin:read' }, only: [:index, :show]
|
||||||
before_action -> { authorize_if_got_token! :'admin:write' }, only: :update
|
before_action -> { authorize_if_got_token! :'admin:write' }, only: :update
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class Api::V1::InvitesController < Api::BaseController
|
|||||||
skip_around_action :set_locale
|
skip_around_action :set_locale
|
||||||
|
|
||||||
before_action :set_invite
|
before_action :set_invite
|
||||||
|
before_action :check_valid_usage!
|
||||||
before_action :check_enabled_registrations!
|
before_action :check_enabled_registrations!
|
||||||
|
|
||||||
# Override `current_user` to avoid reading session cookies
|
# Override `current_user` to avoid reading session cookies
|
||||||
@@ -22,9 +23,11 @@ class Api::V1::InvitesController < Api::BaseController
|
|||||||
@invite = Invite.find_by!(code: params[:invite_code])
|
@invite = Invite.find_by!(code: params[:invite_code])
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_enabled_registrations!
|
def check_valid_usage!
|
||||||
return render json: { error: I18n.t('invites.invalid') }, status: 401 unless @invite.valid_for_use?
|
render json: { error: I18n.t('invites.invalid') }, status: 401 unless @invite.valid_for_use?
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_enabled_registrations!
|
||||||
raise Mastodon::NotPermittedError unless allowed_registration?(request.remote_ip, @invite)
|
raise Mastodon::NotPermittedError unless allowed_registration?(request.remote_ip, @invite)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -16,16 +16,7 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
|
|||||||
def create
|
def create
|
||||||
with_redis_lock("push_subscription:#{current_user.id}") do
|
with_redis_lock("push_subscription:#{current_user.id}") do
|
||||||
destroy_web_push_subscriptions!
|
destroy_web_push_subscriptions!
|
||||||
|
@push_subscription = Web::PushSubscription.create!(web_push_subscription_params)
|
||||||
@push_subscription = Web::PushSubscription.create!(
|
|
||||||
endpoint: subscription_params[:endpoint],
|
|
||||||
key_p256dh: subscription_params[:keys][:p256dh],
|
|
||||||
key_auth: subscription_params[:keys][:auth],
|
|
||||||
standard: subscription_params[:standard] || false,
|
|
||||||
data: data_params,
|
|
||||||
user_id: current_user.id,
|
|
||||||
access_token_id: doorkeeper_token.id
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||||
@@ -55,6 +46,18 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
|
|||||||
not_found if @push_subscription.nil?
|
not_found if @push_subscription.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def web_push_subscription_params
|
||||||
|
{
|
||||||
|
access_token_id: doorkeeper_token.id,
|
||||||
|
data: data_params,
|
||||||
|
endpoint: subscription_params[:endpoint],
|
||||||
|
key_auth: subscription_params[:keys][:auth],
|
||||||
|
key_p256dh: subscription_params[:keys][:p256dh],
|
||||||
|
standard: subscription_params[:standard] || false,
|
||||||
|
user_id: current_user.id,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def subscription_params
|
def subscription_params
|
||||||
params.expect(subscription: [:endpoint, :standard, keys: [:auth, :p256dh]])
|
params.expect(subscription: [:endpoint, :standard, keys: [:auth, :p256dh]])
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Statuses::InteractionPoliciesController < Api::V1::Statuses::BaseController
|
||||||
|
include Api::InteractionPoliciesConcern
|
||||||
|
|
||||||
|
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
|
||||||
|
before_action -> { check_feature_enabled }
|
||||||
|
|
||||||
|
def update
|
||||||
|
authorize @status, :update?
|
||||||
|
|
||||||
|
@status.update!(quote_approval_policy: quote_approval_policy)
|
||||||
|
|
||||||
|
broadcast_updates! if @status.quote_approval_policy_previously_changed?
|
||||||
|
|
||||||
|
render json: @status, serializer: REST::StatusSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def status_params
|
||||||
|
params.permit(:quote_approval_policy)
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_feature_enabled
|
||||||
|
raise ActionController::RoutingError unless Mastodon::Feature.outgoing_quotes_enabled?
|
||||||
|
end
|
||||||
|
|
||||||
|
def broadcast_updates!
|
||||||
|
DistributionWorker.perform_async(@status.id, { 'update' => true })
|
||||||
|
ActivityPub::StatusUpdateDistributionWorker.perform_async(@status.id, { 'updated_at' => Time.now.utc.iso8601 })
|
||||||
|
end
|
||||||
|
end
|
||||||
72
app/controllers/api/v1/statuses/quotes_controller.rb
Normal file
72
app/controllers/api/v1/statuses/quotes_controller.rb
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Statuses::QuotesController < Api::V1::Statuses::BaseController
|
||||||
|
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :index
|
||||||
|
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: :revoke
|
||||||
|
|
||||||
|
before_action :check_owner!
|
||||||
|
before_action :set_quote, only: :revoke
|
||||||
|
after_action :insert_pagination_headers, only: :index
|
||||||
|
|
||||||
|
def index
|
||||||
|
cache_if_unauthenticated!
|
||||||
|
@statuses = load_statuses
|
||||||
|
render json: @statuses, each_serializer: REST::StatusSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def revoke
|
||||||
|
authorize @quote, :revoke?
|
||||||
|
|
||||||
|
RevokeQuoteService.new.call(@quote)
|
||||||
|
|
||||||
|
render json: @quote.status, serializer: REST::StatusSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def check_owner!
|
||||||
|
authorize @status, :list_quotes?
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_quote
|
||||||
|
@quote = @status.quotes.find_by!(status_id: params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_statuses
|
||||||
|
scope = default_statuses
|
||||||
|
scope = scope.not_excluded_by_account(current_account) unless current_account.nil?
|
||||||
|
scope.merge(paginated_quotes).to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_statuses
|
||||||
|
Status.includes(:quote).references(:quote)
|
||||||
|
end
|
||||||
|
|
||||||
|
def paginated_quotes
|
||||||
|
@status.quotes.accepted.paginate_by_max_id(
|
||||||
|
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||||
|
params[:max_id],
|
||||||
|
params[:since_id]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_path
|
||||||
|
api_v1_status_quotes_url pagination_params(max_id: pagination_max_id) if records_continue?
|
||||||
|
end
|
||||||
|
|
||||||
|
def prev_path
|
||||||
|
api_v1_status_quotes_url pagination_params(since_id: pagination_since_id) unless @statuses.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_max_id
|
||||||
|
@statuses.last.quote.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_since_id
|
||||||
|
@statuses.first.quote.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def records_continue?
|
||||||
|
@statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
class Api::V1::StatusesController < Api::BaseController
|
class Api::V1::StatusesController < Api::BaseController
|
||||||
include Authorization
|
include Authorization
|
||||||
|
include AsyncRefreshesConcern
|
||||||
|
include Api::InteractionPoliciesConcern
|
||||||
|
|
||||||
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy]
|
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy]
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy]
|
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy]
|
||||||
@@ -9,6 +11,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
before_action :set_statuses, only: [:index]
|
before_action :set_statuses, only: [:index]
|
||||||
before_action :set_status, only: [:show, :context]
|
before_action :set_status, only: [:show, :context]
|
||||||
before_action :set_thread, only: [:create]
|
before_action :set_thread, only: [:create]
|
||||||
|
before_action :set_quoted_status, only: [:create]
|
||||||
before_action :check_statuses_limit, only: [:index]
|
before_action :check_statuses_limit, only: [:index]
|
||||||
|
|
||||||
override_rate_limit_headers :create, family: :statuses
|
override_rate_limit_headers :create, family: :statuses
|
||||||
@@ -57,9 +60,21 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
@context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
|
@context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
|
||||||
statuses = [@status] + @context.ancestors + @context.descendants
|
statuses = [@status] + @context.ancestors + @context.descendants
|
||||||
|
|
||||||
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
|
refresh_key = "context:#{@status.id}:refresh"
|
||||||
|
async_refresh = AsyncRefresh.new(refresh_key)
|
||||||
|
|
||||||
ActivityPub::FetchAllRepliesWorker.perform_async(@status.id) if !current_account.nil? && @status.should_fetch_replies?
|
if async_refresh.running?
|
||||||
|
add_async_refresh_header(async_refresh)
|
||||||
|
elsif !current_account.nil? && @status.should_fetch_replies?
|
||||||
|
add_async_refresh_header(AsyncRefresh.create(refresh_key))
|
||||||
|
|
||||||
|
WorkerBatch.new.within do |batch|
|
||||||
|
batch.connect(refresh_key, threshold: 1.0)
|
||||||
|
ActivityPub::FetchAllRepliesWorker.perform_async(@status.id, { 'batch_id' => batch.id })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@@ -67,6 +82,8 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
current_user.account,
|
current_user.account,
|
||||||
text: status_params[:status],
|
text: status_params[:status],
|
||||||
thread: @thread,
|
thread: @thread,
|
||||||
|
quoted_status: @quoted_status,
|
||||||
|
quote_approval_policy: quote_approval_policy,
|
||||||
media_ids: status_params[:media_ids],
|
media_ids: status_params[:media_ids],
|
||||||
sensitive: status_params[:sensitive],
|
sensitive: status_params[:sensitive],
|
||||||
spoiler_text: status_params[:spoiler_text],
|
spoiler_text: status_params[:spoiler_text],
|
||||||
@@ -100,6 +117,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
language: status_params[:language],
|
language: status_params[:language],
|
||||||
spoiler_text: status_params[:spoiler_text],
|
spoiler_text: status_params[:spoiler_text],
|
||||||
poll: status_params[:poll],
|
poll: status_params[:poll],
|
||||||
|
quote_approval_policy: quote_approval_policy,
|
||||||
content_type: status_params[:content_type]
|
content_type: status_params[:content_type]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -140,6 +158,16 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
render json: { error: I18n.t('statuses.errors.in_reply_not_found') }, status: 404
|
render json: { error: I18n.t('statuses.errors.in_reply_not_found') }, status: 404
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_quoted_status
|
||||||
|
return unless Mastodon::Feature.outgoing_quotes_enabled?
|
||||||
|
|
||||||
|
@quoted_status = Status.find(status_params[:quoted_status_id]) if status_params[:quoted_status_id].present?
|
||||||
|
authorize(@quoted_status, :quote?) if @quoted_status.present?
|
||||||
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
|
# TODO: distinguish between non-existing and non-quotable posts
|
||||||
|
render json: { error: I18n.t('statuses.errors.quoted_status_not_found') }, status: 404
|
||||||
|
end
|
||||||
|
|
||||||
def check_statuses_limit
|
def check_statuses_limit
|
||||||
raise(Mastodon::ValidationError) if status_ids.size > DEFAULT_STATUSES_LIMIT
|
raise(Mastodon::ValidationError) if status_ids.size > DEFAULT_STATUSES_LIMIT
|
||||||
end
|
end
|
||||||
@@ -156,6 +184,8 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
params.permit(
|
params.permit(
|
||||||
:status,
|
:status,
|
||||||
:in_reply_to_id,
|
:in_reply_to_id,
|
||||||
|
:quoted_status_id,
|
||||||
|
:quote_approval_policy,
|
||||||
:sensitive,
|
:sensitive,
|
||||||
:spoiler_text,
|
:spoiler_text,
|
||||||
:visibility,
|
:visibility,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class Api::V2::SearchController < Api::BaseController
|
|||||||
@search = Search.new(search_results)
|
@search = Search.new(search_results)
|
||||||
render json: @search, serializer: REST::SearchSerializer
|
render json: @search, serializer: REST::SearchSerializer
|
||||||
rescue Mastodon::SyntaxError
|
rescue Mastodon::SyntaxError
|
||||||
unprocessable_entity
|
unprocessable_content
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
|||||||
{
|
{
|
||||||
policy: 'all',
|
policy: 'all',
|
||||||
alerts: Notification::TYPES.index_with { alerts_enabled },
|
alerts: Notification::TYPES.index_with { alerts_enabled },
|
||||||
}
|
}.deep_stringify_keys
|
||||||
end
|
end
|
||||||
|
|
||||||
def alerts_enabled
|
def alerts_enabled
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class ApplicationController < ActionController::Base
|
|||||||
rescue_from Mastodon::NotPermittedError, with: :forbidden
|
rescue_from Mastodon::NotPermittedError, with: :forbidden
|
||||||
rescue_from ActionController::RoutingError, ActiveRecord::RecordNotFound, with: :not_found
|
rescue_from ActionController::RoutingError, ActiveRecord::RecordNotFound, with: :not_found
|
||||||
rescue_from ActionController::UnknownFormat, with: :not_acceptable
|
rescue_from ActionController::UnknownFormat, with: :not_acceptable
|
||||||
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
|
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_content
|
||||||
rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests
|
rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests
|
||||||
|
|
||||||
rescue_from(*Mastodon::HTTP_CONNECTION_ERRORS, with: :internal_server_error)
|
rescue_from(*Mastodon::HTTP_CONNECTION_ERRORS, with: :internal_server_error)
|
||||||
@@ -126,7 +126,7 @@ class ApplicationController < ActionController::Base
|
|||||||
respond_with_error(410)
|
respond_with_error(410)
|
||||||
end
|
end
|
||||||
|
|
||||||
def unprocessable_entity
|
def unprocessable_content
|
||||||
respond_with_error(422)
|
respond_with_error(422)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -38,8 +38,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def record_login_activity
|
def record_login_activity
|
||||||
LoginActivity.create(
|
@user.login_activities.create(
|
||||||
user: @user,
|
|
||||||
success: true,
|
success: true,
|
||||||
authentication_method: :omniauth,
|
authentication_method: :omniauth,
|
||||||
provider: @provider,
|
provider: @provider,
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ class Auth::PasswordsController < Devise::PasswordsController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def redirect_invalid_reset_token
|
def redirect_invalid_reset_token
|
||||||
flash[:error] = I18n.t('auth.invalid_reset_password_token')
|
redirect_to new_password_path(resource_name), flash: { error: t('auth.invalid_reset_password_token') }
|
||||||
redirect_to new_password_path(resource_name)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def reset_password_token_is_valid?
|
def reset_password_token_is_valid?
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||||||
super(&:build_invite_request)
|
super(&:build_invite_request)
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit # rubocop:disable Lint/UselessMethodDefinition
|
def edit
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
def create # rubocop:disable Lint/UselessMethodDefinition
|
def create
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ class Auth::SessionsController < Devise::SessionsController
|
|||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
skip_before_action :update_user_sign_in
|
skip_before_action :update_user_sign_in
|
||||||
|
|
||||||
|
around_action :preserve_stored_location, only: :destroy, if: :continue_after?
|
||||||
|
|
||||||
prepend_before_action :check_suspicious!, only: [:create]
|
prepend_before_action :check_suspicious!, only: [:create]
|
||||||
|
|
||||||
include Auth::TwoFactorAuthenticationConcern
|
include Auth::TwoFactorAuthenticationConcern
|
||||||
@@ -31,11 +33,9 @@ class Auth::SessionsController < Devise::SessionsController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
tmp_stored_location = stored_location_for(:user)
|
|
||||||
super
|
super
|
||||||
session.delete(:challenge_passed_at)
|
session.delete(:challenge_passed_at)
|
||||||
flash.delete(:notice)
|
flash.delete(:notice)
|
||||||
store_location_for(:user, tmp_stored_location) if continue_after?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def webauthn_options
|
def webauthn_options
|
||||||
@@ -96,6 +96,12 @@ class Auth::SessionsController < Devise::SessionsController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def preserve_stored_location
|
||||||
|
original_stored_location = stored_location_for(:user)
|
||||||
|
yield
|
||||||
|
store_location_for(:user, original_stored_location)
|
||||||
|
end
|
||||||
|
|
||||||
def check_suspicious!
|
def check_suspicious!
|
||||||
user = find_user
|
user = find_user
|
||||||
@login_is_suspicious = suspicious_sign_in?(user) unless user.nil?
|
@login_is_suspicious = suspicious_sign_in?(user) unless user.nil?
|
||||||
@@ -151,12 +157,11 @@ class Auth::SessionsController < Devise::SessionsController
|
|||||||
sign_in(user)
|
sign_in(user)
|
||||||
flash.delete(:notice)
|
flash.delete(:notice)
|
||||||
|
|
||||||
LoginActivity.create(
|
user.login_activities.create(
|
||||||
user: user,
|
request_details.merge(
|
||||||
success: true,
|
|
||||||
authentication_method: security_measure,
|
authentication_method: security_measure,
|
||||||
ip: request.remote_ip,
|
success: true
|
||||||
user_agent: request.user_agent
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
UserMailer.suspicious_sign_in(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later! if @login_is_suspicious
|
UserMailer.suspicious_sign_in(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later! if @login_is_suspicious
|
||||||
@@ -167,13 +172,12 @@ class Auth::SessionsController < Devise::SessionsController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def on_authentication_failure(user, security_measure, failure_reason)
|
def on_authentication_failure(user, security_measure, failure_reason)
|
||||||
LoginActivity.create(
|
user.login_activities.create(
|
||||||
user: user,
|
request_details.merge(
|
||||||
success: false,
|
|
||||||
authentication_method: security_measure,
|
authentication_method: security_measure,
|
||||||
failure_reason: failure_reason,
|
failure_reason: failure_reason,
|
||||||
ip: request.remote_ip,
|
success: false
|
||||||
user_agent: request.user_agent
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Only send a notification email every hour at most
|
# Only send a notification email every hour at most
|
||||||
@@ -182,6 +186,13 @@ class Auth::SessionsController < Devise::SessionsController
|
|||||||
UserMailer.failed_2fa(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later!
|
UserMailer.failed_2fa(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def request_details
|
||||||
|
{
|
||||||
|
ip: request.remote_ip,
|
||||||
|
user_agent: request.user_agent,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def second_factor_attempts_key(user)
|
def second_factor_attempts_key(user)
|
||||||
"2fa_auth_attempts:#{user.id}:#{Time.now.utc.hour}"
|
"2fa_auth_attempts:#{user.id}:#{Time.now.utc.hour}"
|
||||||
end
|
end
|
||||||
|
|||||||
21
app/controllers/concerns/api/interaction_policies_concern.rb
Normal file
21
app/controllers/concerns/api/interaction_policies_concern.rb
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api::InteractionPoliciesConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
def quote_approval_policy
|
||||||
|
return nil unless Mastodon::Feature.outgoing_quotes_enabled?
|
||||||
|
|
||||||
|
case status_params[:quote_approval_policy].presence || current_user.setting_default_quote_policy
|
||||||
|
when 'public'
|
||||||
|
Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16
|
||||||
|
when 'followers'
|
||||||
|
Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16
|
||||||
|
when 'nobody'
|
||||||
|
0
|
||||||
|
else
|
||||||
|
# TODO: raise more useful message
|
||||||
|
raise ActiveRecord::RecordInvalid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,6 +5,18 @@ module Auth::CaptchaConcern
|
|||||||
|
|
||||||
include Hcaptcha::Adapters::ViewMethods
|
include Hcaptcha::Adapters::ViewMethods
|
||||||
|
|
||||||
|
CAPTCHA_DIRECTIVES = %w(
|
||||||
|
connect_src
|
||||||
|
frame_src
|
||||||
|
script_src
|
||||||
|
style_src
|
||||||
|
).freeze
|
||||||
|
|
||||||
|
CAPTCHA_SOURCES = %w(
|
||||||
|
https://*.hcaptcha.com
|
||||||
|
https://hcaptcha.com
|
||||||
|
).freeze
|
||||||
|
|
||||||
included do
|
included do
|
||||||
helper_method :render_captcha
|
helper_method :render_captcha
|
||||||
end
|
end
|
||||||
@@ -42,20 +54,9 @@ module Auth::CaptchaConcern
|
|||||||
end
|
end
|
||||||
|
|
||||||
def extend_csp_for_captcha!
|
def extend_csp_for_captcha!
|
||||||
policy = request.content_security_policy&.clone
|
return unless captcha_required? && request.content_security_policy.present?
|
||||||
|
|
||||||
return unless captcha_required? && policy.present?
|
request.content_security_policy = captcha_adjusted_policy
|
||||||
|
|
||||||
%w(script_src frame_src style_src connect_src).each do |directive|
|
|
||||||
values = policy.send(directive)
|
|
||||||
|
|
||||||
values << 'https://hcaptcha.com' unless values.include?('https://hcaptcha.com') || values.include?('https:')
|
|
||||||
values << 'https://*.hcaptcha.com' unless values.include?('https://*.hcaptcha.com') || values.include?('https:')
|
|
||||||
|
|
||||||
policy.send(directive, *values)
|
|
||||||
end
|
|
||||||
|
|
||||||
request.content_security_policy = policy
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_captcha
|
def render_captcha
|
||||||
@@ -63,4 +64,24 @@ module Auth::CaptchaConcern
|
|||||||
|
|
||||||
hcaptcha_tags
|
hcaptcha_tags
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def captcha_adjusted_policy
|
||||||
|
request.content_security_policy.clone.tap do |policy|
|
||||||
|
populate_captcha_policy(policy)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def populate_captcha_policy(policy)
|
||||||
|
CAPTCHA_DIRECTIVES.each do |directive|
|
||||||
|
values = policy.send(directive)
|
||||||
|
|
||||||
|
CAPTCHA_SOURCES.each do |source|
|
||||||
|
values << source unless values.include?(source) || values.include?('https:')
|
||||||
|
end
|
||||||
|
|
||||||
|
policy.send(directive, *values)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ module SignatureVerification
|
|||||||
|
|
||||||
EXPIRATION_WINDOW_LIMIT = 12.hours
|
EXPIRATION_WINDOW_LIMIT = 12.hours
|
||||||
CLOCK_SKEW_MARGIN = 1.hour
|
CLOCK_SKEW_MARGIN = 1.hour
|
||||||
|
STOPLIGHT_COOL_OFF_TIME = 5.minutes.seconds
|
||||||
|
STOPLIGHT_THRESHOLD = 1
|
||||||
|
|
||||||
def require_account_signature!
|
def require_account_signature!
|
||||||
render json: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
|
render json: signature_verification_failure_reason, status: signature_verification_failure_code unless signed_request_account
|
||||||
@@ -64,6 +66,9 @@ module SignatureVerification
|
|||||||
return (@signed_request_actor = actor) if signed_request.verified?(actor)
|
return (@signed_request_actor = actor) if signed_request.verified?(actor)
|
||||||
|
|
||||||
fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri}"
|
fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri}"
|
||||||
|
rescue Mastodon::MalformedHeaderError => e
|
||||||
|
@signature_verification_failure_code = 400
|
||||||
|
fail_with! e.message
|
||||||
rescue Mastodon::SignatureVerificationError => e
|
rescue Mastodon::SignatureVerificationError => e
|
||||||
fail_with! e.message
|
fail_with! e.message
|
||||||
rescue *Mastodon::HTTP_CONNECTION_ERRORS => e
|
rescue *Mastodon::HTTP_CONNECTION_ERRORS => e
|
||||||
@@ -104,10 +109,12 @@ module SignatureVerification
|
|||||||
end
|
end
|
||||||
|
|
||||||
def stoplight_wrapper
|
def stoplight_wrapper
|
||||||
Stoplight("source:#{request.remote_ip}")
|
Stoplight(
|
||||||
.with_threshold(1)
|
"source:#{request.remote_ip}",
|
||||||
.with_cool_off_time(5.minutes.seconds)
|
cool_off_time: STOPLIGHT_COOL_OFF_TIME,
|
||||||
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) }
|
threshold: STOPLIGHT_THRESHOLD,
|
||||||
|
tracked_errors: [HTTP::Error, OpenSSL::SSL::SSLError]
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def actor_refresh_key!(actor)
|
def actor_refresh_key!(actor)
|
||||||
|
|||||||
@@ -50,6 +50,13 @@ module WebAppControllerConcern
|
|||||||
return unless current_user&.require_tos_interstitial?
|
return unless current_user&.require_tos_interstitial?
|
||||||
|
|
||||||
@terms_of_service = TermsOfService.published.first
|
@terms_of_service = TermsOfService.published.first
|
||||||
|
|
||||||
|
# Handle case where terms of service have been removed from the database
|
||||||
|
if @terms_of_service.nil?
|
||||||
|
current_user.update(require_tos_interstitial: false)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
render 'terms_of_service_interstitial/show', layout: 'auth'
|
render 'terms_of_service_interstitial/show', layout: 'auth'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ class Settings::LoginActivitiesController < Settings::BaseController
|
|||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@login_activities = LoginActivity.where(user: current_user).order(id: :desc).page(params[:page])
|
@login_activities = current_user.login_activities.order(id: :desc).page(params[:page])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class Settings::Migration::RedirectsController < Settings::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
if current_account.moved_to_account_id.present?
|
if current_account.moved?
|
||||||
current_account.update!(moved_to_account: nil)
|
current_account.update!(moved_to_account: nil)
|
||||||
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
|
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Settings::Preferences::PostingDefaultsController < Settings::Preferences::BaseController
|
||||||
|
private
|
||||||
|
|
||||||
|
def after_update_redirect_path
|
||||||
|
settings_preferences_posting_defaults_path
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_params
|
||||||
|
super.tap do |params|
|
||||||
|
params[:settings_attributes][:default_quote_policy] = 'nobody' if params[:settings_attributes][:default_privacy] == 'private'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -8,8 +8,7 @@ class Settings::SessionsController < Settings::BaseController
|
|||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@session.destroy!
|
@session.destroy!
|
||||||
flash[:notice] = I18n.t('sessions.revoke_success')
|
redirect_to edit_user_registration_path, notice: t('sessions.revoke_success')
|
||||||
redirect_to edit_user_registration_path
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ module Settings
|
|||||||
end
|
end
|
||||||
else
|
else
|
||||||
flash[:error] = I18n.t('webauthn_credentials.create.error')
|
flash[:error] = I18n.t('webauthn_credentials.create.error')
|
||||||
status = :unprocessable_entity
|
status = :unprocessable_content
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
flash[:error] = t('webauthn_credentials.create.error')
|
flash[:error] = t('webauthn_credentials.create.error')
|
||||||
@@ -86,13 +86,11 @@ module Settings
|
|||||||
private
|
private
|
||||||
|
|
||||||
def redirect_invalid_otp
|
def redirect_invalid_otp
|
||||||
flash[:error] = t('webauthn_credentials.otp_required')
|
redirect_to settings_two_factor_authentication_methods_path, flash: { error: t('webauthn_credentials.otp_required') }
|
||||||
redirect_to settings_two_factor_authentication_methods_path
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def redirect_invalid_webauthn
|
def redirect_invalid_webauthn
|
||||||
flash[:error] = t('webauthn_credentials.not_enabled')
|
redirect_to settings_two_factor_authentication_methods_path, flash: { error: t('webauthn_credentials.not_enabled') }
|
||||||
redirect_to settings_two_factor_authentication_methods_path
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class StatusesController < ApplicationController
|
|||||||
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
|
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
|
||||||
before_action :set_status
|
before_action :set_status
|
||||||
before_action :redirect_to_original, only: :show
|
before_action :redirect_to_original, only: :show
|
||||||
|
before_action :verify_embed_allowed, only: :embed
|
||||||
|
|
||||||
after_action :set_link_headers
|
after_action :set_link_headers
|
||||||
|
|
||||||
@@ -40,8 +41,6 @@ class StatusesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def embed
|
def embed
|
||||||
return not_found if @status.hidden? || @status.reblog?
|
|
||||||
|
|
||||||
expires_in 180, public: true
|
expires_in 180, public: true
|
||||||
response.headers.delete('X-Frame-Options')
|
response.headers.delete('X-Frame-Options')
|
||||||
|
|
||||||
@@ -50,6 +49,10 @@ class StatusesController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def verify_embed_allowed
|
||||||
|
not_found if @status.hidden? || @status.reblog?
|
||||||
|
end
|
||||||
|
|
||||||
def set_link_headers
|
def set_link_headers
|
||||||
response.headers['Link'] = LinkHeader.new(
|
response.headers['Link'] = LinkHeader.new(
|
||||||
[[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]]
|
[[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]]
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ module Admin::ActionLogsHelper
|
|||||||
end
|
end
|
||||||
when 'UserRole'
|
when 'UserRole'
|
||||||
link_to log.human_identifier, admin_roles_path(log.target_id)
|
link_to log.human_identifier, admin_roles_path(log.target_id)
|
||||||
|
when 'UsernameBlock'
|
||||||
|
link_to log.human_identifier, edit_admin_username_block_path(log.target_id)
|
||||||
when 'Report'
|
when 'Report'
|
||||||
link_to "##{log.human_identifier.presence || log.target_id}", admin_report_path(log.target_id)
|
link_to "##{log.human_identifier.presence || log.target_id}", admin_report_path(log.target_id)
|
||||||
when 'Instance', 'DomainBlock', 'DomainAllow', 'UnavailableDomain'
|
when 'Instance', 'DomainBlock', 'DomainAllow', 'UnavailableDomain'
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ module ApplicationHelper
|
|||||||
|
|
||||||
def provider_sign_in_link(provider)
|
def provider_sign_in_link(provider)
|
||||||
label = Devise.omniauth_configs[provider]&.strategy&.display_name.presence || I18n.t("auth.providers.#{provider}", default: provider.to_s.chomp('_oauth2').capitalize)
|
label = Devise.omniauth_configs[provider]&.strategy&.display_name.presence || I18n.t("auth.providers.#{provider}", default: provider.to_s.chomp('_oauth2').capitalize)
|
||||||
link_to label, omniauth_authorize_path(:user, provider), class: "button button-#{provider}", method: :post
|
link_to label, omniauth_authorize_path(:user, provider), class: "btn button-#{provider}", method: :post
|
||||||
end
|
end
|
||||||
|
|
||||||
def locale_direction
|
def locale_direction
|
||||||
@@ -102,6 +102,16 @@ module ApplicationHelper
|
|||||||
policy(record).public_send(:"#{action}?")
|
policy(record).public_send(:"#{action}?")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def conditional_link_to(condition, name, options = {}, html_options = {}, &block)
|
||||||
|
if condition && !current_page?(block_given? ? name : options)
|
||||||
|
link_to(name, options, html_options, &block)
|
||||||
|
elsif block_given?
|
||||||
|
content_tag(:span, options, html_options, &block)
|
||||||
|
else
|
||||||
|
content_tag(:span, name, html_options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def material_symbol(icon, attributes = {})
|
def material_symbol(icon, attributes = {})
|
||||||
safe_join(
|
safe_join(
|
||||||
[
|
[
|
||||||
@@ -234,6 +244,10 @@ module ApplicationHelper
|
|||||||
tag.input(type: :text, maxlength: 999, spellcheck: false, readonly: true, **options)
|
tag.input(type: :text, maxlength: 999, spellcheck: false, readonly: true, **options)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def recent_tag_users(tag)
|
||||||
|
tag.statuses.public_visibility.joins(:account).merge(Account.without_suspended.without_silenced).includes(:account).limit(3).map(&:account)
|
||||||
|
end
|
||||||
|
|
||||||
def recent_tag_usage(tag)
|
def recent_tag_usage(tag)
|
||||||
people = tag.history.aggregate(2.days.ago.to_date..Time.zone.today).accounts
|
people = tag.history.aggregate(2.days.ago.to_date..Time.zone.today).accounts
|
||||||
I18n.t 'user_mailer.welcome.hashtags_recent_count', people: number_with_delimiter(people), count: people
|
I18n.t 'user_mailer.welcome.hashtags_recent_count', people: number_with_delimiter(people), count: people
|
||||||
@@ -247,6 +261,10 @@ module ApplicationHelper
|
|||||||
'https://play.google.com/store/apps/details?id=org.joinmastodon.android'
|
'https://play.google.com/store/apps/details?id=org.joinmastodon.android'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def within_authorization_flow?
|
||||||
|
session[:user_return_to].present? && Rails.application.routes.recognize_path(session[:user_return_to])[:controller] == 'oauth/authorizations'
|
||||||
|
end
|
||||||
|
|
||||||
# glitch-soc addition to handle the multiple flavors
|
# glitch-soc addition to handle the multiple flavors
|
||||||
def flavoured_vite_typescript_tag(pack_name, **)
|
def flavoured_vite_typescript_tag(pack_name, **)
|
||||||
vite_typescript_tag("#{Themes.instance.flavour(current_flavour)['pack_directory'].delete_prefix('app/javascript/')}/#{pack_name}", **)
|
vite_typescript_tag("#{Themes.instance.flavour(current_flavour)['pack_directory'].delete_prefix('app/javascript/')}/#{pack_name}", **)
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ module ContextHelper
|
|||||||
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
|
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
|
||||||
attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } },
|
attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } },
|
||||||
quote_requests: { 'QuoteRequest' => 'https://w3id.org/fep/044f#QuoteRequest' },
|
quote_requests: { 'QuoteRequest' => 'https://w3id.org/fep/044f#QuoteRequest' },
|
||||||
|
quotes: {
|
||||||
|
'quote' => 'https://w3id.org/fep/044f#quote',
|
||||||
|
'quoteUri' => 'http://fedibird.com/ns#quoteUri',
|
||||||
|
'_misskey_quote' => 'https://misskey-hub.net/ns#_misskey_quote',
|
||||||
|
'quoteAuthorization' => { '@id' => 'https://w3id.org/fep/044f#quoteAuthorization', '@type' => '@id' },
|
||||||
|
},
|
||||||
interaction_policies: {
|
interaction_policies: {
|
||||||
'gts' => 'https://gotosocial.org/ns#',
|
'gts' => 'https://gotosocial.org/ns#',
|
||||||
'interactionPolicy' => { '@id' => 'gts:interactionPolicy', '@type' => '@id' },
|
'interactionPolicy' => { '@id' => 'gts:interactionPolicy', '@type' => '@id' },
|
||||||
@@ -34,6 +40,12 @@ module ContextHelper
|
|||||||
'automaticApproval' => { '@id' => 'gts:automaticApproval', '@type' => '@id' },
|
'automaticApproval' => { '@id' => 'gts:automaticApproval', '@type' => '@id' },
|
||||||
'manualApproval' => { '@id' => 'gts:manualApproval', '@type' => '@id' },
|
'manualApproval' => { '@id' => 'gts:manualApproval', '@type' => '@id' },
|
||||||
},
|
},
|
||||||
|
quote_authorizations: {
|
||||||
|
'gts' => 'https://gotosocial.org/ns#',
|
||||||
|
'quoteAuthorization' => { '@id' => 'https://w3id.org/fep/044f#quoteAuthorization', '@type' => '@id' },
|
||||||
|
'interactingObject' => { '@id' => 'gts:interactingObject' },
|
||||||
|
'interactionTarget' => { '@id' => 'gts:interactionTarget' },
|
||||||
|
},
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
def full_context
|
def full_context
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module EmailHelper
|
|
||||||
def self.included(base)
|
|
||||||
base.extend(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
def email_to_canonical_email(str)
|
|
||||||
username, domain = str.downcase.split('@', 2)
|
|
||||||
username, = username.delete('.').split('+', 2)
|
|
||||||
|
|
||||||
"#{username}@#{domain}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def email_to_canonical_email_hash(str)
|
|
||||||
Digest::SHA2.new(256).hexdigest(email_to_canonical_email(str))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -27,7 +27,9 @@ module FormattingHelper
|
|||||||
module_function :extract_status_plain_text
|
module_function :extract_status_plain_text
|
||||||
|
|
||||||
def status_content_format(status)
|
def status_content_format(status)
|
||||||
html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []), content_type: status.content_type)
|
quoted_status = status.quote&.quoted_status if status.local?
|
||||||
|
|
||||||
|
html_aware_format(status.text, status.local?, preloaded_accounts: [status.account] + (status.respond_to?(:active_mentions) ? status.active_mentions.map(&:account) : []), quoted_status: quoted_status, content_type: status.content_type)
|
||||||
end
|
end
|
||||||
|
|
||||||
def rss_status_content_format(status)
|
def rss_status_content_format(status)
|
||||||
@@ -65,13 +67,13 @@ module FormattingHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def rss_content_preroll(status)
|
def rss_content_preroll(status)
|
||||||
if status.spoiler_text?
|
return unless status.spoiler_text?
|
||||||
|
|
||||||
safe_join [
|
safe_join [
|
||||||
tag.p { spoiler_with_warning(status) },
|
tag.p { spoiler_with_warning(status) },
|
||||||
tag.hr,
|
tag.hr,
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def spoiler_with_warning(status)
|
def spoiler_with_warning(status)
|
||||||
safe_join [
|
safe_join [
|
||||||
@@ -81,12 +83,12 @@ module FormattingHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def rss_content_postroll(status)
|
def rss_content_postroll(status)
|
||||||
if status.preloadable_poll
|
return unless status.preloadable_poll
|
||||||
|
|
||||||
tag.p do
|
tag.p do
|
||||||
poll_option_tags(status)
|
poll_option_tags(status)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def poll_option_tags(status)
|
def poll_option_tags(status)
|
||||||
safe_join(
|
safe_join(
|
||||||
|
|||||||
@@ -39,18 +39,8 @@ module HomeHelper
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def obscured_counter(count)
|
def field_verified_class(verified)
|
||||||
if count <= 0
|
if verified
|
||||||
'0'
|
|
||||||
elsif count == 1
|
|
||||||
'1'
|
|
||||||
else
|
|
||||||
'1+'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def custom_field_classes(field)
|
|
||||||
if field.verified?
|
|
||||||
'verified'
|
'verified'
|
||||||
else
|
else
|
||||||
'emojify'
|
'emojify'
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ module JsonLdHelper
|
|||||||
patch_for_forwarding!(value, compacted_value)
|
patch_for_forwarding!(value, compacted_value)
|
||||||
elsif value.is_a?(Array)
|
elsif value.is_a?(Array)
|
||||||
compacted_value = [compacted_value] unless compacted_value.is_a?(Array)
|
compacted_value = [compacted_value] unless compacted_value.is_a?(Array)
|
||||||
return if value.size != compacted_value.size
|
return nil if value.size != compacted_value.size
|
||||||
|
|
||||||
compacted[key] = value.zip(compacted_value).map do |v, vc|
|
compacted[key] = value.zip(compacted_value).map do |v, vc|
|
||||||
if v.is_a?(Hash) && vc.is_a?(Hash)
|
if v.is_a?(Hash) && vc.is_a?(Hash)
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ module LanguagesHelper
|
|||||||
mk: ['Macedonian', 'македонски јазик'].freeze,
|
mk: ['Macedonian', 'македонски јазик'].freeze,
|
||||||
ml: ['Malayalam', 'മലയാളം'].freeze,
|
ml: ['Malayalam', 'മലയാളം'].freeze,
|
||||||
mn: ['Mongolian', 'Монгол хэл'].freeze,
|
mn: ['Mongolian', 'Монгол хэл'].freeze,
|
||||||
|
'mn-Mong': ['Traditional Mongolian', 'ᠮᠣᠩᠭᠣᠯ ᠬᠡᠯᠡ'].freeze,
|
||||||
mr: ['Marathi', 'मराठी'].freeze,
|
mr: ['Marathi', 'मराठी'].freeze,
|
||||||
ms: ['Malay', 'Bahasa Melayu'].freeze,
|
ms: ['Malay', 'Bahasa Melayu'].freeze,
|
||||||
'ms-Arab': ['Jawi Malay', 'بهاس ملايو'].freeze,
|
'ms-Arab': ['Jawi Malay', 'بهاس ملايو'].freeze,
|
||||||
|
|||||||
@@ -64,4 +64,16 @@ module StatusesHelper
|
|||||||
def prefers_autoplay?
|
def prefers_autoplay?
|
||||||
ActiveModel::Type::Boolean.new.cast(params[:autoplay]) || current_user&.setting_auto_play_gif
|
ActiveModel::Type::Boolean.new.cast(params[:autoplay]) || current_user&.setting_auto_play_gif
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render_seo_schema(status)
|
||||||
|
json = ActiveModelSerializers::SerializableResource.new(
|
||||||
|
status,
|
||||||
|
serializer: SEO::SocialMediaPostingSerializer,
|
||||||
|
adapter: SEO::Adapter
|
||||||
|
).to_json
|
||||||
|
|
||||||
|
# rubocop:disable Rails/OutputSafety
|
||||||
|
content_tag(:script, json_escape(json).html_safe, type: 'application/ld+json')
|
||||||
|
# rubocop:enable Rails/OutputSafety
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ module ThemeHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def custom_stylesheet
|
def custom_stylesheet
|
||||||
if active_custom_stylesheet.present?
|
return if active_custom_stylesheet.blank?
|
||||||
|
|
||||||
stylesheet_link_tag(
|
stylesheet_link_tag(
|
||||||
custom_css_path(active_custom_stylesheet),
|
custom_css_path(active_custom_stylesheet),
|
||||||
host: root_url,
|
host: root_url,
|
||||||
@@ -36,17 +37,16 @@ module ThemeHelper
|
|||||||
skip_pipeline: true
|
skip_pipeline: true
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def active_custom_stylesheet
|
def active_custom_stylesheet
|
||||||
if cached_custom_css_digest.present?
|
return if cached_custom_css_digest.blank?
|
||||||
|
|
||||||
[:custom, cached_custom_css_digest.to_s.first(8)]
|
[:custom, cached_custom_css_digest.to_s.first(8)]
|
||||||
.compact_blank
|
.compact_blank
|
||||||
.join('-')
|
.join('-')
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def cached_custom_css_digest
|
def cached_custom_css_digest
|
||||||
Rails.cache.fetch(:setting_digest_custom_css) do
|
Rails.cache.fetch(:setting_digest_custom_css) do
|
||||||
|
|||||||
@@ -145,6 +145,10 @@ function loaded() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateDefaultQuotePrivacyFromPrivacy(
|
||||||
|
document.querySelector('#user_settings_attributes_default_privacy'),
|
||||||
|
);
|
||||||
|
|
||||||
const reactComponents = document.querySelectorAll('[data-component]');
|
const reactComponents = document.querySelectorAll('[data-component]');
|
||||||
|
|
||||||
if (reactComponents.length > 0) {
|
if (reactComponents.length > 0) {
|
||||||
@@ -347,6 +351,31 @@ const setInputDisabled = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setInputHint = (
|
||||||
|
input: HTMLInputElement | HTMLSelectElement,
|
||||||
|
hintPrefix: string,
|
||||||
|
) => {
|
||||||
|
const fieldWrapper = input.closest<HTMLElement>('.fields-group > .input');
|
||||||
|
if (!fieldWrapper) return;
|
||||||
|
|
||||||
|
const hint = fieldWrapper.dataset[`${hintPrefix}Hint`];
|
||||||
|
const hintElement =
|
||||||
|
fieldWrapper.querySelector<HTMLSpanElement>(':scope > .hint');
|
||||||
|
|
||||||
|
if (hint) {
|
||||||
|
if (hintElement) {
|
||||||
|
hintElement.textContent = hint;
|
||||||
|
} else {
|
||||||
|
const newHintElement = document.createElement('span');
|
||||||
|
newHintElement.className = 'hint';
|
||||||
|
newHintElement.textContent = hint;
|
||||||
|
fieldWrapper.appendChild(newHintElement);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hintElement?.remove();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Rails.delegate(
|
Rails.delegate(
|
||||||
document,
|
document,
|
||||||
'#account_statuses_cleanup_policy_enabled',
|
'#account_statuses_cleanup_policy_enabled',
|
||||||
@@ -364,6 +393,36 @@ Rails.delegate(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const updateDefaultQuotePrivacyFromPrivacy = (
|
||||||
|
privacySelect: EventTarget | null,
|
||||||
|
) => {
|
||||||
|
if (!(privacySelect instanceof HTMLSelectElement) || !privacySelect.form)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const select = privacySelect.form.querySelector<HTMLSelectElement>(
|
||||||
|
'select#user_settings_attributes_default_quote_policy',
|
||||||
|
);
|
||||||
|
if (!select) return;
|
||||||
|
|
||||||
|
setInputHint(select, privacySelect.value);
|
||||||
|
|
||||||
|
if (privacySelect.value === 'private') {
|
||||||
|
select.value = 'nobody';
|
||||||
|
setInputDisabled(select, true);
|
||||||
|
} else {
|
||||||
|
setInputDisabled(select, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Rails.delegate(
|
||||||
|
document,
|
||||||
|
'#user_settings_attributes_default_privacy',
|
||||||
|
'change',
|
||||||
|
({ target }) => {
|
||||||
|
updateDefaultQuotePrivacyFromPrivacy(target);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Empty the honeypot fields in JS in case something like an extension
|
// Empty the honeypot fields in JS in case something like an extension
|
||||||
// automatically filled them.
|
// automatically filled them.
|
||||||
Rails.delegate(document, '#registration_new_user,#new_user', 'submit', () => {
|
Rails.delegate(document, '#registration_new_user,#new_user', 'submit', () => {
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export const COMPOSE_FOCUS = 'COMPOSE_FOCUS';
|
|||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
||||||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||||
|
uploadQuote: { id: 'upload_error.quote', defaultMessage: 'File upload not allowed with quotes.' },
|
||||||
open: { id: 'compose.published.open', defaultMessage: 'Open' },
|
open: { id: 'compose.published.open', defaultMessage: 'Open' },
|
||||||
published: { id: 'compose.published.body', defaultMessage: 'Post published.' },
|
published: { id: 'compose.published.body', defaultMessage: 'Post published.' },
|
||||||
saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' },
|
saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' },
|
||||||
@@ -101,13 +102,18 @@ export const ensureComposeIsVisible = (getState) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function setComposeToStatus(status, text, spoiler_text, content_type) {
|
export function setComposeToStatus(status, text, spoiler_text, content_type) {
|
||||||
return{
|
return (dispatch, getState) => {
|
||||||
|
const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
type: COMPOSE_SET_STATUS,
|
type: COMPOSE_SET_STATUS,
|
||||||
status,
|
status,
|
||||||
text,
|
text,
|
||||||
spoiler_text,
|
spoiler_text,
|
||||||
content_type,
|
content_type,
|
||||||
};
|
maxOptions,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function changeCompose(text) {
|
export function changeCompose(text) {
|
||||||
@@ -154,7 +160,7 @@ export function resetCompose() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const focusCompose = (defaultText) => (dispatch, getState) => {
|
export const focusCompose = (defaultText = '') => (dispatch, getState) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: COMPOSE_FOCUS,
|
type: COMPOSE_FOCUS,
|
||||||
defaultText,
|
defaultText,
|
||||||
@@ -193,8 +199,9 @@ export function directCompose(account) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {null | string} overridePrivacy
|
* @param {null | string} overridePrivacy
|
||||||
|
* @param {undefined | Function} successCallback
|
||||||
*/
|
*/
|
||||||
export function submitCompose(overridePrivacy = null) {
|
export function submitCompose(overridePrivacy = null, successCallback = undefined) {
|
||||||
return function (dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
let status = getState().getIn(['compose', 'text'], '');
|
let status = getState().getIn(['compose', 'text'], '');
|
||||||
const media = getState().getIn(['compose', 'media_attachments']);
|
const media = getState().getIn(['compose', 'media_attachments']);
|
||||||
@@ -232,6 +239,7 @@ export function submitCompose(overridePrivacy = null) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const visibility = overridePrivacy || getState().getIn(['compose', 'privacy']);
|
||||||
api().request({
|
api().request({
|
||||||
url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`,
|
url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`,
|
||||||
method: statusId === null ? 'post' : 'put',
|
method: statusId === null ? 'post' : 'put',
|
||||||
@@ -243,9 +251,11 @@ export function submitCompose(overridePrivacy = null) {
|
|||||||
media_attributes,
|
media_attributes,
|
||||||
sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0),
|
sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0),
|
||||||
spoiler_text: spoilerText,
|
spoiler_text: spoilerText,
|
||||||
visibility: overridePrivacy || getState().getIn(['compose', 'privacy']),
|
visibility: visibility,
|
||||||
poll: getState().getIn(['compose', 'poll'], null),
|
poll: getState().getIn(['compose', 'poll'], null),
|
||||||
language: getState().getIn(['compose', 'language']),
|
language: getState().getIn(['compose', 'language']),
|
||||||
|
quoted_status_id: getState().getIn(['compose', 'quoted_status_id']),
|
||||||
|
quote_approval_policy: visibility === 'private' || visibility === 'direct' ? 'nobody' : getState().getIn(['compose', 'quote_policy']),
|
||||||
},
|
},
|
||||||
headers: {
|
headers: {
|
||||||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
||||||
@@ -259,6 +269,9 @@ export function submitCompose(overridePrivacy = null) {
|
|||||||
|
|
||||||
dispatch(insertIntoTagHistory(response.data.tags, status));
|
dispatch(insertIntoTagHistory(response.data.tags, status));
|
||||||
dispatch(submitComposeSuccess({ ...response.data }));
|
dispatch(submitComposeSuccess({ ...response.data }));
|
||||||
|
if (typeof successCallback === 'function') {
|
||||||
|
successCallback(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
// To make the app more responsive, immediately push the status
|
// To make the app more responsive, immediately push the status
|
||||||
// into the columns
|
// into the columns
|
||||||
@@ -330,6 +343,11 @@ export function doodleSet(options) {
|
|||||||
|
|
||||||
export function uploadCompose(files) {
|
export function uploadCompose(files) {
|
||||||
return function (dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
|
// Exit if there's a quote.
|
||||||
|
if (getState().compose.get('quoted_status_id')) {
|
||||||
|
dispatch(showAlert({ message: messages.uploadQuote }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
const uploadLimit = getState().getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']);
|
const uploadLimit = getState().getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']);
|
||||||
const media = getState().getIn(['compose', 'media_attachments']);
|
const media = getState().getIn(['compose', 'media_attachments']);
|
||||||
const pending = getState().getIn(['compose', 'pending_media_attachments']);
|
const pending = getState().getIn(['compose', 'pending_media_attachments']);
|
||||||
|
|||||||
@@ -1,9 +1,41 @@
|
|||||||
|
import { defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
import { apiUpdateMedia } from 'flavours/glitch/api/compose';
|
import { apiUpdateMedia } from 'flavours/glitch/api/compose';
|
||||||
import type { ApiMediaAttachmentJSON } from 'flavours/glitch/api_types/media_attachments';
|
import type { ApiMediaAttachmentJSON } from 'flavours/glitch/api_types/media_attachments';
|
||||||
import type { MediaAttachment } from 'flavours/glitch/models/media_attachment';
|
import type { MediaAttachment } from 'flavours/glitch/models/media_attachment';
|
||||||
import { createDataLoadingThunk } from 'flavours/glitch/store/typed_functions';
|
import {
|
||||||
|
createDataLoadingThunk,
|
||||||
|
createAppThunk,
|
||||||
|
} from 'flavours/glitch/store/typed_functions';
|
||||||
|
|
||||||
|
import type { ApiQuotePolicy } from '../api_types/quotes';
|
||||||
|
import type { Status } from '../models/status';
|
||||||
|
|
||||||
|
import { showAlert } from './alerts';
|
||||||
|
import { focusCompose } from './compose';
|
||||||
|
import { openModal } from './modal';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
quoteErrorUpload: {
|
||||||
|
id: 'quote_error.upload',
|
||||||
|
defaultMessage: 'Quoting is not allowed with media attachments.',
|
||||||
|
},
|
||||||
|
quoteErrorPoll: {
|
||||||
|
id: 'quote_error.poll',
|
||||||
|
defaultMessage: 'Quoting is not allowed with polls.',
|
||||||
|
},
|
||||||
|
quoteErrorQuote: {
|
||||||
|
id: 'quote_error.quote',
|
||||||
|
defaultMessage: 'Only one quote at a time is allowed.',
|
||||||
|
},
|
||||||
|
quoteErrorUnauthorized: {
|
||||||
|
id: 'quote_error.unauthorized',
|
||||||
|
defaultMessage: 'You are not authorized to quote this post.',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & {
|
type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & {
|
||||||
unattached?: boolean;
|
unattached?: boolean;
|
||||||
@@ -68,3 +100,73 @@ export const changeUploadCompose = createDataLoadingThunk(
|
|||||||
useLoadingBar: false,
|
useLoadingBar: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const quoteCompose = createAppThunk(
|
||||||
|
'compose/quoteComposeStatus',
|
||||||
|
(status: Status, { dispatch }) => {
|
||||||
|
dispatch(focusCompose());
|
||||||
|
return status;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const quoteComposeByStatus = createAppThunk(
|
||||||
|
(status: Status, { dispatch, getState }) => {
|
||||||
|
const state = getState();
|
||||||
|
const composeState = state.compose;
|
||||||
|
const mediaAttachments = composeState.get('media_attachments');
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
const wasQuietPostHintModalDismissed: boolean =
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||||
|
state.settings.getIn(
|
||||||
|
['dismissed_banners', 'quote/quiet_post_hint'],
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (composeState.get('poll')) {
|
||||||
|
dispatch(showAlert({ message: messages.quoteErrorPoll }));
|
||||||
|
} else if (
|
||||||
|
composeState.get('is_uploading') ||
|
||||||
|
(mediaAttachments &&
|
||||||
|
typeof mediaAttachments !== 'string' &&
|
||||||
|
typeof mediaAttachments !== 'number' &&
|
||||||
|
typeof mediaAttachments !== 'boolean' &&
|
||||||
|
mediaAttachments.size !== 0)
|
||||||
|
) {
|
||||||
|
dispatch(showAlert({ message: messages.quoteErrorUpload }));
|
||||||
|
} else if (composeState.get('quoted_status_id')) {
|
||||||
|
dispatch(showAlert({ message: messages.quoteErrorQuote }));
|
||||||
|
} else if (
|
||||||
|
status.getIn(['quote_approval', 'current_user']) !== 'automatic' &&
|
||||||
|
status.getIn(['quote_approval', 'current_user']) !== 'manual'
|
||||||
|
) {
|
||||||
|
dispatch(showAlert({ message: messages.quoteErrorUnauthorized }));
|
||||||
|
} else if (
|
||||||
|
status.get('visibility') === 'unlisted' &&
|
||||||
|
!wasQuietPostHintModalDismissed
|
||||||
|
) {
|
||||||
|
dispatch(
|
||||||
|
openModal({
|
||||||
|
modalType: 'CONFIRM_QUIET_QUOTE',
|
||||||
|
modalProps: { status },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
dispatch(quoteCompose(status));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const quoteComposeById = createAppThunk(
|
||||||
|
(statusId: string, { dispatch, getState }) => {
|
||||||
|
const status = getState().statuses.get(statusId);
|
||||||
|
if (status) {
|
||||||
|
dispatch(quoteComposeByStatus(status));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const quoteComposeCancel = createAction('compose/quoteComposeCancel');
|
||||||
|
|
||||||
|
export const setComposeQuotePolicy = createAction<ApiQuotePolicy>(
|
||||||
|
'compose/setQuotePolicy',
|
||||||
|
);
|
||||||
|
|||||||
@@ -21,6 +21,15 @@ export function normalizeFilterResult(result) {
|
|||||||
return normalResult;
|
return normalResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripQuoteFallback(text) {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.innerHTML = text;
|
||||||
|
|
||||||
|
wrapper.querySelector('.quote-inline')?.remove();
|
||||||
|
|
||||||
|
return wrapper.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeStatus(status, normalOldStatus, settings) {
|
export function normalizeStatus(status, normalOldStatus, settings) {
|
||||||
const normalStatus = { ...status };
|
const normalStatus = { ...status };
|
||||||
|
|
||||||
@@ -78,6 +87,11 @@ export function normalizeStatus(status, normalOldStatus, settings) {
|
|||||||
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
|
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
|
||||||
normalStatus.hidden = (spoilerText.length > 0 || normalStatus.sensitive) && autoHideCW(settings, spoilerText);
|
normalStatus.hidden = (spoilerText.length > 0 || normalStatus.sensitive) && autoHideCW(settings, spoilerText);
|
||||||
|
|
||||||
|
// Remove quote fallback link from the DOM so it doesn't mess with paragraph margins
|
||||||
|
if (normalStatus.quote) {
|
||||||
|
normalStatus.contentHtml = stripQuoteFallback(normalStatus.contentHtml);
|
||||||
|
}
|
||||||
|
|
||||||
if (normalStatus.url && !(normalStatus.url.startsWith('http://') || normalStatus.url.startsWith('https://'))) {
|
if (normalStatus.url && !(normalStatus.url.startsWith('http://') || normalStatus.url.startsWith('https://'))) {
|
||||||
normalStatus.url = null;
|
normalStatus.url = null;
|
||||||
}
|
}
|
||||||
@@ -117,6 +131,11 @@ export function normalizeStatusTranslation(translation, status) {
|
|||||||
spoiler_text: translation.spoiler_text,
|
spoiler_text: translation.spoiler_text,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Remove quote fallback link from the DOM so it doesn't mess with paragraph margins
|
||||||
|
if (status.get('quote')) {
|
||||||
|
normalTranslation.contentHtml = stripQuoteFallback(normalTranslation.contentHtml);
|
||||||
|
}
|
||||||
|
|
||||||
return normalTranslation;
|
return normalTranslation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { apiReblog, apiUnreblog } from 'flavours/glitch/api/interactions';
|
import {
|
||||||
|
apiReblog,
|
||||||
|
apiUnreblog,
|
||||||
|
apiRevokeQuote,
|
||||||
|
apiGetQuotes,
|
||||||
|
} from 'flavours/glitch/api/interactions';
|
||||||
import type { StatusVisibility } from 'flavours/glitch/models/status';
|
import type { StatusVisibility } from 'flavours/glitch/models/status';
|
||||||
import { createDataLoadingThunk } from 'flavours/glitch/store/typed_functions';
|
import { createDataLoadingThunk } from 'flavours/glitch/store/typed_functions';
|
||||||
|
|
||||||
import { importFetchedStatus } from './importer';
|
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||||
|
|
||||||
export const reblog = createDataLoadingThunk(
|
export const reblog = createDataLoadingThunk(
|
||||||
'status/reblog',
|
'status/reblog',
|
||||||
@@ -33,3 +38,35 @@ export const unreblog = createDataLoadingThunk(
|
|||||||
return discardLoadData;
|
return discardLoadData;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const revokeQuote = createDataLoadingThunk(
|
||||||
|
'status/revoke_quote',
|
||||||
|
({
|
||||||
|
statusId,
|
||||||
|
quotedStatusId,
|
||||||
|
}: {
|
||||||
|
statusId: string;
|
||||||
|
quotedStatusId: string;
|
||||||
|
}) => apiRevokeQuote(quotedStatusId, statusId),
|
||||||
|
(data, { dispatch, discardLoadData }) => {
|
||||||
|
dispatch(importFetchedStatus(data));
|
||||||
|
|
||||||
|
return discardLoadData;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const fetchQuotes = createDataLoadingThunk(
|
||||||
|
'status/fetch_quotes',
|
||||||
|
async ({ statusId, next }: { statusId: string; next?: string }) => {
|
||||||
|
const { links, statuses } = await apiGetQuotes(statusId, next);
|
||||||
|
|
||||||
|
return {
|
||||||
|
links,
|
||||||
|
statuses,
|
||||||
|
replace: !next,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
(payload, { dispatch }) => {
|
||||||
|
dispatch(importFetchedStatuses(payload.statuses));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -30,8 +30,21 @@ import { importFetchedAccounts, importFetchedStatuses } from './importer';
|
|||||||
import { NOTIFICATIONS_FILTER_SET } from './notifications';
|
import { NOTIFICATIONS_FILTER_SET } from './notifications';
|
||||||
import { saveSettings } from './settings';
|
import { saveSettings } from './settings';
|
||||||
|
|
||||||
|
function notificationTypeForFilter(type: NotificationType) {
|
||||||
|
if (type === 'quoted_update') return 'update';
|
||||||
|
else return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
function notificationTypeForQuickFilter(type: NotificationType) {
|
||||||
|
if (type === 'quoted_update') return 'update';
|
||||||
|
else if (type === 'quote') return 'mention';
|
||||||
|
else return type;
|
||||||
|
}
|
||||||
|
|
||||||
function excludeAllTypesExcept(filter: string) {
|
function excludeAllTypesExcept(filter: string) {
|
||||||
return allNotificationTypes.filter((item) => item !== filter);
|
return allNotificationTypes.filter(
|
||||||
|
(item) => notificationTypeForQuickFilter(item) !== filter,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExcludedTypes(state: RootState) {
|
function getExcludedTypes(state: RootState) {
|
||||||
@@ -155,13 +168,17 @@ export const processNewNotificationForGroups = createAppAsyncThunk(
|
|||||||
|
|
||||||
const showInColumn =
|
const showInColumn =
|
||||||
activeFilter === 'all'
|
activeFilter === 'all'
|
||||||
? notificationShows[notification.type] !== false
|
? notificationShows[notificationTypeForFilter(notification.type)] !==
|
||||||
: activeFilter === notification.type;
|
false
|
||||||
|
: activeFilter === notificationTypeForQuickFilter(notification.type);
|
||||||
|
|
||||||
if (!showInColumn) return;
|
if (!showInColumn) return;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(notification.type === 'mention' || notification.type === 'update') &&
|
(notification.type === 'mention' ||
|
||||||
|
notification.type === 'quote' ||
|
||||||
|
notification.type === 'update' ||
|
||||||
|
notification.type === 'quoted_update') &&
|
||||||
notification.status?.filtered
|
notification.status?.filtered
|
||||||
) {
|
) {
|
||||||
const filters = notification.status.filtered.filter((result) =>
|
const filters = notification.status.filtered.filter((result) =>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
|||||||
|
|
||||||
let filtered = false;
|
let filtered = false;
|
||||||
|
|
||||||
if (['mention', 'status'].includes(notification.type) && notification.status.filtered) {
|
if (['mention', 'quote', 'status'].includes(notification.type) && notification.status.filtered) {
|
||||||
const filters = notification.status.filtered.filter(result => result.filter.context.includes('notifications'));
|
const filters = notification.status.filtered.filter(result => result.filter.context.includes('notifications'));
|
||||||
|
|
||||||
if (filters.some(result => result.filter.filter_action === 'hide')) {
|
if (filters.some(result => result.filter.filter_action === 'hide')) {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import { defineMessages } from 'react-intl';
|
||||||
|
|
||||||
import { browserHistory } from 'flavours/glitch/components/router';
|
import { browserHistory } from 'flavours/glitch/components/router';
|
||||||
|
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
|
||||||
|
import { showAlert } from './alerts';
|
||||||
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
|
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
|
||||||
import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
|
import { importFetchedStatus, importFetchedAccount } from './importer';
|
||||||
import { fetchContext } from './statuses_typed';
|
import { fetchContext } from './statuses_typed';
|
||||||
import { deleteFromTimelines } from './timelines';
|
import { deleteFromTimelines } from './timelines';
|
||||||
|
|
||||||
@@ -40,6 +43,10 @@ export const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS';
|
|||||||
export const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL';
|
export const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL';
|
||||||
export const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO';
|
export const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
deleteSuccess: { id: 'status.delete.success', defaultMessage: 'Post deleted' },
|
||||||
|
});
|
||||||
|
|
||||||
export function fetchStatusRequest(id, skipLoading) {
|
export function fetchStatusRequest(id, skipLoading) {
|
||||||
return {
|
return {
|
||||||
type: STATUS_FETCH_REQUEST,
|
type: STATUS_FETCH_REQUEST,
|
||||||
@@ -48,7 +55,18 @@ export function fetchStatusRequest(id, skipLoading) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) {
|
/**
|
||||||
|
* @param {string} id
|
||||||
|
* @param {Object} [options]
|
||||||
|
* @param {boolean} [options.forceFetch]
|
||||||
|
* @param {boolean} [options.alsoFetchContext]
|
||||||
|
* @param {string | null | undefined} [options.parentQuotePostId]
|
||||||
|
*/
|
||||||
|
export function fetchStatus(id, {
|
||||||
|
forceFetch = false,
|
||||||
|
alsoFetchContext = true,
|
||||||
|
parentQuotePostId,
|
||||||
|
} = {}) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null;
|
const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null;
|
||||||
|
|
||||||
@@ -66,7 +84,7 @@ export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) {
|
|||||||
dispatch(importFetchedStatus(response.data));
|
dispatch(importFetchedStatus(response.data));
|
||||||
dispatch(fetchStatusSuccess(skipLoading));
|
dispatch(fetchStatusSuccess(skipLoading));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchStatusFail(id, error, skipLoading));
|
dispatch(fetchStatusFail(id, error, skipLoading, parentQuotePostId));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -78,22 +96,28 @@ export function fetchStatusSuccess(skipLoading) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchStatusFail(id, error, skipLoading) {
|
export function fetchStatusFail(id, error, skipLoading, parentQuotePostId) {
|
||||||
return {
|
return {
|
||||||
type: STATUS_FETCH_FAIL,
|
type: STATUS_FETCH_FAIL,
|
||||||
id,
|
id,
|
||||||
error,
|
error,
|
||||||
|
parentQuotePostId,
|
||||||
skipLoading,
|
skipLoading,
|
||||||
skipAlert: true,
|
skipAlert: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function redraft(status, raw_text, content_type) {
|
export function redraft(status, raw_text, content_type) {
|
||||||
return {
|
return (dispatch, getState) => {
|
||||||
|
const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
type: REDRAFT,
|
type: REDRAFT,
|
||||||
status,
|
status,
|
||||||
raw_text,
|
raw_text,
|
||||||
content_type,
|
content_type,
|
||||||
|
maxOptions,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +162,7 @@ export function deleteStatus(id, withRedraft = false) {
|
|||||||
|
|
||||||
dispatch(deleteStatusRequest(id));
|
dispatch(deleteStatusRequest(id));
|
||||||
|
|
||||||
api().delete(`/api/v1/statuses/${id}`, { params: { delete_media: !withRedraft } }).then(response => {
|
return api().delete(`/api/v1/statuses/${id}`, { params: { delete_media: !withRedraft } }).then(response => {
|
||||||
dispatch(deleteStatusSuccess(id));
|
dispatch(deleteStatusSuccess(id));
|
||||||
dispatch(deleteFromTimelines(id));
|
dispatch(deleteFromTimelines(id));
|
||||||
dispatch(importFetchedAccount(response.data.account));
|
dispatch(importFetchedAccount(response.data.account));
|
||||||
@@ -146,9 +170,14 @@ export function deleteStatus(id, withRedraft = false) {
|
|||||||
if (withRedraft) {
|
if (withRedraft) {
|
||||||
dispatch(redraft(status, response.data.text, response.data.content_type));
|
dispatch(redraft(status, response.data.text, response.data.content_type));
|
||||||
ensureComposeIsVisible(getState);
|
ensureComposeIsVisible(getState);
|
||||||
|
} else {
|
||||||
|
dispatch(showAlert({ message: messages.deleteSuccess }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(deleteStatusFail(id, error));
|
dispatch(deleteStatusFail(id, error));
|
||||||
|
throw error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,34 @@
|
|||||||
import { apiGetContext } from 'flavours/glitch/api/statuses';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import { apiGetContext, apiSetQuotePolicy } from 'flavours/glitch/api/statuses';
|
||||||
import { createDataLoadingThunk } from 'flavours/glitch/store/typed_functions';
|
import { createDataLoadingThunk } from 'flavours/glitch/store/typed_functions';
|
||||||
|
|
||||||
|
import type { ApiQuotePolicy } from '../api_types/quotes';
|
||||||
|
|
||||||
import { importFetchedStatuses } from './importer';
|
import { importFetchedStatuses } from './importer';
|
||||||
|
|
||||||
export const fetchContext = createDataLoadingThunk(
|
export const fetchContext = createDataLoadingThunk(
|
||||||
'status/context',
|
'status/context',
|
||||||
({ statusId }: { statusId: string }) => apiGetContext(statusId),
|
({ statusId }: { statusId: string }) => apiGetContext(statusId),
|
||||||
(context, { dispatch }) => {
|
({ context, refresh }, { dispatch }) => {
|
||||||
const statuses = context.ancestors.concat(context.descendants);
|
const statuses = context.ancestors.concat(context.descendants);
|
||||||
|
|
||||||
dispatch(importFetchedStatuses(statuses));
|
dispatch(importFetchedStatuses(statuses));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
context,
|
context,
|
||||||
|
refresh,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const completeContextRefresh = createAction<{ statusId: string }>(
|
||||||
|
'status/context/complete',
|
||||||
|
);
|
||||||
|
|
||||||
|
export const setStatusQuotePolicy = createDataLoadingThunk(
|
||||||
|
'status/setQuotePolicy',
|
||||||
|
({ statusId, policy }: { statusId: string; policy: ApiQuotePolicy }) => {
|
||||||
|
return apiSetQuotePolicy(statusId, policy);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -15,6 +15,50 @@ export const getLinks = (response: AxiosResponse) => {
|
|||||||
return LinkHeader.parse(value);
|
return LinkHeader.parse(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface AsyncRefreshHeader {
|
||||||
|
id: string;
|
||||||
|
retry: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAsyncRefreshHeader = (obj: object): obj is AsyncRefreshHeader =>
|
||||||
|
'id' in obj && 'retry' in obj;
|
||||||
|
|
||||||
|
export const getAsyncRefreshHeader = (
|
||||||
|
response: AxiosResponse,
|
||||||
|
): AsyncRefreshHeader | null => {
|
||||||
|
const value = response.headers['mastodon-async-refresh'] as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const asyncRefreshHeader: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
value.split(/,\s*/).forEach((pair) => {
|
||||||
|
const [key, val] = pair.split('=', 2);
|
||||||
|
|
||||||
|
let typedValue: string | number;
|
||||||
|
|
||||||
|
if (key && ['id', 'retry'].includes(key) && val) {
|
||||||
|
if (val.startsWith('"')) {
|
||||||
|
typedValue = val.slice(1, -1);
|
||||||
|
} else {
|
||||||
|
typedValue = parseInt(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
asyncRefreshHeader[key] = typedValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isAsyncRefreshHeader(asyncRefreshHeader)) {
|
||||||
|
return asyncRefreshHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const csrfHeader: RawAxiosRequestHeaders = {};
|
const csrfHeader: RawAxiosRequestHeaders = {};
|
||||||
|
|
||||||
const setCSRFHeader = () => {
|
const setCSRFHeader = () => {
|
||||||
@@ -62,7 +106,7 @@ export default function api(withAuthorization = true) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
type ApiUrl = `v${1 | 2}/${string}`;
|
type ApiUrl = `v${1 | '1_alpha' | 2}/${string}`;
|
||||||
type RequestParamsOrData = Record<string, unknown>;
|
type RequestParamsOrData = Record<string, unknown>;
|
||||||
|
|
||||||
export async function apiRequest<ApiResponse = unknown>(
|
export async function apiRequest<ApiResponse = unknown>(
|
||||||
|
|||||||
5
app/javascript/flavours/glitch/api/async_refreshes.ts
Normal file
5
app/javascript/flavours/glitch/api/async_refreshes.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { apiRequestGet } from 'flavours/glitch/api';
|
||||||
|
import type { ApiAsyncRefreshJSON } from 'flavours/glitch/api_types/async_refreshes';
|
||||||
|
|
||||||
|
export const apiGetAsyncRefresh = (id: string) =>
|
||||||
|
apiRequestGet<ApiAsyncRefreshJSON>(`v1_alpha/async_refreshes/${id}`);
|
||||||
@@ -1,10 +1,28 @@
|
|||||||
import { apiRequestPost } from 'flavours/glitch/api';
|
import api, { apiRequestPost, getLinks } from 'flavours/glitch/api';
|
||||||
import type { Status, StatusVisibility } from 'flavours/glitch/models/status';
|
import type { ApiStatusJSON } from 'flavours/glitch/api_types/statuses';
|
||||||
|
import type { StatusVisibility } from 'flavours/glitch/models/status';
|
||||||
|
|
||||||
export const apiReblog = (statusId: string, visibility: StatusVisibility) =>
|
export const apiReblog = (statusId: string, visibility: StatusVisibility) =>
|
||||||
apiRequestPost<{ reblog: Status }>(`v1/statuses/${statusId}/reblog`, {
|
apiRequestPost<{ reblog: ApiStatusJSON }>(`v1/statuses/${statusId}/reblog`, {
|
||||||
visibility,
|
visibility,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiUnreblog = (statusId: string) =>
|
export const apiUnreblog = (statusId: string) =>
|
||||||
apiRequestPost<Status>(`v1/statuses/${statusId}/unreblog`);
|
apiRequestPost<ApiStatusJSON>(`v1/statuses/${statusId}/unreblog`);
|
||||||
|
|
||||||
|
export const apiRevokeQuote = (quotedStatusId: string, statusId: string) =>
|
||||||
|
apiRequestPost<ApiStatusJSON>(
|
||||||
|
`v1/statuses/${quotedStatusId}/quotes/${statusId}/revoke`,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const apiGetQuotes = async (statusId: string, url?: string) => {
|
||||||
|
const response = await api().request<ApiStatusJSON[]>({
|
||||||
|
method: 'GET',
|
||||||
|
url: url ?? `/api/v1/statuses/${statusId}/quotes`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statuses: response.data,
|
||||||
|
links: getLinks(response),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,31 @@
|
|||||||
import { apiRequestGet } from 'flavours/glitch/api';
|
import api, { apiRequestPut, getAsyncRefreshHeader } from 'flavours/glitch/api';
|
||||||
import type { ApiContextJSON } from 'flavours/glitch/api_types/statuses';
|
import type {
|
||||||
|
ApiContextJSON,
|
||||||
|
ApiStatusJSON,
|
||||||
|
} from 'flavours/glitch/api_types/statuses';
|
||||||
|
|
||||||
export const apiGetContext = (statusId: string) =>
|
import type { ApiQuotePolicy } from '../api_types/quotes';
|
||||||
apiRequestGet<ApiContextJSON>(`v1/statuses/${statusId}/context`);
|
|
||||||
|
export const apiGetContext = async (statusId: string) => {
|
||||||
|
const response = await api().request<ApiContextJSON>({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/api/v1/statuses/${statusId}/context`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
context: response.data,
|
||||||
|
refresh: getAsyncRefreshHeader(response),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const apiSetQuotePolicy = async (
|
||||||
|
statusId: string,
|
||||||
|
policy: ApiQuotePolicy,
|
||||||
|
) => {
|
||||||
|
return apiRequestPut<ApiStatusJSON>(
|
||||||
|
`v1/statuses/${statusId}/interaction_policy`,
|
||||||
|
{
|
||||||
|
quote_approval_policy: policy,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export interface BaseApiAccountJSON {
|
|||||||
roles?: ApiAccountJSON[];
|
roles?: ApiAccountJSON[];
|
||||||
statuses_count: number;
|
statuses_count: number;
|
||||||
uri: string;
|
uri: string;
|
||||||
url: string;
|
url?: string;
|
||||||
username: string;
|
username: string;
|
||||||
moved?: ApiAccountJSON;
|
moved?: ApiAccountJSON;
|
||||||
suspended?: boolean;
|
suspended?: boolean;
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export interface ApiAsyncRefreshJSON {
|
||||||
|
async_refresh: {
|
||||||
|
id: string;
|
||||||
|
status: 'running' | 'finished';
|
||||||
|
result_count: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -7,12 +7,13 @@ import type { ApiReportJSON } from './reports';
|
|||||||
import type { ApiStatusJSON } from './statuses';
|
import type { ApiStatusJSON } from './statuses';
|
||||||
|
|
||||||
// See app/model/notification.rb
|
// See app/model/notification.rb
|
||||||
export const allNotificationTypes = [
|
export const allNotificationTypes: NotificationType[] = [
|
||||||
'follow',
|
'follow',
|
||||||
'follow_request',
|
'follow_request',
|
||||||
'favourite',
|
'favourite',
|
||||||
'reblog',
|
'reblog',
|
||||||
'mention',
|
'mention',
|
||||||
|
'quote',
|
||||||
'poll',
|
'poll',
|
||||||
'status',
|
'status',
|
||||||
'update',
|
'update',
|
||||||
@@ -28,8 +29,10 @@ export type NotificationWithStatusType =
|
|||||||
| 'reblog'
|
| 'reblog'
|
||||||
| 'status'
|
| 'status'
|
||||||
| 'mention'
|
| 'mention'
|
||||||
|
| 'quote'
|
||||||
| 'poll'
|
| 'poll'
|
||||||
| 'update';
|
| 'update'
|
||||||
|
| 'quoted_update';
|
||||||
|
|
||||||
export type NotificationType =
|
export type NotificationType =
|
||||||
| NotificationWithStatusType
|
| NotificationWithStatusType
|
||||||
|
|||||||
39
app/javascript/flavours/glitch/api_types/quotes.ts
Normal file
39
app/javascript/flavours/glitch/api_types/quotes.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import type { ApiStatusJSON } from './statuses';
|
||||||
|
|
||||||
|
export type ApiQuoteState = 'accepted' | 'pending' | 'revoked' | 'unauthorized';
|
||||||
|
export type ApiQuotePolicy =
|
||||||
|
| 'public'
|
||||||
|
| 'followers'
|
||||||
|
| 'following'
|
||||||
|
| 'nobody'
|
||||||
|
| 'unsupported_policy';
|
||||||
|
export type ApiUserQuotePolicy = 'automatic' | 'manual' | 'denied' | 'unknown';
|
||||||
|
|
||||||
|
interface ApiQuoteEmptyJSON {
|
||||||
|
state: Exclude<ApiQuoteState, 'accepted'>;
|
||||||
|
quoted_status: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiNestedQuoteJSON {
|
||||||
|
state: 'accepted';
|
||||||
|
quoted_status_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiQuoteAcceptedJSON {
|
||||||
|
state: 'accepted';
|
||||||
|
quoted_status: Omit<ApiStatusJSON, 'quote'> & {
|
||||||
|
quote: ApiNestedQuoteJSON | ApiQuoteEmptyJSON;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiQuoteJSON = ApiQuoteAcceptedJSON | ApiQuoteEmptyJSON;
|
||||||
|
|
||||||
|
export interface ApiQuotePolicyJSON {
|
||||||
|
automatic: ApiQuotePolicy[];
|
||||||
|
manual: ApiQuotePolicy[];
|
||||||
|
current_user: ApiUserQuotePolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isQuotePolicy(policy: string): policy is ApiQuotePolicy {
|
||||||
|
return ['public', 'followers', 'nobody'].includes(policy);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import type { ApiAccountJSON } from './accounts';
|
|||||||
import type { ApiCustomEmojiJSON } from './custom_emoji';
|
import type { ApiCustomEmojiJSON } from './custom_emoji';
|
||||||
import type { ApiMediaAttachmentJSON } from './media_attachments';
|
import type { ApiMediaAttachmentJSON } from './media_attachments';
|
||||||
import type { ApiPollJSON } from './polls';
|
import type { ApiPollJSON } from './polls';
|
||||||
|
import type { ApiQuoteJSON, ApiQuotePolicyJSON } from './quotes';
|
||||||
|
|
||||||
// See app/modals/status.rb
|
// See app/modals/status.rb
|
||||||
export type StatusVisibility =
|
export type StatusVisibility =
|
||||||
@@ -95,6 +96,7 @@ export interface ApiStatusJSON {
|
|||||||
replies_count: number;
|
replies_count: number;
|
||||||
reblogs_count: number;
|
reblogs_count: number;
|
||||||
favorites_count: number;
|
favorites_count: number;
|
||||||
|
quotes_count: number;
|
||||||
edited_at?: string;
|
edited_at?: string;
|
||||||
|
|
||||||
favorited?: boolean;
|
favorited?: boolean;
|
||||||
@@ -118,6 +120,8 @@ export interface ApiStatusJSON {
|
|||||||
|
|
||||||
card?: ApiPreviewCardJSON;
|
card?: ApiPreviewCardJSON;
|
||||||
poll?: ApiPollJSON;
|
poll?: ApiPollJSON;
|
||||||
|
quote?: ApiQuoteJSON;
|
||||||
|
quote_approval?: ApiQuotePolicyJSON;
|
||||||
|
|
||||||
// glitch-soc additions
|
// glitch-soc additions
|
||||||
local_only?: boolean;
|
local_only?: boolean;
|
||||||
@@ -128,3 +132,15 @@ export interface ApiContextJSON {
|
|||||||
ancestors: ApiStatusJSON[];
|
ancestors: ApiStatusJSON[];
|
||||||
descendants: ApiStatusJSON[];
|
descendants: ApiStatusJSON[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiStatusSourceJSON {
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
spoiler_text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isStatusVisibility(
|
||||||
|
visibility: string,
|
||||||
|
): visibility is StatusVisibility {
|
||||||
|
return ['public', 'unlisted', 'private', 'direct'].includes(visibility);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,20 +1,76 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { useLinks } from 'flavours/glitch/hooks/useLinks';
|
import { useLinks } from 'flavours/glitch/hooks/useLinks';
|
||||||
|
|
||||||
export const AccountBio: React.FC<{
|
import { EmojiHTML } from '../features/emoji/emoji_html';
|
||||||
note: string;
|
import { useAppSelector } from '../store';
|
||||||
className: string;
|
import { isModernEmojiEnabled } from '../utils/environment';
|
||||||
}> = ({ note, className }) => {
|
|
||||||
const handleClick = useLinks();
|
|
||||||
|
|
||||||
if (note.length === 0 || note === '<p></p>') {
|
interface AccountBioProps {
|
||||||
|
className: string;
|
||||||
|
accountId: string;
|
||||||
|
showDropdown?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AccountBio: React.FC<AccountBioProps> = ({
|
||||||
|
className,
|
||||||
|
accountId,
|
||||||
|
showDropdown = false,
|
||||||
|
}) => {
|
||||||
|
const handleClick = useLinks(showDropdown);
|
||||||
|
const handleNodeChange = useCallback(
|
||||||
|
(node: HTMLDivElement | null) => {
|
||||||
|
if (!showDropdown || !node || node.childNodes.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addDropdownToHashtags(node, accountId);
|
||||||
|
},
|
||||||
|
[showDropdown, accountId],
|
||||||
|
);
|
||||||
|
const note = useAppSelector((state) => {
|
||||||
|
const account = state.accounts.get(accountId);
|
||||||
|
if (!account) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return isModernEmojiEnabled() ? account.note : account.note_emojified;
|
||||||
|
});
|
||||||
|
const extraEmojis = useAppSelector((state) => {
|
||||||
|
const account = state.accounts.get(accountId);
|
||||||
|
return account?.emojis;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (note.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${className} translate`}
|
className={`${className} translate`}
|
||||||
dangerouslySetInnerHTML={{ __html: note }}
|
|
||||||
onClickCapture={handleClick}
|
onClickCapture={handleClick}
|
||||||
/>
|
ref={handleNodeChange}
|
||||||
|
>
|
||||||
|
<EmojiHTML htmlString={note} extraEmojis={extraEmojis} />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function addDropdownToHashtags(node: HTMLElement | null, accountId: string) {
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const childNode of node.childNodes) {
|
||||||
|
if (!(childNode instanceof HTMLElement)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
childNode instanceof HTMLAnchorElement &&
|
||||||
|
(childNode.classList.contains('hashtag') ||
|
||||||
|
childNode.innerText.startsWith('#')) &&
|
||||||
|
!childNode.dataset.menuHashtag
|
||||||
|
) {
|
||||||
|
childNode.dataset.menuHashtag = accountId;
|
||||||
|
} else if (childNode.childNodes.length > 0) {
|
||||||
|
addDropdownToHashtags(childNode, accountId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
import { fn, expect } from 'storybook/test';
|
||||||
|
|
||||||
|
import { Alert } from '.';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Alert',
|
||||||
|
component: Alert,
|
||||||
|
args: {
|
||||||
|
isActive: true,
|
||||||
|
animateFrom: 'side',
|
||||||
|
title: '',
|
||||||
|
message: '',
|
||||||
|
action: '',
|
||||||
|
onActionClick: fn(),
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
isActive: {
|
||||||
|
control: 'boolean',
|
||||||
|
type: 'boolean',
|
||||||
|
description: 'Animate to the active (displayed) state of the alert',
|
||||||
|
},
|
||||||
|
animateFrom: {
|
||||||
|
control: 'radio',
|
||||||
|
type: 'string',
|
||||||
|
options: ['side', 'below'],
|
||||||
|
description:
|
||||||
|
'Direction that the alert animates in from when activated. `side` is dependent on reading direction, defaulting to left in ltr languages.',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
control: 'text',
|
||||||
|
type: 'string',
|
||||||
|
description: '(Optional) title of the alert',
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
control: 'text',
|
||||||
|
type: 'string',
|
||||||
|
description: 'Main alert text',
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
control: 'text',
|
||||||
|
type: 'string',
|
||||||
|
description:
|
||||||
|
'Label of the alert action (requires `onActionClick` handler)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tags: ['test'],
|
||||||
|
} satisfies Meta<typeof Alert>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Simple: Story = {
|
||||||
|
args: {
|
||||||
|
message: 'Post published.',
|
||||||
|
},
|
||||||
|
render: (args) => (
|
||||||
|
<div style={{ overflow: 'clip', padding: '1rem' }}>
|
||||||
|
<Alert {...args} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithAction: Story = {
|
||||||
|
args: {
|
||||||
|
...Simple.args,
|
||||||
|
action: 'Open',
|
||||||
|
},
|
||||||
|
render: Simple.render,
|
||||||
|
play: async ({ args, canvas, userEvent }) => {
|
||||||
|
const button = await canvas.findByRole('button', { name: 'Open' });
|
||||||
|
await userEvent.click(button);
|
||||||
|
await expect(args.onActionClick).toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithTitle: Story = {
|
||||||
|
args: {
|
||||||
|
title: 'Warning:',
|
||||||
|
message: 'This is an alert',
|
||||||
|
},
|
||||||
|
render: Simple.render,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithDismissButton: Story = {
|
||||||
|
args: {
|
||||||
|
message: 'More replies found',
|
||||||
|
action: 'Show',
|
||||||
|
onDismiss: fn(),
|
||||||
|
},
|
||||||
|
render: Simple.render,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InSizedContainer: Story = {
|
||||||
|
args: WithDismissButton.args,
|
||||||
|
render: (args) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
overflow: 'clip',
|
||||||
|
padding: '1rem',
|
||||||
|
width: '380px',
|
||||||
|
maxWidth: '100%',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Alert {...args} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
68
app/javascript/flavours/glitch/components/alert/index.tsx
Normal file
68
app/javascript/flavours/glitch/components/alert/index.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||||
|
|
||||||
|
import { IconButton } from '../icon_button';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Snackbar/Toast-style notification component.
|
||||||
|
*/
|
||||||
|
export const Alert: React.FC<{
|
||||||
|
isActive?: boolean;
|
||||||
|
animateFrom?: 'side' | 'below';
|
||||||
|
title?: string;
|
||||||
|
message: string;
|
||||||
|
action?: string;
|
||||||
|
onActionClick?: () => void;
|
||||||
|
onDismiss?: () => void;
|
||||||
|
}> = ({
|
||||||
|
isActive,
|
||||||
|
animateFrom = 'side',
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
action,
|
||||||
|
onActionClick,
|
||||||
|
onDismiss,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const hasAction = Boolean(action && onActionClick);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames('notification-bar', {
|
||||||
|
'notification-bar--active': isActive,
|
||||||
|
'from-side': animateFrom === 'side',
|
||||||
|
'from-below': animateFrom === 'below',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span className='notification-bar__content'>
|
||||||
|
{Boolean(title) && (
|
||||||
|
<span className='notification-bar__title'>{title}</span>
|
||||||
|
)}
|
||||||
|
{message}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{hasAction && (
|
||||||
|
<button className='notification-bar__action' onClick={onActionClick}>
|
||||||
|
{action}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{onDismiss && (
|
||||||
|
<IconButton
|
||||||
|
title={intl.formatMessage({
|
||||||
|
id: 'dismissable_banner.dismiss',
|
||||||
|
defaultMessage: 'Dismiss',
|
||||||
|
})}
|
||||||
|
icon='times'
|
||||||
|
iconComponent={CloseIcon}
|
||||||
|
className='notification-bar__dismiss-button'
|
||||||
|
onClick={onDismiss}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,16 +3,16 @@ import { useState, useEffect } from 'react';
|
|||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import type { IntlShape } from 'react-intl';
|
import type { IntlShape } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import { dismissAlert } from 'flavours/glitch/actions/alerts';
|
import { dismissAlert } from 'flavours/glitch/actions/alerts';
|
||||||
import type {
|
import type {
|
||||||
Alert,
|
Alert as AlertType,
|
||||||
TranslatableString,
|
TranslatableString,
|
||||||
TranslatableValues,
|
TranslatableValues,
|
||||||
} from 'flavours/glitch/models/alert';
|
} from 'flavours/glitch/models/alert';
|
||||||
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
import { Alert } from './alert';
|
||||||
|
|
||||||
const formatIfNeeded = (
|
const formatIfNeeded = (
|
||||||
intl: IntlShape,
|
intl: IntlShape,
|
||||||
message: TranslatableString,
|
message: TranslatableString,
|
||||||
@@ -25,8 +25,8 @@ const formatIfNeeded = (
|
|||||||
return message;
|
return message;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Alert: React.FC<{
|
const TimedAlert: React.FC<{
|
||||||
alert: Alert;
|
alert: AlertType;
|
||||||
dismissAfter: number;
|
dismissAfter: number;
|
||||||
}> = ({
|
}> = ({
|
||||||
alert: { key, title, message, values, action, onClick },
|
alert: { key, title, message, values, action, onClick },
|
||||||
@@ -62,29 +62,13 @@ const Alert: React.FC<{
|
|||||||
}, [dispatch, setActive, key, dismissAfter]);
|
}, [dispatch, setActive, key, dismissAfter]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Alert
|
||||||
className={classNames('notification-bar', {
|
isActive={active}
|
||||||
'notification-bar-active': active,
|
title={title ? formatIfNeeded(intl, title, values) : undefined}
|
||||||
})}
|
message={formatIfNeeded(intl, message, values)}
|
||||||
>
|
action={action ? formatIfNeeded(intl, action, values) : undefined}
|
||||||
<div className='notification-bar-wrapper'>
|
onActionClick={onClick}
|
||||||
{title && (
|
/>
|
||||||
<span className='notification-bar-title'>
|
|
||||||
{formatIfNeeded(intl, title, values)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<span className='notification-bar-message'>
|
|
||||||
{formatIfNeeded(intl, message, values)}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{action && (
|
|
||||||
<button className='notification-bar-action' onClick={onClick}>
|
|
||||||
{formatIfNeeded(intl, action, values)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,7 +82,11 @@ export const AlertsController: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className='notification-list'>
|
<div className='notification-list'>
|
||||||
{alerts.map((alert, idx) => (
|
{alerts.map((alert, idx) => (
|
||||||
<Alert key={alert.key} alert={alert} dismissAfter={5000 + idx * 1000} />
|
<TimedAlert
|
||||||
|
key={alert.key}
|
||||||
|
alert={alert}
|
||||||
|
dismissAfter={5000 + idx * 1000}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import { useSelectableClick } from 'flavours/glitch/hooks/useSelectableClick';
|
|||||||
const offset = [0, 4] as OffsetValue;
|
const offset = [0, 4] as OffsetValue;
|
||||||
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
|
||||||
|
|
||||||
export const AltTextBadge: React.FC<{
|
export const AltTextBadge: React.FC<{ description: string }> = ({
|
||||||
description: string;
|
description,
|
||||||
}> = ({ description }) => {
|
}) => {
|
||||||
const accessibilityId = useId();
|
const accessibilityId = useId();
|
||||||
const anchorRef = useRef<HTMLButtonElement>(null);
|
const anchorRef = useRef<HTMLButtonElement>(null);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -56,7 +56,7 @@ export const AltTextBadge: React.FC<{
|
|||||||
{({ props }) => (
|
{({ props }) => (
|
||||||
<div {...props} className='hover-card-controller'>
|
<div {...props} className='hover-card-controller'>
|
||||||
<div // eslint-disable-line jsx-a11y/no-noninteractive-element-interactions
|
<div // eslint-disable-line jsx-a11y/no-noninteractive-element-interactions
|
||||||
className='media-gallery__alt__popover dropdown-animation'
|
className='info-tooltip dropdown-animation'
|
||||||
role='region'
|
role='region'
|
||||||
id={accessibilityId}
|
id={accessibilityId}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ const AutosuggestTextarea = forwardRef(({
|
|||||||
onFocus,
|
onFocus,
|
||||||
autoFocus = true,
|
autoFocus = true,
|
||||||
lang,
|
lang,
|
||||||
|
className,
|
||||||
}, textareaRef) => {
|
}, textareaRef) => {
|
||||||
|
|
||||||
const [suggestionsHidden, setSuggestionsHidden] = useState(true);
|
const [suggestionsHidden, setSuggestionsHidden] = useState(true);
|
||||||
@@ -192,7 +193,7 @@ const AutosuggestTextarea = forwardRef(({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='autosuggest-textarea'>
|
<div className={classNames('autosuggest-textarea', className)}>
|
||||||
<Textarea
|
<Textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
className='autosuggest-textarea__textarea'
|
className='autosuggest-textarea__textarea'
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface BaseProps
|
|||||||
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
|
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
|
||||||
block?: boolean;
|
block?: boolean;
|
||||||
secondary?: boolean;
|
secondary?: boolean;
|
||||||
|
plain?: boolean;
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
dangerous?: boolean;
|
dangerous?: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
@@ -35,6 +36,7 @@ export const Button: React.FC<Props> = ({
|
|||||||
disabled,
|
disabled,
|
||||||
block,
|
block,
|
||||||
secondary,
|
secondary,
|
||||||
|
plain,
|
||||||
compact,
|
compact,
|
||||||
dangerous,
|
dangerous,
|
||||||
loading,
|
loading,
|
||||||
@@ -62,6 +64,7 @@ export const Button: React.FC<Props> = ({
|
|||||||
<button
|
<button
|
||||||
className={classNames('button', className, {
|
className={classNames('button', className, {
|
||||||
'button-secondary': secondary,
|
'button-secondary': secondary,
|
||||||
|
'button--plain': plain,
|
||||||
'button--compact': compact,
|
'button--compact': compact,
|
||||||
'button--block': block,
|
'button--block': block,
|
||||||
'button--dangerous': dangerous,
|
'button--dangerous': dangerous,
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-call,
|
|
||||||
@typescript-eslint/no-unsafe-return,
|
|
||||||
@typescript-eslint/no-unsafe-assignment,
|
|
||||||
@typescript-eslint/no-unsafe-member-access
|
|
||||||
-- the settings store is not yet typed */
|
|
||||||
import type { PropsWithChildren } from 'react';
|
import type { PropsWithChildren } from 'react';
|
||||||
import { useCallback, useState, useEffect } from 'react';
|
import { useCallback, useState, useEffect } from 'react';
|
||||||
|
|
||||||
@@ -23,31 +18,48 @@ interface Props {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DismissableBanner: React.FC<PropsWithChildren<Props>> = ({
|
export function useDismissableBannerState({ id }: Props) {
|
||||||
id,
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
children,
|
const dismissed: boolean = useAppSelector((state) =>
|
||||||
}) => {
|
/* eslint-disable-next-line */
|
||||||
const dismissed = useAppSelector((state) =>
|
|
||||||
state.settings.getIn(['dismissed_banners', id], false),
|
state.settings.getIn(['dismissed_banners', id], false),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [isVisible, setIsVisible] = useState(
|
||||||
|
!bannerSettings.get(id) && !dismissed,
|
||||||
|
);
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const [visible, setVisible] = useState(!bannerSettings.get(id) && !dismissed);
|
const dismiss = useCallback(() => {
|
||||||
const intl = useIntl();
|
setIsVisible(false);
|
||||||
|
|
||||||
const handleDismiss = useCallback(() => {
|
|
||||||
setVisible(false);
|
|
||||||
bannerSettings.set(id, true);
|
bannerSettings.set(id, true);
|
||||||
dispatch(changeSetting(['dismissed_banners', id], true));
|
dispatch(changeSetting(['dismissed_banners', id], true));
|
||||||
}, [id, dispatch]);
|
}, [id, dispatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!visible && !dismissed) {
|
// Store legacy localStorage setting on server
|
||||||
|
if (!isVisible && !dismissed) {
|
||||||
dispatch(changeSetting(['dismissed_banners', id], true));
|
dispatch(changeSetting(['dismissed_banners', id], true));
|
||||||
}
|
}
|
||||||
}, [id, dispatch, visible, dismissed]);
|
}, [id, dispatch, isVisible, dismissed]);
|
||||||
|
|
||||||
if (!visible) {
|
return {
|
||||||
|
wasDismissed: !isVisible,
|
||||||
|
dismiss,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DismissableBanner: React.FC<PropsWithChildren<Props>> = ({
|
||||||
|
id,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { wasDismissed, dismiss } = useDismissableBannerState({
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (wasDismissed) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +70,7 @@ export const DismissableBanner: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
icon='times'
|
icon='times'
|
||||||
iconComponent={CloseIcon}
|
iconComponent={CloseIcon}
|
||||||
title={intl.formatMessage(messages.dismiss)}
|
title={intl.formatMessage(messages.dismiss)}
|
||||||
onClick={handleDismiss}
|
onClick={dismiss}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import type { List } from 'immutable';
|
|
||||||
|
|
||||||
import type { Account } from 'flavours/glitch/models/account';
|
|
||||||
|
|
||||||
import { autoPlayGif } from '../initial_state';
|
|
||||||
|
|
||||||
import { Skeleton } from './skeleton';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
account?: Account;
|
|
||||||
others?: List<Account>;
|
|
||||||
localDomain?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DisplayName extends React.PureComponent<Props> {
|
|
||||||
handleMouseEnter: React.ReactEventHandler<HTMLSpanElement> = ({
|
|
||||||
currentTarget,
|
|
||||||
}) => {
|
|
||||||
if (autoPlayGif) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emojis =
|
|
||||||
currentTarget.querySelectorAll<HTMLImageElement>('img.custom-emoji');
|
|
||||||
|
|
||||||
emojis.forEach((emoji) => {
|
|
||||||
const originalSrc = emoji.getAttribute('data-original');
|
|
||||||
if (originalSrc != null) emoji.src = originalSrc;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMouseLeave: React.ReactEventHandler<HTMLSpanElement> = ({
|
|
||||||
currentTarget,
|
|
||||||
}) => {
|
|
||||||
if (autoPlayGif) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emojis =
|
|
||||||
currentTarget.querySelectorAll<HTMLImageElement>('img.custom-emoji');
|
|
||||||
|
|
||||||
emojis.forEach((emoji) => {
|
|
||||||
const staticSrc = emoji.getAttribute('data-static');
|
|
||||||
if (staticSrc != null) emoji.src = staticSrc;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { others, localDomain } = this.props;
|
|
||||||
|
|
||||||
let displayName: React.ReactNode,
|
|
||||||
suffix: React.ReactNode,
|
|
||||||
account: Account | undefined;
|
|
||||||
|
|
||||||
if (others && others.size > 0) {
|
|
||||||
account = others.first();
|
|
||||||
} else if (this.props.account) {
|
|
||||||
account = this.props.account;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (others && others.size > 1) {
|
|
||||||
displayName = others
|
|
||||||
.take(2)
|
|
||||||
.map((a) => (
|
|
||||||
<bdi key={a.get('id')}>
|
|
||||||
<strong
|
|
||||||
className='display-name__html'
|
|
||||||
dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }}
|
|
||||||
/>
|
|
||||||
</bdi>
|
|
||||||
))
|
|
||||||
.reduce((prev, cur) => [prev, ', ', cur]);
|
|
||||||
|
|
||||||
if (others.size - 2 > 0) {
|
|
||||||
suffix = `+${others.size - 2}`;
|
|
||||||
}
|
|
||||||
} else if (account) {
|
|
||||||
let acct = account.get('acct');
|
|
||||||
|
|
||||||
if (!acct.includes('@') && localDomain) {
|
|
||||||
acct = `${acct}@${localDomain}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
displayName = (
|
|
||||||
<bdi>
|
|
||||||
<strong
|
|
||||||
className='display-name__html'
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: account.get('display_name_html'),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</bdi>
|
|
||||||
);
|
|
||||||
suffix = <span className='display-name__account'>@{acct}</span>;
|
|
||||||
} else {
|
|
||||||
displayName = (
|
|
||||||
<bdi>
|
|
||||||
<strong className='display-name__html'>
|
|
||||||
<Skeleton width='10ch' />
|
|
||||||
</strong>
|
|
||||||
</bdi>
|
|
||||||
);
|
|
||||||
suffix = (
|
|
||||||
<span className='display-name__account'>
|
|
||||||
<Skeleton width='7ch' />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className='display-name'
|
|
||||||
onMouseEnter={this.handleMouseEnter}
|
|
||||||
onMouseLeave={this.handleMouseLeave}
|
|
||||||
>
|
|
||||||
{displayName} {suffix}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import type { ComponentPropsWithoutRef, FC } from 'react';
|
||||||
|
|
||||||
|
import { Skeleton } from '../skeleton';
|
||||||
|
|
||||||
|
import type { DisplayNameProps } from './index';
|
||||||
|
import { DisplayNameWithoutDomain } from './no-domain';
|
||||||
|
|
||||||
|
export const DisplayNameDefault: FC<
|
||||||
|
Omit<DisplayNameProps, 'variant'> & ComponentPropsWithoutRef<'span'>
|
||||||
|
> = ({ account, localDomain, className, ...props }) => {
|
||||||
|
const username = useMemo(() => {
|
||||||
|
if (!account) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let acct = account.get('acct');
|
||||||
|
|
||||||
|
if (!acct.includes('@') && localDomain) {
|
||||||
|
acct = `${acct}@${localDomain}`;
|
||||||
|
}
|
||||||
|
return `@${acct}`;
|
||||||
|
}, [account, localDomain]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DisplayNameWithoutDomain
|
||||||
|
account={account}
|
||||||
|
className={className}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
<span className='display-name__account'>
|
||||||
|
{username ?? <Skeleton width='7ch' />}
|
||||||
|
</span>
|
||||||
|
</DisplayNameWithoutDomain>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import type { ComponentPropsWithoutRef, FC } from 'react';
|
||||||
|
|
||||||
|
import type { LinkProps } from 'react-router-dom';
|
||||||
|
|
||||||
|
import type { Account } from '@/flavours/glitch/models/account';
|
||||||
|
import { Permalink } from 'flavours/glitch/components/permalink';
|
||||||
|
|
||||||
|
import { DisplayNameDefault } from './default';
|
||||||
|
import { DisplayNameWithoutDomain } from './no-domain';
|
||||||
|
import { DisplayNameSimple } from './simple';
|
||||||
|
|
||||||
|
export interface DisplayNameProps {
|
||||||
|
account?: Account;
|
||||||
|
localDomain?: string;
|
||||||
|
variant?: 'default' | 'simple' | 'noDomain';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DisplayName: FC<
|
||||||
|
DisplayNameProps & ComponentPropsWithoutRef<'span'>
|
||||||
|
> = ({ variant = 'default', ...props }) => {
|
||||||
|
if (variant === 'simple') {
|
||||||
|
return <DisplayNameSimple {...props} />;
|
||||||
|
} else if (variant === 'noDomain') {
|
||||||
|
return <DisplayNameWithoutDomain {...props} />;
|
||||||
|
}
|
||||||
|
return <DisplayNameDefault {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LinkedDisplayName: FC<
|
||||||
|
Omit<LinkProps, 'to'> & {
|
||||||
|
displayProps: DisplayNameProps & ComponentPropsWithoutRef<'span'>;
|
||||||
|
}
|
||||||
|
> = ({ displayProps, children, ...linkProps }) => {
|
||||||
|
const { account } = displayProps;
|
||||||
|
if (!account) {
|
||||||
|
return <DisplayName {...displayProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Permalink
|
||||||
|
href={account.url}
|
||||||
|
to={`/@${account.acct}`}
|
||||||
|
title={`@${account.acct}`}
|
||||||
|
data-id={account.id}
|
||||||
|
data-hover-card-account={account.id}
|
||||||
|
{...linkProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<DisplayName {...displayProps} />
|
||||||
|
</Permalink>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import type { ComponentPropsWithoutRef, FC } from 'react';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { EmojiHTML } from '@/flavours/glitch/features/emoji/emoji_html';
|
||||||
|
import { isModernEmojiEnabled } from '@/flavours/glitch/utils/environment';
|
||||||
|
|
||||||
|
import { Skeleton } from '../skeleton';
|
||||||
|
|
||||||
|
import type { DisplayNameProps } from './index';
|
||||||
|
|
||||||
|
export const DisplayNameWithoutDomain: FC<
|
||||||
|
Omit<DisplayNameProps, 'variant' | 'localDomain'> &
|
||||||
|
ComponentPropsWithoutRef<'span'>
|
||||||
|
> = ({ account, className, children, ...props }) => {
|
||||||
|
return (
|
||||||
|
<span {...props} className={classNames('display-name', className)}>
|
||||||
|
<bdi>
|
||||||
|
{account ? (
|
||||||
|
<EmojiHTML
|
||||||
|
className='display-name__html'
|
||||||
|
htmlString={
|
||||||
|
isModernEmojiEnabled()
|
||||||
|
? account.get('display_name')
|
||||||
|
: account.get('display_name_html')
|
||||||
|
}
|
||||||
|
shallow
|
||||||
|
as='strong'
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<strong className='display-name__html'>
|
||||||
|
<Skeleton width='10ch' />
|
||||||
|
</strong>
|
||||||
|
)}
|
||||||
|
</bdi>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { ComponentPropsWithoutRef, FC } from 'react';
|
||||||
|
|
||||||
|
import { EmojiHTML } from '@/flavours/glitch/features/emoji/emoji_html';
|
||||||
|
import { isModernEmojiEnabled } from '@/flavours/glitch/utils/environment';
|
||||||
|
|
||||||
|
import type { DisplayNameProps } from './index';
|
||||||
|
|
||||||
|
export const DisplayNameSimple: FC<
|
||||||
|
Omit<DisplayNameProps, 'variant' | 'localDomain'> &
|
||||||
|
ComponentPropsWithoutRef<'span'>
|
||||||
|
> = ({ account, ...props }) => {
|
||||||
|
if (!account) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const accountName = isModernEmojiEnabled()
|
||||||
|
? account.get('display_name')
|
||||||
|
: account.get('display_name_html');
|
||||||
|
return (
|
||||||
|
<bdi>
|
||||||
|
<EmojiHTML {...props} htmlString={accountName} shallow as='span' />
|
||||||
|
</bdi>
|
||||||
|
);
|
||||||
|
};
|
||||||
141
app/javascript/flavours/glitch/components/dropdown/index.tsx
Normal file
141
app/javascript/flavours/glitch/components/dropdown/index.tsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import { useCallback, useId, useMemo, useRef, useState } from 'react';
|
||||||
|
import type { ComponentPropsWithoutRef, FC } from 'react';
|
||||||
|
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
import type { MessageDescriptor } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import Overlay from 'react-overlays/Overlay';
|
||||||
|
|
||||||
|
import UnfoldMoreIcon from '@/material-icons/400-24px/unfold_more.svg?react';
|
||||||
|
|
||||||
|
import type { SelectItem } from '../dropdown_selector';
|
||||||
|
import { DropdownSelector } from '../dropdown_selector';
|
||||||
|
import { Icon } from '../icon';
|
||||||
|
|
||||||
|
import { matchWidth } from './utils';
|
||||||
|
|
||||||
|
interface DropdownProps {
|
||||||
|
disabled?: boolean;
|
||||||
|
items: SelectItem[];
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
current: string;
|
||||||
|
labelId: string;
|
||||||
|
descriptionId?: string;
|
||||||
|
emptyText?: MessageDescriptor;
|
||||||
|
classPrefix: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Dropdown: FC<
|
||||||
|
DropdownProps & Omit<ComponentPropsWithoutRef<'button'>, keyof DropdownProps>
|
||||||
|
> = ({
|
||||||
|
disabled,
|
||||||
|
items,
|
||||||
|
current,
|
||||||
|
onChange,
|
||||||
|
labelId,
|
||||||
|
descriptionId,
|
||||||
|
classPrefix,
|
||||||
|
className,
|
||||||
|
id,
|
||||||
|
...buttonProps
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const uniqueId = useId();
|
||||||
|
const buttonId = id ?? `${uniqueId}-button`;
|
||||||
|
const listboxId = `${uniqueId}-listbox`;
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleToggle = useCallback(() => {
|
||||||
|
if (!disabled) {
|
||||||
|
setOpen((prevOpen) => {
|
||||||
|
buttonRef.current?.focus();
|
||||||
|
return !prevOpen;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [disabled]);
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
setOpen(false);
|
||||||
|
buttonRef.current?.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const currentText = useMemo(
|
||||||
|
() =>
|
||||||
|
items.find((i) => i.value === current)?.text ??
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'dropdown.empty',
|
||||||
|
defaultMessage: 'Select an option',
|
||||||
|
}),
|
||||||
|
[current, intl, items],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
{...buttonProps}
|
||||||
|
id={buttonId}
|
||||||
|
aria-labelledby={`${labelId} ${buttonId}`}
|
||||||
|
aria-describedby={descriptionId}
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-controls={listboxId}
|
||||||
|
onClick={handleToggle}
|
||||||
|
disabled={disabled}
|
||||||
|
className={classNames(
|
||||||
|
`${classPrefix}__button`,
|
||||||
|
{
|
||||||
|
active: open,
|
||||||
|
disabled,
|
||||||
|
},
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
ref={buttonRef}
|
||||||
|
>
|
||||||
|
{currentText}
|
||||||
|
<Icon
|
||||||
|
id='unfold-icon'
|
||||||
|
icon={UnfoldMoreIcon}
|
||||||
|
className={`${classPrefix}__icon`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Overlay
|
||||||
|
show={open}
|
||||||
|
offset={[0, 0]}
|
||||||
|
placement='bottom-start'
|
||||||
|
onHide={handleClose}
|
||||||
|
flip
|
||||||
|
target={buttonRef.current}
|
||||||
|
popperConfig={{
|
||||||
|
strategy: 'fixed',
|
||||||
|
modifiers: [matchWidth],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ props, placement }) => (
|
||||||
|
<div {...props} className={`${classPrefix}__overlay`}>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'dropdown-animation',
|
||||||
|
`${classPrefix}__dropdown`,
|
||||||
|
placement,
|
||||||
|
)}
|
||||||
|
id={listboxId}
|
||||||
|
>
|
||||||
|
<DropdownSelector
|
||||||
|
items={items}
|
||||||
|
value={current}
|
||||||
|
onClose={handleClose}
|
||||||
|
onChange={onChange}
|
||||||
|
classNamePrefix={classPrefix}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Overlay>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user