mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-13 07:49:29 +00:00
Compare commits
634 Commits
v4.5.0-bet
...
8b418b84d0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b418b84d0 | ||
|
|
f817300d8d | ||
|
|
35a89a0173 | ||
|
|
b5721dbd4a | ||
|
|
38f623eee7 | ||
|
|
17ba99e5de | ||
|
|
d6f2a3ac8d | ||
|
|
c42b9f6996 | ||
|
|
76184c998c | ||
|
|
8137ce87ce | ||
|
|
37426288d9 | ||
|
|
801fee7593 | ||
|
|
6838497fe8 | ||
|
|
7b8a5d42f1 | ||
|
|
cd71fdcdff | ||
|
|
91500a7f53 | ||
|
|
5422e43e31 | ||
|
|
5a66331003 | ||
|
|
09e3955145 | ||
|
|
e554e5723d | ||
|
|
315f5e5a31 | ||
|
|
9d81561bb2 | ||
|
|
ac71771d98 | ||
|
|
697569e5f9 | ||
|
|
4cdcdaa7d9 | ||
|
|
9702cbb41c | ||
|
|
ea768c17db | ||
|
|
5347cabf3e | ||
|
|
eef40ba96b | ||
|
|
9063c3b660 | ||
|
|
e147947eb8 | ||
|
|
8c52889c86 | ||
|
|
05e45beb34 | ||
|
|
607449336d | ||
|
|
85bf5be604 | ||
|
|
cf23f0414f | ||
|
|
55becaa1b5 | ||
|
|
8625721805 | ||
|
|
cd34331842 | ||
|
|
691f4e3210 | ||
|
|
7f1862b67e | ||
|
|
71cda79e3b | ||
|
|
c3f254f170 | ||
|
|
05691293f0 | ||
|
|
2f6cdd6d47 | ||
|
|
4625bbe282 | ||
|
|
4dc196b595 | ||
|
|
3b4c4c5b09 | ||
|
|
7fe3e80758 | ||
|
|
1ae3b4672b | ||
|
|
007ae588d8 | ||
|
|
ce22c835ac | ||
|
|
9b851616fe | ||
|
|
591776d7ad | ||
|
|
7f1f3236c6 | ||
|
|
852727a226 | ||
|
|
429d6bcab4 | ||
|
|
e47a5dd1c2 | ||
|
|
4ec761debd | ||
|
|
d895ea3433 | ||
|
|
49105a28a3 | ||
|
|
1cb650d107 | ||
|
|
0f2ba97c99 | ||
|
|
0061f9a699 | ||
|
|
2d93e63e43 | ||
|
|
5a5ba02f96 | ||
|
|
c8f365fd1d | ||
|
|
691fe7cb4c | ||
|
|
376332bfe7 | ||
|
|
8bec8c373b | ||
|
|
5f2d64c4b0 | ||
|
|
1faaa9706a | ||
|
|
ac926baa74 | ||
|
|
f21f8df4cc | ||
|
|
62dc7c1ee6 | ||
|
|
c63393c963 | ||
|
|
34aa825e96 | ||
|
|
46f3b39fae | ||
|
|
65b216353e | ||
|
|
0dac31dfd5 | ||
|
|
75b9e9a8b0 | ||
|
|
88aed3c11a | ||
|
|
9921fa1af7 | ||
|
|
5a7a4fff11 | ||
|
|
9cf52fb976 | ||
|
|
baef5b1659 | ||
|
|
832d8c7397 | ||
|
|
d063af2594 | ||
|
|
31c392b1bc | ||
|
|
498e88f059 | ||
|
|
7c730e9041 | ||
|
|
b3b5bf26d1 | ||
|
|
4e6d1892b9 | ||
|
|
e5e3a64a9b | ||
|
|
234990cc37 | ||
|
|
08da9d8fc5 | ||
|
|
c97d25fcbd | ||
|
|
e222664539 | ||
|
|
9d10137c7c | ||
|
|
5d84957117 | ||
|
|
954f397743 | ||
|
|
73294e2561 | ||
|
|
d2e1c0e1e2 | ||
|
|
d600950b83 | ||
|
|
1ca3894048 | ||
|
|
13cf55c501 | ||
|
|
f393ff93cb | ||
|
|
9c3b41f0a4 | ||
|
|
e45ecc7d13 | ||
|
|
f8422e1fa4 | ||
|
|
9aec6936e5 | ||
|
|
2b25b65972 | ||
|
|
100b20f290 | ||
|
|
c5c8100d02 | ||
|
|
2e5744e8c6 | ||
|
|
2d203ca72a | ||
|
|
1f9ddb7cf6 | ||
|
|
048746e56b | ||
|
|
f5d6f60ca7 | ||
|
|
e5651e7e04 | ||
|
|
edfbcfb3f5 | ||
|
|
f562ad9f67 | ||
|
|
a4357def8a | ||
|
|
bb097056dc | ||
|
|
906dd88d7e | ||
|
|
b3135c1eed | ||
|
|
b1d00f288f | ||
|
|
7bc48f2833 | ||
|
|
9ede21cbe2 | ||
|
|
17379b73f7 | ||
|
|
99ff59b7b2 | ||
|
|
51698213b5 | ||
|
|
78feddec79 | ||
|
|
9d47d6790f | ||
|
|
3b4b57e950 | ||
|
|
b75a01634e | ||
|
|
18d46054b5 | ||
|
|
281d12d5d6 | ||
|
|
44e6abe48b | ||
|
|
2e543ff246 | ||
|
|
9334bd9ede | ||
|
|
801672e3cb | ||
|
|
92278796c3 | ||
|
|
37ccffa95a | ||
|
|
84ffb107c3 | ||
|
|
f896bbac3b | ||
|
|
6b38352b17 | ||
|
|
f12f198f61 | ||
|
|
e0912c1729 | ||
|
|
945ef5a8e1 | ||
|
|
5f33ac208f | ||
|
|
2bd7c855b0 | ||
|
|
44ff2c32d3 | ||
|
|
826e9d7047 | ||
|
|
f07cff42c2 | ||
|
|
0d2e9522ff | ||
|
|
0004ed4c80 | ||
|
|
07ecf648dd | ||
|
|
90466d0262 | ||
|
|
199376a080 | ||
|
|
e126cfc76d | ||
|
|
322a4fee53 | ||
|
|
be2caba527 | ||
|
|
002632c3bb | ||
|
|
81510455d1 | ||
|
|
ee7e756e89 | ||
|
|
f87f30c1ac | ||
|
|
1757a0f0f3 | ||
|
|
cb4f1cc89c | ||
|
|
00163e89bf | ||
|
|
59e48657cf | ||
|
|
384594f462 | ||
|
|
cd9d166312 | ||
|
|
6f4f9942b9 | ||
|
|
7e7c21032b | ||
|
|
382dec843b | ||
|
|
868d45df2f | ||
|
|
0725afe1a9 | ||
|
|
09697045a9 | ||
|
|
3e77c3bc8c | ||
|
|
bd02cd4591 | ||
|
|
4ca458e0b4 | ||
|
|
8c772028ac | ||
|
|
861625fdca | ||
|
|
ca53195b31 | ||
|
|
a26636ff1f | ||
|
|
204143becc | ||
|
|
f0d7ea61ef | ||
|
|
4d92051f40 | ||
|
|
b76530a7f1 | ||
|
|
76d8ac3fe6 | ||
|
|
96d5e57351 | ||
|
|
57bfe863f3 | ||
|
|
b16452dd99 | ||
|
|
1bc13609ab | ||
|
|
e44a9c0879 | ||
|
|
f1bf6e6344 | ||
|
|
5885b6715a | ||
|
|
975c7097b8 | ||
|
|
652ed7ab50 | ||
|
|
585545d0d5 | ||
|
|
d967137adf | ||
|
|
ad7839e551 | ||
|
|
8a235dd187 | ||
|
|
48fe679728 | ||
|
|
687f3a2a01 | ||
|
|
7ffa5fa0c4 | ||
|
|
cfa4f402ef | ||
|
|
aa131e538c | ||
|
|
6151febd73 | ||
|
|
a54334b714 | ||
|
|
2f2065751d | ||
|
|
aec23fd4a2 | ||
|
|
8e70c54d0e | ||
|
|
284223f45f | ||
|
|
8e68d6c6bf | ||
|
|
dd7d750f5d | ||
|
|
21ce99f746 | ||
|
|
8e8669b5ef | ||
|
|
a28f1d0110 | ||
|
|
f01e80bed3 | ||
|
|
dc67dbba82 | ||
|
|
bb9a633b99 | ||
|
|
21110f0270 | ||
|
|
4612014192 | ||
|
|
b1974a2147 | ||
|
|
c57ca36006 | ||
|
|
69dfde3153 | ||
|
|
c5a6519af9 | ||
|
|
9c7f27ba14 | ||
|
|
76ba4000d9 | ||
|
|
89d04f3bb3 | ||
|
|
7a56972381 | ||
|
|
a4fd9b704a | ||
|
|
fa721568e0 | ||
|
|
ea616ac4a4 | ||
|
|
01b11c328c | ||
|
|
bc7c83ba76 | ||
|
|
366856f3bc | ||
|
|
4d0aab4a31 | ||
|
|
c22b203bca | ||
|
|
52b92bdc9c | ||
|
|
4f6a7e44d1 | ||
|
|
81ffd1d3c7 | ||
|
|
9872197d1f | ||
|
|
41279ac801 | ||
|
|
902b5a169c | ||
|
|
be0e23bb0a | ||
|
|
c820c66d3c | ||
|
|
b4daad8c89 | ||
|
|
b14f113929 | ||
|
|
caffb0fd63 | ||
|
|
53703202fb | ||
|
|
59fdff5dc5 | ||
|
|
04bdfa1957 | ||
|
|
04c566e2e9 | ||
|
|
72c582e7e5 | ||
|
|
284b46fee7 | ||
|
|
489bee8f4e | ||
|
|
932f479a34 | ||
|
|
8839ecf2a4 | ||
|
|
5645a017b3 | ||
|
|
8817ebda50 | ||
|
|
f782c2c8e9 | ||
|
|
ee257dc307 | ||
|
|
8240644b6e | ||
|
|
593d21d2ed | ||
|
|
951816c5d6 | ||
|
|
e0d7230f97 | ||
|
|
c87b052829 | ||
|
|
ebc99cd597 | ||
|
|
6db4297193 | ||
|
|
bc47cba123 | ||
|
|
f8ffb85566 | ||
|
|
7dbb2ac79a | ||
|
|
bc81e299f2 | ||
|
|
277a4c80c0 | ||
|
|
7be8fe6370 | ||
|
|
4ab1d5d724 | ||
|
|
c5eca8ffb2 | ||
|
|
f25e066112 | ||
|
|
6d8c43ab85 | ||
|
|
0d7c23469b | ||
|
|
f243a00b90 | ||
|
|
6e294828d6 | ||
|
|
101bd01e6e | ||
|
|
d53ff25529 | ||
|
|
8ab9040afc | ||
|
|
19cc39abf0 | ||
|
|
bebc79d160 | ||
|
|
98c8c1ebd2 | ||
|
|
998d4cc0dc | ||
|
|
9dbebbb2ee | ||
|
|
3f292e0f5b | ||
|
|
9dd7c816d2 | ||
|
|
191d6b071c | ||
|
|
5f01e75290 | ||
|
|
1c749e21f8 | ||
|
|
01f9397e37 | ||
|
|
62ce66dacb | ||
|
|
9525134c28 | ||
|
|
7e7f63a2ef | ||
|
|
ed3710e58f | ||
|
|
8abec0ffcb | ||
|
|
00cbc1b910 | ||
|
|
f303f3458d | ||
|
|
9f3573d446 | ||
|
|
4b1532e008 | ||
|
|
ff0fca018a | ||
|
|
12ac3317aa | ||
|
|
fdfbc63199 | ||
|
|
e265c6bd4c | ||
|
|
48146e5371 | ||
|
|
b5a2fe715d | ||
|
|
11b75d616a | ||
|
|
394ed551bb | ||
|
|
565f437f93 | ||
|
|
a002048c8c | ||
|
|
fa5318b333 | ||
|
|
095a9571e2 | ||
|
|
7ca2a7d9d6 | ||
|
|
b53ee04475 | ||
|
|
2759bafe09 | ||
|
|
84cdb6cc66 | ||
|
|
ff5d745e3d | ||
|
|
391c77f277 | ||
|
|
bc5397a0db | ||
|
|
f5cbe73d76 | ||
|
|
5af57000a0 | ||
|
|
c428129c48 | ||
|
|
5b75667c03 | ||
|
|
01f7a6796f | ||
|
|
d31aaf9ed8 | ||
|
|
1c3e7545cb | ||
|
|
400943cb4e | ||
|
|
9a42d00c12 | ||
|
|
9addad8ce5 | ||
|
|
4ddddc2573 | ||
|
|
1280792678 | ||
|
|
63e2ca5d27 | ||
|
|
b0790d828c | ||
|
|
89b5ceb5dc | ||
|
|
91d17b5891 | ||
|
|
6153479bad | ||
|
|
474cab03bd | ||
|
|
08ef682995 | ||
|
|
c1ef6e31cb | ||
|
|
441eb89537 | ||
|
|
882afd7748 | ||
|
|
8fb06ea0ca | ||
|
|
c7dc5767d3 | ||
|
|
6833878f95 | ||
|
|
70d71c10c8 | ||
|
|
315833cb75 | ||
|
|
dcf7fc1028 | ||
|
|
0ace564537 | ||
|
|
16dfa32578 | ||
|
|
0d48005b8a | ||
|
|
59e0ead418 | ||
|
|
76fb15dced | ||
|
|
4d44f4c57e | ||
|
|
5d3c1cdc9b | ||
|
|
15c33a16f7 | ||
|
|
152505dd9e | ||
|
|
6f1a30c4a6 | ||
|
|
1a890d2077 | ||
|
|
c8ebc974d1 | ||
|
|
53780dd49e | ||
|
|
663f93ca6a | ||
|
|
13395e2d4d | ||
|
|
e1b7da0985 | ||
|
|
ce080a1ca8 | ||
|
|
58b29be439 | ||
|
|
58b3fc0379 | ||
|
|
41a4022988 | ||
|
|
532bb7ea3c | ||
|
|
987104f435 | ||
|
|
50e1320c8d | ||
|
|
6337e036f3 | ||
|
|
1a31c412ca | ||
|
|
4a6f479535 | ||
|
|
15a7abd581 | ||
|
|
1b8d1cd6e4 | ||
|
|
35bd985727 | ||
|
|
c0c6f5ea32 | ||
|
|
3aeae8cafd | ||
|
|
947dfcc548 | ||
|
|
049dcebf9a | ||
|
|
f361a2c766 | ||
|
|
d4ec991126 | ||
|
|
1f4fe91708 | ||
|
|
a18d96ae2d | ||
|
|
9475eeaada | ||
|
|
e24151f688 | ||
|
|
58158eba00 | ||
|
|
e011d0fc53 | ||
|
|
7d8dc68c5b | ||
|
|
649187c30e | ||
|
|
4b5282881a | ||
|
|
a116d11bc6 | ||
|
|
31b72c0600 | ||
|
|
a7ba4ba446 | ||
|
|
26e7fe9771 | ||
|
|
1b795c12e9 | ||
|
|
afd5d5c2e5 | ||
|
|
3ab5ae1e4a | ||
|
|
4a9460f7bd | ||
|
|
e7692d0de8 | ||
|
|
cc77844540 | ||
|
|
337f16d33e | ||
|
|
ef20dcbf95 | ||
|
|
0c101b47bf | ||
|
|
f221ce530b | ||
|
|
8f1c73ed99 | ||
|
|
1a698d3b35 | ||
|
|
5a2edebc2b | ||
|
|
90d4b3b943 | ||
|
|
13457111d5 | ||
|
|
3a54d56fbd | ||
|
|
b5c550ff0b | ||
|
|
6c176e56ee | ||
|
|
b999a626e5 | ||
|
|
bb084da1f5 | ||
|
|
84e351cc3a | ||
|
|
7fced55ce7 | ||
|
|
8f37f9d012 | ||
|
|
8e4c9cf933 | ||
|
|
cf87da25ad | ||
|
|
966aaaaf56 | ||
|
|
5b880a2046 | ||
|
|
24aa5d0460 | ||
|
|
5ac3cceaf5 | ||
|
|
e5fbb49033 | ||
|
|
310ae6317e | ||
|
|
5c7d22e60a | ||
|
|
8d1208224f | ||
|
|
d1f57822af | ||
|
|
9b3e92bf17 | ||
|
|
e79e42f8f1 | ||
|
|
bae5877c84 | ||
|
|
61c0daffc9 | ||
|
|
f10c79c8d1 | ||
|
|
8781abf2bd | ||
|
|
7faf2eaa79 | ||
|
|
0bf974a758 | ||
|
|
5c0c77223b | ||
|
|
5fe74d2092 | ||
|
|
6dff6ae7f3 | ||
|
|
2ab482da18 | ||
|
|
a0686536c6 | ||
|
|
3d80e8b021 | ||
|
|
43fbff50b5 | ||
|
|
9f8e812c56 | ||
|
|
6ff4dad89d | ||
|
|
055f581ca5 | ||
|
|
8a2826604c | ||
|
|
d865a095d0 | ||
|
|
35abaa7ff1 | ||
|
|
fd4e51b3d8 | ||
|
|
2c4367bcfc | ||
|
|
d47ca1cc36 | ||
|
|
499ddfe8e1 | ||
|
|
7b61ad936d | ||
|
|
fcecbf31ed | ||
|
|
aefd728309 | ||
|
|
13a070f8d1 | ||
|
|
28cb345131 | ||
|
|
f3d9a4ed44 | ||
|
|
762e87b121 | ||
|
|
e5e9f8da93 | ||
|
|
ff1e19a506 | ||
|
|
2c5d3f934c | ||
|
|
a77038b288 | ||
|
|
ebf5cee38e | ||
|
|
e7cd5a430e | ||
|
|
868d782b2b | ||
|
|
1b60f597d7 | ||
|
|
2a9c7d2b9e | ||
|
|
9db64d6908 | ||
|
|
074b3fe57e | ||
|
|
002e592667 | ||
|
|
51877081b4 | ||
|
|
7b66eefd3e | ||
|
|
e437bb919f | ||
|
|
8f00874a0e | ||
|
|
ac920eb364 | ||
|
|
d04e6ec597 | ||
|
|
24234e6632 | ||
|
|
8cd8e69c4b | ||
|
|
293b8f6744 | ||
|
|
48f2597a36 | ||
|
|
12c487cc3e | ||
|
|
adfa407f6b | ||
|
|
c43a5a1834 | ||
|
|
52e2d24a4b | ||
|
|
f94353e1e3 | ||
|
|
0565eb62d6 | ||
|
|
48ec31bec8 | ||
|
|
3bd56b92c1 | ||
|
|
70b8281730 | ||
|
|
fb9e33099f | ||
|
|
79169408b0 | ||
|
|
5a051d07c6 | ||
|
|
5f3f75559f | ||
|
|
4896d2c4c6 | ||
|
|
795aaa14bf | ||
|
|
e1bd9b944a | ||
|
|
26ec19a649 | ||
|
|
b01d21c4d4 | ||
|
|
3ccb6632f2 | ||
|
|
8fb524e07f | ||
|
|
9c7d09993d | ||
|
|
3efc747be3 | ||
|
|
1f5cdb30c7 | ||
|
|
3cace4098a | ||
|
|
ccfac2716d | ||
|
|
422fa1cf9f | ||
|
|
2b5f6838ed | ||
|
|
85d0cdb5f7 | ||
|
|
e4fc18abfd | ||
|
|
e322c1777b | ||
|
|
f53c4db05c | ||
|
|
4905c194b8 | ||
|
|
7ba06a661c | ||
|
|
5d00ae7eb3 | ||
|
|
4b42fe6aba | ||
|
|
3bf99b8a4a | ||
|
|
d0d09fd3a5 | ||
|
|
76053fb4a9 | ||
|
|
402686c76c | ||
|
|
dc851c9efc | ||
|
|
1dead10312 | ||
|
|
e8382c7332 | ||
|
|
bfcf21e915 | ||
|
|
b60bae6361 | ||
|
|
38f15a89fe | ||
|
|
ab5b7e3776 | ||
|
|
1230d05b18 | ||
|
|
779a1f8448 | ||
|
|
e40ca321ed | ||
|
|
5f837001e6 | ||
|
|
2640cf5317 | ||
|
|
7f19b5ca2b | ||
|
|
305f1e5757 | ||
|
|
b11bd2bdbb | ||
|
|
deed31ba8c | ||
|
|
1ba579b0a1 | ||
|
|
6b2051b7b3 | ||
|
|
2fa5dd6d1f | ||
|
|
f7b99cd48a | ||
|
|
92aeecfbdc | ||
|
|
7774cd6670 | ||
|
|
c6e2ac5af9 | ||
|
|
ee87afd6a4 | ||
|
|
2d8b7a7fd8 | ||
|
|
cbc07af929 | ||
|
|
ee9a15031b | ||
|
|
9f7075a0ce | ||
|
|
c40648f7b3 | ||
|
|
2bd5c2f528 | ||
|
|
1e28ec628b | ||
|
|
7538bc77b7 | ||
|
|
7ea2af6ae2 | ||
|
|
6adbd9ce52 | ||
|
|
08ae77fd9c | ||
|
|
a0aa5fe8ea | ||
|
|
209434cb1d | ||
|
|
17eb1a7e66 | ||
|
|
aba30a85be | ||
|
|
de80a54555 | ||
|
|
b80ec3721d | ||
|
|
f4ca3e6c74 | ||
|
|
5674100f0b | ||
|
|
118ff13bd0 | ||
|
|
405a49df44 | ||
|
|
2b9e4294fe | ||
|
|
2eccd7b53c | ||
|
|
74172ced81 | ||
|
|
a74b3c549a | ||
|
|
8b3ff4f8b1 | ||
|
|
af1b658c20 | ||
|
|
7b44fd6d3c | ||
|
|
9080083263 | ||
|
|
341ea7f462 | ||
|
|
168cba35e3 | ||
|
|
6e2973aa2d | ||
|
|
97c8cc5606 | ||
|
|
50dfab30c2 | ||
|
|
53e20d5c83 | ||
|
|
0a1111d5a5 | ||
|
|
95be29d700 | ||
|
|
843c43c97a | ||
|
|
82483ed8b0 | ||
|
|
4c5e9e2419 | ||
|
|
5123f8aa94 | ||
|
|
460222e8e1 | ||
|
|
22e3c0e745 | ||
|
|
84563e54cf | ||
|
|
e1b109f074 | ||
|
|
d81bdb96e0 | ||
|
|
2b2de5cdb1 | ||
|
|
4c0104084d | ||
|
|
cfee957dd1 | ||
|
|
90ab9fe13c | ||
|
|
515ce38a52 | ||
|
|
612771de46 | ||
|
|
950e7beeea | ||
|
|
1018a4def4 | ||
|
|
811c1eaf7e | ||
|
|
6dad80eb8c | ||
|
|
c96e28a41d | ||
|
|
ccac6da3e8 | ||
|
|
ff8f0135b3 | ||
|
|
be5c1cceea | ||
|
|
614dd00457 | ||
|
|
323c6e51fe | ||
|
|
cf16039ea1 | ||
|
|
383445c977 | ||
|
|
2fb14cbb2b | ||
|
|
0512ffcbcd | ||
|
|
f56fc1bab4 | ||
|
|
20961c7538 | ||
|
|
63dc426fae | ||
|
|
210b389643 | ||
|
|
51d0bfcb38 | ||
|
|
7530f06dee | ||
|
|
28339cad6d | ||
|
|
5404f92cee | ||
|
|
05244c335d | ||
|
|
241ad1c587 | ||
|
|
ef53dcfd8c | ||
|
|
869eeecfee | ||
|
|
28a42bb62c | ||
|
|
905aa9434d |
@@ -73,7 +73,7 @@ services:
|
|||||||
hard: -1
|
hard: -1
|
||||||
|
|
||||||
libretranslate:
|
libretranslate:
|
||||||
image: libretranslate/libretranslate:v1.6.2
|
image: libretranslate/libretranslate:v1.7.3
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- lt-data:/home/libretranslate/.local
|
- lt-data:/home/libretranslate/.local
|
||||||
|
|||||||
@@ -318,21 +318,3 @@ MAX_POLL_OPTION_CHARS=100
|
|||||||
# -----------------------
|
# -----------------------
|
||||||
IP_RETENTION_PERIOD=31556952
|
IP_RETENTION_PERIOD=31556952
|
||||||
SESSION_RETENTION_PERIOD=31556952
|
SESSION_RETENTION_PERIOD=31556952
|
||||||
|
|
||||||
# Fetch All Replies Behavior
|
|
||||||
# --------------------------
|
|
||||||
|
|
||||||
# Period to wait between fetching replies (in minutes)
|
|
||||||
FETCH_REPLIES_COOLDOWN_MINUTES=15
|
|
||||||
|
|
||||||
# Period to wait after a post is first created before fetching its replies (in minutes)
|
|
||||||
FETCH_REPLIES_INITIAL_WAIT_MINUTES=5
|
|
||||||
|
|
||||||
# Max number of replies to fetch - total, recursively through a whole reply tree
|
|
||||||
FETCH_REPLIES_MAX_GLOBAL=1000
|
|
||||||
|
|
||||||
# Max number of replies to fetch - for a single post
|
|
||||||
FETCH_REPLIES_MAX_SINGLE=500
|
|
||||||
|
|
||||||
# Max number of replies Collection pages to fetch - total
|
|
||||||
FETCH_REPLIES_MAX_PAGES=500
|
|
||||||
|
|||||||
2
.github/actions/setup-javascript/action.yml
vendored
2
.github/actions/setup-javascript/action.yml
vendored
@@ -9,7 +9,7 @@ runs:
|
|||||||
using: 'composite'
|
using: 'composite'
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
|
|||||||
3
.github/renovate.json5
vendored
3
.github/renovate.json5
vendored
@@ -5,7 +5,6 @@
|
|||||||
'customManagers:dockerfileVersions',
|
'customManagers:dockerfileVersions',
|
||||||
':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.
|
|
||||||
':enableVulnerabilityAlertsWithLabel(security)',
|
':enableVulnerabilityAlertsWithLabel(security)',
|
||||||
],
|
],
|
||||||
rebaseWhen: 'conflicted',
|
rebaseWhen: 'conflicted',
|
||||||
@@ -23,8 +22,6 @@
|
|||||||
// Require Dependency Dashboard Approval for major version bumps of these node packages
|
// Require Dependency Dashboard Approval for major version bumps of these node packages
|
||||||
matchManagers: ['npm'],
|
matchManagers: ['npm'],
|
||||||
matchPackageNames: [
|
matchPackageNames: [
|
||||||
'tesseract.js', // Requires code changes
|
|
||||||
|
|
||||||
// react-router: Requires manual upgrade
|
// react-router: Requires manual upgrade
|
||||||
'history',
|
'history',
|
||||||
'react-router-dom',
|
'react-router-dom',
|
||||||
|
|||||||
8
.github/workflows/build-container-image.yml
vendored
8
.github/workflows/build-container-image.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
|||||||
- linux/arm64
|
- linux/arm64
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
env:
|
env:
|
||||||
@@ -100,7 +100,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
if: ${{ inputs.push_to_images != '' }}
|
if: ${{ inputs.push_to_images != '' }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
# `hashFiles` is used to disambiguate between streaming and non-streaming images
|
# `hashFiles` is used to disambiguate between streaming and non-streaming images
|
||||||
name: digests-${{ hashFiles(inputs.file_to_build) }}-${{ env.PLATFORM_PAIR }}
|
name: digests-${{ hashFiles(inputs.file_to_build) }}-${{ env.PLATFORM_PAIR }}
|
||||||
@@ -119,10 +119,10 @@ jobs:
|
|||||||
PUSH_TO_IMAGES: ${{ inputs.push_to_images }}
|
PUSH_TO_IMAGES: ${{ inputs.push_to_images }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
path: ${{ runner.temp }}/digests
|
path: ${{ runner.temp }}/digests
|
||||||
# `hashFiles` is used to disambiguate between streaming and non-streaming images
|
# `hashFiles` is used to disambiguate between streaming and non-streaming images
|
||||||
|
|||||||
2
.github/workflows/build-push-pr.yml
vendored
2
.github/workflows/build-push-pr.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# Repository needs to be cloned so `git rev-parse` below works
|
# Repository needs to be cloned so `git rev-parse` below works
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
- id: version_vars
|
- id: version_vars
|
||||||
run: |
|
run: |
|
||||||
echo mastodon_version_metadata=pr-${{ github.event.pull_request.number }}-$(git rev-parse --short ${{github.event.pull_request.head.sha}}) >> $GITHUB_OUTPUT
|
echo mastodon_version_metadata=pr-${{ github.event.pull_request.number }}-$(git rev-parse --short ${{github.event.pull_request.head.sha}}) >> $GITHUB_OUTPUT
|
||||||
|
|||||||
4
.github/workflows/build-releases.yml
vendored
4
.github/workflows/build-releases.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
# Only tag with latest when ran against the latest stable branch
|
# Only tag with latest when ran against the latest stable branch
|
||||||
# This needs to be updated after each minor version release
|
# This needs to be updated after each minor version release
|
||||||
flavor: |
|
flavor: |
|
||||||
latest=${{ startsWith(github.ref, 'refs/tags/v4.3.') }}
|
latest=${{ startsWith(github.ref, 'refs/tags/v4.5.') }}
|
||||||
tags: |
|
tags: |
|
||||||
type=pep440,pattern={{raw}}
|
type=pep440,pattern={{raw}}
|
||||||
type=pep440,pattern=v{{major}}.{{minor}}
|
type=pep440,pattern=v{{major}}.{{minor}}
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
# Only tag with latest when ran against the latest stable branch
|
# Only tag with latest when ran against the latest stable branch
|
||||||
# This needs to be updated after each minor version release
|
# This needs to be updated after each minor version release
|
||||||
flavor: |
|
flavor: |
|
||||||
latest=${{ startsWith(github.ref, 'refs/tags/v4.3.') }}
|
latest=${{ startsWith(github.ref, 'refs/tags/v4.5.') }}
|
||||||
tags: |
|
tags: |
|
||||||
type=pep440,pattern={{raw}}
|
type=pep440,pattern={{raw}}
|
||||||
type=pep440,pattern=v{{major}}.{{minor}}
|
type=pep440,pattern=v{{major}}.{{minor}}
|
||||||
|
|||||||
2
.github/workflows/bundler-audit.yml
vendored
2
.github/workflows/bundler-audit.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
|
|||||||
2
.github/workflows/check-i18n.yml
vendored
2
.github/workflows/check-i18n.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Ruby environment
|
- name: Set up Ruby environment
|
||||||
uses: ./.github/actions/setup-ruby
|
uses: ./.github/actions/setup-ruby
|
||||||
|
|||||||
33
.github/workflows/chromatic.yml
vendored
33
.github/workflows/chromatic.yml
vendored
@@ -1,11 +1,30 @@
|
|||||||
name: 'Chromatic'
|
name: 'Chromatic'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches-ignore:
|
branches-ignore:
|
||||||
- renovate/*
|
- renovate/*
|
||||||
- stable-*
|
- stable-*
|
||||||
paths:
|
|
||||||
|
jobs:
|
||||||
|
pathcheck:
|
||||||
|
name: Check for relevant changes
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
changed: ${{ steps.filter.outputs.src }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- uses: dorny/paths-filter@v3
|
||||||
|
id: filter
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
src:
|
||||||
- 'package.json'
|
- 'package.json'
|
||||||
- 'yarn.lock'
|
- 'yarn.lock'
|
||||||
- '**/*.js'
|
- '**/*.js'
|
||||||
@@ -16,16 +35,17 @@ on:
|
|||||||
- '**/*.scss'
|
- '**/*.scss'
|
||||||
- '.github/workflows/chromatic.yml'
|
- '.github/workflows/chromatic.yml'
|
||||||
|
|
||||||
jobs:
|
|
||||||
chromatic:
|
chromatic:
|
||||||
name: Run Chromatic
|
name: Run Chromatic
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.repository == 'mastodon/mastodon'
|
needs: pathcheck
|
||||||
|
if: github.repository == 'mastodon/mastodon' && needs.pathcheck.outputs.changed == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Javascript environment
|
- name: Set up Javascript environment
|
||||||
uses: ./.github/actions/setup-javascript
|
uses: ./.github/actions/setup-javascript
|
||||||
|
|
||||||
@@ -33,9 +53,10 @@ jobs:
|
|||||||
run: yarn build-storybook
|
run: yarn build-storybook
|
||||||
|
|
||||||
- name: Run Chromatic
|
- name: Run Chromatic
|
||||||
uses: chromaui/action@v12
|
uses: chromaui/action@v13
|
||||||
with:
|
with:
|
||||||
# ⚠️ Make sure to configure a `CHROMATIC_PROJECT_TOKEN` repository secret
|
|
||||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||||
zip: true
|
zip: true
|
||||||
storybookBuildDir: 'storybook-static'
|
storybookBuildDir: 'storybook-static'
|
||||||
|
exitZeroOnChanges: false # Fail workflow if changes are found
|
||||||
|
autoAcceptChanges: 'main' # Auto-accept changes on main branch only
|
||||||
|
|||||||
8
.github/workflows/codeql.yml
vendored
8
.github/workflows/codeql.yml
vendored
@@ -31,11 +31,11 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v4
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v3
|
uses: github/codeql-action/autobuild@v4
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
@@ -61,6 +61,6 @@ jobs:
|
|||||||
# ./location_of_script_within_repo/buildscript.sh
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3
|
uses: github/codeql-action/analyze@v4
|
||||||
with:
|
with:
|
||||||
category: '/language:${{matrix.language}}'
|
category: '/language:${{matrix.language}}'
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Increase Git http.postBuffer
|
- name: Increase Git http.postBuffer
|
||||||
# This is needed due to a bug in Ubuntu's cURL version?
|
# This is needed due to a bug in Ubuntu's cURL version?
|
||||||
|
|||||||
2
.github/workflows/crowdin-download.yml
vendored
2
.github/workflows/crowdin-download.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Increase Git http.postBuffer
|
- name: Increase Git http.postBuffer
|
||||||
# This is needed due to a bug in Ubuntu's cURL version?
|
# This is needed due to a bug in Ubuntu's cURL version?
|
||||||
|
|||||||
2
.github/workflows/crowdin-upload.yml
vendored
2
.github/workflows/crowdin-upload.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: crowdin action
|
- name: crowdin action
|
||||||
uses: crowdin/github-action@v2
|
uses: crowdin/github-action@v2
|
||||||
|
|||||||
2
.github/workflows/format-check.yml
vendored
2
.github/workflows/format-check.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Javascript environment
|
- name: Set up Javascript environment
|
||||||
uses: ./.github/actions/setup-javascript
|
uses: ./.github/actions/setup-javascript
|
||||||
|
|||||||
2
.github/workflows/lint-css.yml
vendored
2
.github/workflows/lint-css.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Javascript environment
|
- name: Set up Javascript environment
|
||||||
uses: ./.github/actions/setup-javascript
|
uses: ./.github/actions/setup-javascript
|
||||||
|
|||||||
2
.github/workflows/lint-haml.yml
vendored
2
.github/workflows/lint-haml.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
|
|||||||
2
.github/workflows/lint-js.yml
vendored
2
.github/workflows/lint-js.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Javascript environment
|
- name: Set up Javascript environment
|
||||||
uses: ./.github/actions/setup-javascript
|
uses: ./.github/actions/setup-javascript
|
||||||
|
|||||||
2
.github/workflows/lint-ruby.yml
vendored
2
.github/workflows/lint-ruby.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
|
|||||||
2
.github/workflows/test-js.yml
vendored
2
.github/workflows/test-js.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Javascript environment
|
- name: Set up Javascript environment
|
||||||
uses: ./.github/actions/setup-javascript
|
uses: ./.github/actions/setup-javascript
|
||||||
|
|||||||
2
.github/workflows/test-migrations.yml
vendored
2
.github/workflows/test-migrations.yml
vendored
@@ -72,7 +72,7 @@ jobs:
|
|||||||
BUNDLE_RETRY: 3
|
BUNDLE_RETRY: 3
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Ruby environment
|
- name: Set up Ruby environment
|
||||||
uses: ./.github/actions/setup-ruby
|
uses: ./.github/actions/setup-ruby
|
||||||
|
|||||||
28
.github/workflows/test-ruby.yml
vendored
28
.github/workflows/test-ruby.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
|||||||
SECRET_KEY_BASE_DUMMY: 1
|
SECRET_KEY_BASE_DUMMY: 1
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Ruby environment
|
- name: Set up Ruby environment
|
||||||
uses: ./.github/actions/setup-ruby
|
uses: ./.github/actions/setup-ruby
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs* tmp/cache/vite/last-build*.json
|
tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs* tmp/cache/vite/last-build*.json
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v5
|
||||||
if: matrix.mode == 'test'
|
if: matrix.mode == 'test'
|
||||||
with:
|
with:
|
||||||
path: |-
|
path: |-
|
||||||
@@ -128,9 +128,9 @@ jobs:
|
|||||||
- '3.3'
|
- '3.3'
|
||||||
- '.ruby-version'
|
- '.ruby-version'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
path: './'
|
path: './'
|
||||||
name: ${{ github.sha }}
|
name: ${{ github.sha }}
|
||||||
@@ -230,9 +230,9 @@ jobs:
|
|||||||
- '3.3'
|
- '3.3'
|
||||||
- '.ruby-version'
|
- '.ruby-version'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
path: './'
|
path: './'
|
||||||
name: ${{ github.sha }}
|
name: ${{ github.sha }}
|
||||||
@@ -309,9 +309,9 @@ jobs:
|
|||||||
- '.ruby-version'
|
- '.ruby-version'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
path: './'
|
path: './'
|
||||||
name: ${{ github.sha }}
|
name: ${{ github.sha }}
|
||||||
@@ -350,14 +350,14 @@ jobs:
|
|||||||
- run: bin/rspec spec/system --tag streaming --tag js
|
- run: bin/rspec spec/system --tag streaming --tag js
|
||||||
|
|
||||||
- name: Archive logs
|
- name: Archive logs
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: e2e-logs-${{ matrix.ruby-version }}
|
name: e2e-logs-${{ matrix.ruby-version }}
|
||||||
path: log/
|
path: log/
|
||||||
|
|
||||||
- name: Archive test screenshots
|
- name: Archive test screenshots
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: e2e-screenshots-${{ matrix.ruby-version }}
|
name: e2e-screenshots-${{ matrix.ruby-version }}
|
||||||
@@ -447,9 +447,9 @@ jobs:
|
|||||||
search-image: opensearchproject/opensearch:2
|
search-image: opensearchproject/opensearch:2
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
path: './'
|
path: './'
|
||||||
name: ${{ github.sha }}
|
name: ${{ github.sha }}
|
||||||
@@ -469,14 +469,14 @@ jobs:
|
|||||||
- run: bin/rspec --tag search
|
- run: bin/rspec --tag search
|
||||||
|
|
||||||
- name: Archive logs
|
- name: Archive logs
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: test-search-logs-${{ matrix.ruby-version }}
|
name: test-search-logs-${{ matrix.ruby-version }}
|
||||||
path: log/
|
path: log/
|
||||||
|
|
||||||
- name: Archive test screenshots
|
- name: Archive test screenshots
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: test-search-screenshots
|
name: test-search-screenshots
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,6 +23,7 @@
|
|||||||
/public/packs
|
/public/packs
|
||||||
/public/packs-dev
|
/public/packs-dev
|
||||||
/public/packs-test
|
/public/packs-test
|
||||||
|
stats.html
|
||||||
.env
|
.env
|
||||||
.env.production
|
.env.production
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ AUTHORS.md
|
|||||||
|
|
||||||
# Ignore glitch-soc vendored CSS reset
|
# Ignore glitch-soc vendored CSS reset
|
||||||
app/javascript/flavours/glitch/styles/reset.scss
|
app/javascript/flavours/glitch/styles/reset.scss
|
||||||
|
app/javascript/flavours/glitch/styles_new/mastodon/reset.scss
|
||||||
|
|
||||||
# Ignore win95 theme
|
# Ignore win95 theme
|
||||||
app/javascript/styles/win95.scss
|
app/javascript/styles/win95.scss
|
||||||
@@ -31,7 +31,7 @@ const config: StorybookConfig = {
|
|||||||
viteFinal(config) {
|
viteFinal(config) {
|
||||||
// For an unknown reason, Storybook does not use the root
|
// For an unknown reason, Storybook does not use the root
|
||||||
// from the Vite config so we need to set it manually.
|
// from the Vite config so we need to set it manually.
|
||||||
config.root = resolve(__dirname, '../app/javascript');
|
config.root = resolve(import.meta.dirname, '../app/javascript');
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
<html class="no-reduce-motion">
|
<html class="no-reduce-motion theme-light">
|
||||||
</html>
|
</html>
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
* - Please do NOT modify this file.
|
* - Please do NOT modify this file.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const PACKAGE_VERSION = '2.11.3'
|
const PACKAGE_VERSION = '2.12.1'
|
||||||
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
|
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
|
||||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||||
const activeClientIds = new Set()
|
const activeClientIds = new Set()
|
||||||
@@ -205,6 +205,7 @@ async function resolveMainClient(event) {
|
|||||||
* @param {FetchEvent} event
|
* @param {FetchEvent} event
|
||||||
* @param {Client | undefined} client
|
* @param {Client | undefined} client
|
||||||
* @param {string} requestId
|
* @param {string} requestId
|
||||||
|
* @param {number} requestInterceptedAt
|
||||||
* @returns {Promise<Response>}
|
* @returns {Promise<Response>}
|
||||||
*/
|
*/
|
||||||
async function getResponse(event, client, requestId, requestInterceptedAt) {
|
async function getResponse(event, client, requestId, requestInterceptedAt) {
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
diff --git a/lib/index.js b/lib/index.js
|
|
||||||
index 16ed6be8be8f555cc99096c2ff60954b42dc313d..d009c069770d066ad0db7ad02de1ea473a29334e 100644
|
|
||||||
--- a/lib/index.js
|
|
||||||
+++ b/lib/index.js
|
|
||||||
@@ -99,7 +99,7 @@ function lodash(_ref) {
|
|
||||||
|
|
||||||
var node = _ref3;
|
|
||||||
|
|
||||||
- if ((0, _types.isModuleDeclaration)(node)) {
|
|
||||||
+ if ((0, _types.isImportDeclaration)(node) || (0, _types.isExportDeclaration)(node)) {
|
|
||||||
isModule = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
@@ -538,7 +538,7 @@ and provided thanks to the work of the following contributors:
|
|||||||
* [Drew Schuster](mailto:dtschust@gmail.com)
|
* [Drew Schuster](mailto:dtschust@gmail.com)
|
||||||
* [Dryusdan](mailto:dryusdan@dryusdan.fr)
|
* [Dryusdan](mailto:dryusdan@dryusdan.fr)
|
||||||
* [Eai](mailto:eai@mizle.net)
|
* [Eai](mailto:eai@mizle.net)
|
||||||
* [Eashwar Ranganathan](mailto:eranganathan@lyft.com)
|
* [Eashwar Ranganathan](mailto:eashwar@eashwar.com)
|
||||||
* [Ed Knutson](mailto:knutsoned@gmail.com)
|
* [Ed Knutson](mailto:knutsoned@gmail.com)
|
||||||
* [Elizabeth Martín Campos](mailto:me@elizabeth.sh)
|
* [Elizabeth Martín Campos](mailto:me@elizabeth.sh)
|
||||||
* [Elizabeth Myers](mailto:elizabeth@interlinked.me)
|
* [Elizabeth Myers](mailto:elizabeth@interlinked.me)
|
||||||
|
|||||||
113
CHANGELOG.md
113
CHANGELOG.md
@@ -2,42 +2,117 @@
|
|||||||
|
|
||||||
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.5.0] - UNRELEASED
|
## [4.5.3] - 2025-12-08
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fix inconsistent error handling leaking information on existence of private posts ([GHSA-gwhw-gcjx-72v8](https://github.com/mastodon/mastodon/security/advisories/GHSA-gwhw-gcjx-72v8))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix “Delete and Redraft” on a non-quote being treated as a quote post in some cases (#37140 by @ClearlyClaire)
|
||||||
|
- Fix YouTube embeds by sending referer (#37126 by @ChaosExAnima)
|
||||||
|
- Fix streamed quoted polls not being hydrated correctly (#37118 by @ClearlyClaire)
|
||||||
|
- Fix creation of duplicate conversations (#37108 by @oneiros)
|
||||||
|
- Fix extraneous `noreferrer` in external links (#37107 by @ChaosExAnima)
|
||||||
|
- Fix edge case error handling in some database migrations (#37079 by @ClearlyClaire)
|
||||||
|
- Fix error handling when re-fetching already-known statuses (#37077 by @ClearlyClaire)
|
||||||
|
- Fix post navigation in single-column mode when Advanced UI is enabled (#37044 by @diondiondion)
|
||||||
|
- Fix `tootctl status remove` removing quoted posts and remote quotes of local posts (#37009 by @ClearlyClaire)
|
||||||
|
- Fix known expensive S3 batch delete operation failing because of short timeouts (#37004 by @ClearlyClaire)
|
||||||
|
- Fix compose autosuggest always lowercasing input token (#36995 by @ClearlyClaire)
|
||||||
|
|
||||||
|
## [4.5.2] - 2025-11-20
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change private quote education modal to not show up on self-quotes (#36926 by @ClearlyClaire)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix missing fallback link in CW-only quote posts (#36963 by @ClearlyClaire)
|
||||||
|
- Fix statuses without text being hidden while loading (#36962 by @ClearlyClaire)
|
||||||
|
- Fix `g` + `h` keyboard shortcut not working when a post is focused (#36935 by @diondiondion)
|
||||||
|
- Fix quoting overwriting current content warning (#36934 by @ClearlyClaire)
|
||||||
|
- Fix scroll-to-status in threaded view being unreliable (#36927 by @ClearlyClaire)
|
||||||
|
- Fix path resolution for emoji worker (#36897 by @ChaosExAnima)
|
||||||
|
- Fix `tootctl upgrade storage-schema` failing with `ArgumentError` (#36914 by @shugo)
|
||||||
|
- Fix cross-origin handling of CSS modules (#36890 by @ClearlyClaire)
|
||||||
|
- Fix error with remote tags including percent signs (#36886 and #36925 by @ChaosExAnima and @ClearlyClaire)
|
||||||
|
- Fix bogus quote approval policy not always being replaced correctly (#36885 by @ClearlyClaire)
|
||||||
|
- Fix hashtag completion not being inserted correctly (#36884 by @ClearlyClaire)
|
||||||
|
- Fix Cmd/Ctrl + Enter in the composer triggering confirmation dialog action (#36870 by @diondiondion)
|
||||||
|
|
||||||
|
## [4.5.1] - 2025-11-13
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix Cmd/Ctrl + Enter not submitting Alt text modal on some browsers (#36866 by @diondiondion)
|
||||||
|
- Fix posts coming from public/hashtag streaming being marked as unquotable (#36860 and #36869 by @ClearlyClaire)
|
||||||
|
- Fix old previously-undiscovered posts being treated as new when receiving an `Update` (#36848 by @ClearlyClaire)
|
||||||
|
- Fix blank screen in browsers that don't support `Intl.DisplayNames` (#36847 by @diondiondion)
|
||||||
|
- Fix filters not being applied to quotes in detailed view (#36843 by @ClearlyClaire)
|
||||||
|
- Fix scroll shift caused by fetch-all-replies alerts (#36807 by @diondiondion)
|
||||||
|
- Fix dropdown menu not focusing first item when opened via keyboard (#36804 by @diondiondion)
|
||||||
|
- Fix assets build issue on arch64 (#36781 by @ClearlyClaire)
|
||||||
|
- Fix `/api/v1/statuses/:id/context` sometimes returing `Mastodon-Async-Refresh` without `result_count` (#36779 by @ClearlyClaire)
|
||||||
|
- Fix prepared quote not being discarded with contents when replying (#36778 by @ClearlyClaire)
|
||||||
|
|
||||||
|
## [4.5.0] - 2025-11-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- **Add support for allowing and authoring quotes** (#35355, #35578, #35614, #35618, #35624, #35626, #35652, #35629, #35665, #35653, #35670, #35677, #35690, #35697, #35689, #35699, #35700, #35701, #35709, #35714, #35713, #35715, #35725, #35749, #35769, #35780, #35762, #35804, #35808, #35805, #35819, #35824, #35828, #35822, #35835, #35865, #35860, #35832, #35891, #35894, #35895, #35820, #35917, #35924, #35925, #35914, #35930, #35941, #35939, #35948, #35955, #35967, #35990, #35991, #35975, #35971, #36002, #35986, #36031, #36034, #36038, #36054, #36052, #36055, #36065, #36068, #36083, #36087, #36080, #36091, #36090, #36118, #36119, #36128, #36094, #36129, #36138, #36132, #36151, #36158, #36171, #36194, #36220, #36169, #36130, #36249, #36153, #36299, #36291, #36301, #36315, #36317, #36364, #36383, #36381, #36459, #36464, and #36461 by @ChaosExAnima, @ClearlyClaire, @Lycolia, @diondiondion, and @tribela)\
|
- **Add support for allowing and authoring quotes** (#35355, #35578, #35614, #35618, #35624, #35626, #35652, #35629, #35665, #35653, #35670, #35677, #35690, #35697, #35689, #35699, #35700, #35701, #35709, #35714, #35713, #35715, #35725, #35749, #35769, #35780, #35762, #35804, #35808, #35805, #35819, #35824, #35828, #35822, #35835, #35865, #35860, #35832, #35891, #35894, #35895, #35820, #35917, #35924, #35925, #35914, #35930, #35941, #35939, #35948, #35955, #35967, #35990, #35991, #35975, #35971, #36002, #35986, #36031, #36034, #36038, #36054, #36052, #36055, #36065, #36068, #36083, #36087, #36080, #36091, #36090, #36118, #36119, #36128, #36094, #36129, #36138, #36132, #36151, #36158, #36171, #36194, #36220, #36169, #36130, #36249, #36153, #36299, #36291, #36301, #36315, #36317, #36364, #36383, #36381, #36459, #36464, #36461, #36516, #36528, #36549, #36550, #36559, #36693, #36704, #36690, #36689, #36696, #36721, #36695 and #36736 by @ChaosExAnima, @ClearlyClaire, @Lycolia, @diondiondion, and @tribela)\
|
||||||
This includes a revamp of the composer interface.\
|
This includes a revamp of the composer interface.\
|
||||||
See https://blog.joinmastodon.org/2025/09/introducing-quote-posts/ for a user-centric overview of the feature, and https://docs.joinmastodon.org/client/quotes/ for API documentation.
|
See https://blog.joinmastodon.org/2025/09/introducing-quote-posts/ for a user-centric overview of the feature, and https://docs.joinmastodon.org/client/quotes/ for API documentation.
|
||||||
- **Add support for fetching and refreshing replies to the web UI** (#35210, #35496, #35575, #35500, #35577, #35602, #35603, #35654, #36141, #36237, #36172, #36256, #36271, #36334, #36382, and #36239 by @ClearlyClaire, @Gargron, and @diondiondion)
|
- **Add support for fetching and refreshing replies to the web UI** (#35210, #35496, #35575, #35500, #35577, #35602, #35603, #35654, #36141, #36237, #36172, #36256, #36271, #36334, #36382, #36239, #36484, #36481, #36583, #36627 and #36547 by @ClearlyClaire, @diondiondion, @Gargron and @renchap)
|
||||||
- **Add ability to block words in usernames** (#35407, #35655, and #35806 by @ClearlyClaire and @Gargron)
|
- **Add ability to block words in usernames** (#35407, #35655, and #35806 by @ClearlyClaire and @Gargron)
|
||||||
|
- Add ability to individually disable local or remote feeds for visitors or logged-in users `disabled` value to server setting for live and topic feeds, as well as user permission to bypass that (#36338, #36467, #36497, #36563, #36577, #36585, #36607 and #36703 by @ClearlyClaire)\
|
||||||
|
This splits the `timeline_preview` setting into four more granular settings controlling live feeds and topic (hashtag, trending link) feeds.\
|
||||||
|
The setting for local topic feeds has 2 values: `public` and `authenticated`. Every other setting has 3 values: `public`, `authenticated`, `disabled`.\
|
||||||
|
When `disabled`, users with the “View live and topic feeds” will still be able to view them.
|
||||||
|
- Add support for displaying of quote posts in Moderator UI (#35964 by @ThisIsMissEm)
|
||||||
- Add support for displaying link previews for Admin UI (#35958 by @ThisIsMissEm)
|
- Add support for displaying link previews for Admin UI (#35958 by @ThisIsMissEm)
|
||||||
|
- Add a new server setting to choose the server landing page (#36588 and #36602 by @ClearlyClaire and @renchap)
|
||||||
|
- Add support for `Update` activities on converted object types (#36322 by @ClearlyClaire)
|
||||||
- Add support for dynamic viewport height (#36272 by @e1berd)
|
- Add support for dynamic viewport height (#36272 by @e1berd)
|
||||||
- Add support for numeric-based URIs for new local accounts (#32724, #36304, #36316, and #36365 by @ClearlyClaire)
|
- Add support for numeric-based URIs for new local accounts (#32724, #36304, #36316, and #36365 by @ClearlyClaire)
|
||||||
|
- Add default visualizer for audio upload without poster (#36734 by @ChaosExAnima)
|
||||||
- Add Traditional Mongolian to posting languages (#36196 by @shimon1024)
|
- Add Traditional Mongolian to posting languages (#36196 by @shimon1024)
|
||||||
- Add example post with manual quote approval policy to `dev:populate_sample_data` (#36099 by @ClearlyClaire)
|
- Add example post with manual quote approval policy to `dev:populate_sample_data` (#36099 by @ClearlyClaire)
|
||||||
- Add server-side support for handling posts with a quote policy allowing followers to quote (#36093 and #36127 by @ClearlyClaire)
|
- Add server-side support for handling posts with a quote policy allowing followers to quote (#36093 and #36127 by @ClearlyClaire)
|
||||||
- Add schema.org markup to SEO-enabled posts (#36075 by @Gargron)
|
- Add schema.org markup to SEO-enabled posts (#36075 by @Gargron)
|
||||||
- Add migration to fill unset default quote policy based on default post privacy (#36041 by @ClearlyClaire)
|
- Add migration to fill unset default quote policy based on default post privacy (#36041 by @ClearlyClaire)
|
||||||
|
- Add “Posting defaults” setting page, moving existing settings from “Other” (#35896, #36033, #35966, #35969, and #36084 by @ClearlyClaire and @diondiondion)
|
||||||
|
- Added emoji from Twemoji v16 (#36501 and #36530 by @ChaosExAnima)
|
||||||
|
- Add feature to select custom emoji rendering (#35229, #35282, #35253, #35424, #35473, #35483, #35505, #35568, #35605, #35659, #35664, #35739, #35985, #36051, #36071, #36137, #36165, #36248, #36262, #36275, #36293, #36341, #36342, #36366, #36377, #36378, #36385, #36393, #36397, #36403, #36413, #36410, #36454, #36402, #36503, #36502, #36532, #36603, #36409, #36638 and #36750 by @ChaosExAnima, @ClearlyClaire and @braddunbar)\
|
||||||
|
This also completely reworks the processing and rendering of emojis and server-rendered HTML in statuses and other places.
|
||||||
- Add support for exposing conversation context for new public conversations according to FEP-7888 (#35959 and #36064 by @ClearlyClaire and @jesseplusplus)
|
- Add support for exposing conversation context for new public conversations according to FEP-7888 (#35959 and #36064 by @ClearlyClaire and @jesseplusplus)
|
||||||
- Add digest re-check before removing followers in synchronization mechanism (#34273 by @ClearlyClaire)
|
- Add digest re-check before removing followers in synchronization mechanism (#34273 by @ClearlyClaire)
|
||||||
- Add “Posting defaults” setting page, moving existing settings from “Other” (#35896, #36033, #35966, #35969, and #36084 by @ClearlyClaire and @diondiondion)
|
|
||||||
- Add support for displaying Valkey version on admin dashboard (#35785 by @ykzts)
|
- Add support for displaying Valkey version on admin dashboard (#35785 by @ykzts)
|
||||||
- Add delivery failure tracking and handling to FASP jobs (#35625, #35628, and #35723 by @oneiros)
|
- Add delivery failure tracking and handling to FASP jobs (#35625, #35628, and #35723 by @oneiros)
|
||||||
- Add example of quote post with a preview card to development sample data (#35616 by @ClearlyClaire)
|
- Add example of quote post with a preview card to development sample data (#35616 by @ClearlyClaire)
|
||||||
- Add second set of blocked text that applies to accounts regardless of account age for spam-blocking (#35563 by @ClearlyClaire)
|
- Add second set of blocked text that applies to accounts regardless of account age for spam-blocking (#35563 by @ClearlyClaire)
|
||||||
- Add experimental feature to select custom emoji rendering (#35229, #35282, #35253, #35424, #35473, #35483, #35505, #35568, #35605, #35659, #35664, #35739, #35985, #36051, #36071, #36137, #36165, #36248, #36262, #36275, #36293, #36341, #36342, #36366, #36377, #36378, #36385, #36393, #36397, #36403, #36413, #36410, #36454, and #36402 by @ChaosExAnima and @braddunbar)\
|
|
||||||
This also completely reworks the processing and rendering of emojis and server-rendered HTML in statuses and other places.
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Change confirmation dialogs for follow button actions “unfollow”, “unblock”, and “withdraw request” (#36289 by @diondiondion)
|
- Change confirmation dialogs for follow button actions “unfollow”, “unblock”, and “withdraw request” (#36289 by @diondiondion)
|
||||||
- Change “Follow” button labels (#36264 by @diondiondion)
|
- Change “Follow” button labels (#36264 by @diondiondion)
|
||||||
|
- Change appearance settings to introduce new Advanced settings section (#36496 and #36506 by @diondiondion)
|
||||||
|
- Change display of blocked and muted quoted users (#36619 by @ClearlyClaire)\
|
||||||
|
This adds `blocked_account`, `blocked_domain` and `muted_account` values to the `state` attribute of `Quote` and `ShallowQuote` REST API entities.
|
||||||
|
- Change submitting an empty post to show an error rather than failing silently (#36650 by @diondiondion)
|
||||||
|
- Change "Privacy and reach" settings from "Public profile" to their own top-level category (#27294 by @ChaelCodes)
|
||||||
|
- Change number of times quote verification is retried to better deal with temporary failures (#36698 by @ClearlyClaire)
|
||||||
- Change display of content warnings in Admin UI (#35935 by @ThisIsMissEm)
|
- Change display of content warnings in Admin UI (#35935 by @ThisIsMissEm)
|
||||||
|
- Change styling of column banners (#36531 by @ClearlyClaire)
|
||||||
|
- Change recommended Node version to 24 (LTS) (#36539 by @renchap)
|
||||||
|
- Change min. characters required for logged-out account search from 5 to 3 (#36487 by @Gargron)
|
||||||
|
- Change browser target to Vite legacy plugin defaults (#36611 by @larouxn)
|
||||||
- Change index on `follows` table to improve performance of some queries (#36374 by @ClearlyClaire)
|
- Change index on `follows` table to improve performance of some queries (#36374 by @ClearlyClaire)
|
||||||
- Change links to accounts in settings and moderation views to link to local view unless account is suspended (#36340 by @diondiondion)
|
- Change links to accounts in settings and moderation views to link to local view unless account is suspended (#36340 by @diondiondion)
|
||||||
- Change redirection for denied registration from web app to sign-in page with error message (#36384 by @ClearlyClaire)
|
- Change redirection for denied registration from web app to sign-in page with error message (#36384 by @ClearlyClaire)
|
||||||
- Change `timeline_preview` setting into four more granular settings (#36338 and #36467 by @ClearlyClaire)
|
- Change support for RFC9421 HTTP signatures to be enabled unconditionally (#36610 by @oneiros)
|
||||||
- Change wording and design of interaction dialog to simplify it (#36124 by @diondiondion)
|
- Change wording and design of interaction dialog to simplify it (#36124 by @diondiondion)
|
||||||
- Change dropdown menus to allow disabled items to be focused (#36078 by @diondiondion)
|
- Change dropdown menus to allow disabled items to be focused (#36078 by @diondiondion)
|
||||||
- Change modal background colours in light mode (#36069 by @diondiondion)
|
- Change modal background colours in light mode (#36069 by @diondiondion)
|
||||||
@@ -45,15 +120,27 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Change description of “Quiet public” (#36032 by @ClearlyClaire)
|
- Change description of “Quiet public” (#36032 by @ClearlyClaire)
|
||||||
- Change “Boost with original visibility” to “Share again with your followers” (#36035 by @ClearlyClaire)
|
- Change “Boost with original visibility” to “Share again with your followers” (#36035 by @ClearlyClaire)
|
||||||
- Change handling of push subscriptions to automatically delete invalid ones on delivery (#35987 by @ThisIsMissEm)
|
- Change handling of push subscriptions to automatically delete invalid ones on delivery (#35987 by @ThisIsMissEm)
|
||||||
- Change design of quote posts in web UI (#35584 and #35834 by @ClearlyClaire and @Gargron)
|
- Change design of quote posts in web UI (#35584 and #35834 by @Gargron)
|
||||||
- Change auditable accounts to be sorted by username in admin action logs interface (#35272 by @breadtk)
|
- Change auditable accounts to be sorted by username in admin action logs interface (#35272 by @breadtk)
|
||||||
- Change order of translation restoration and service credit on post card (#33619 by @colindean)
|
- Change order of translation restoration and service credit on post card (#33619 by @colindean)
|
||||||
- Change position of ‘add more’ to be inside table toolbar on reports (#35963 by @ThisIsMissEm)
|
- Change position of ‘add more’ to be inside table toolbar on reports (#35963 by @ThisIsMissEm)
|
||||||
|
- Change docker-compose.yml sidekiq health check to work for both 4.4 and 4.5 (#36498 by @ClearlyClaire)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- Fix relationship not being fetched to evaluate whether to show a quote post (#36517 by @ClearlyClaire)
|
||||||
- Fix rendering of poll options in status history modal (#35633 by @ThisIsMissEm)
|
- Fix rendering of poll options in status history modal (#35633 by @ThisIsMissEm)
|
||||||
- Fix “mute” button being displayed to unauthenticated visitors in hashtag dropdown (#36353 by @mkljczk)
|
- Fix “mute” button being displayed to unauthenticated visitors in hashtag dropdown (#36353 by @mkljczk)
|
||||||
|
- Fix initially selected language in Rules panel, hide selector when no alternative translations exist (#36672 by @diondiondion)
|
||||||
|
- Fix URL comparison for mentions in case of empty path (#36613 and #36626 by @ClearlyClaire)
|
||||||
|
- Fix hashtags not being picked up when full-width hash sign is used (#36103 and #36625 by @ClearlyClaire and @Gargron)
|
||||||
|
- Fix layout of severed relationships when purged events are listed (#36593 by @mejofi)
|
||||||
|
- Fix Skeleton placeholders being animated when setting to reduce animations is enabled (#36716 by @ClearlyClaire)
|
||||||
|
- Fix vacuum tasks being interrupted by a single batch failure (#36606 by @Gargron)
|
||||||
|
- Fix handling of unreachable network error for search services (#36587 by @mjankowski)
|
||||||
|
- Fix bookmarks export when a bookmarked status is soft-deleted (#36576 by @ClearlyClaire)
|
||||||
|
- Fix text overflow alignment for long author names in News (#36562 by @diondiondion)
|
||||||
|
- Fix discovery preamble missing word in admin settings (#36560 by @belatedly)
|
||||||
- Fix overflow handling of `.more-from-author` (#36310 by @edent)
|
- Fix overflow handling of `.more-from-author` (#36310 by @edent)
|
||||||
- Fix unfortunate action button wrapping in admin area (#36247 by @diondiondion)
|
- Fix unfortunate action button wrapping in admin area (#36247 by @diondiondion)
|
||||||
- Fix translate button width in Safari (#36164 and #36216 by @diondiondion)
|
- Fix translate button width in Safari (#36164 and #36216 by @diondiondion)
|
||||||
@@ -76,6 +163,16 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Fix glitchy status keyboard navigation (#35455 and #35504 by @diondiondion)
|
- Fix glitchy status keyboard navigation (#35455 and #35504 by @diondiondion)
|
||||||
- Fix post being submitted when pressing “Enter” in the CW field (#35445 by @diondiondion)
|
- Fix post being submitted when pressing “Enter” in the CW field (#35445 by @diondiondion)
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Remove support for PostgreSQL 13 (#36540 by @renchap)
|
||||||
|
|
||||||
|
## [4.4.8] - 2025-10-21
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fix quote control bypass ([GHSA-8h43-rcqj-wpc6](https://github.com/mastodon/mastodon/security/advisories/GHSA-8h43-rcqj-wpc6))
|
||||||
|
|
||||||
## [4.4.7] - 2025-10-15
|
## [4.4.7] - 2025-10-15
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
12
Dockerfile
12
Dockerfile
@@ -14,9 +14,9 @@ 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.7"
|
ARG RUBY_VERSION="3.4.7"
|
||||||
# # 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="22"]
|
||||||
# renovate: datasource=node-version depName=node
|
# renovate: datasource=node-version depName=node
|
||||||
ARG NODE_MAJOR_VERSION="22"
|
ARG NODE_MAJOR_VERSION="24"
|
||||||
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="trixie"]
|
# Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="trixie"]
|
||||||
ARG DEBIAN_VERSION="trixie"
|
ARG DEBIAN_VERSION="trixie"
|
||||||
# Node.js image to use for base image based on combined variables (ex: 20-trixie-slim)
|
# Node.js image to use for base image based on combined variables (ex: 20-trixie-slim)
|
||||||
@@ -183,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.2
|
ARG VIPS_VERSION=8.17.3
|
||||||
# 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
|
||||||
|
|
||||||
@@ -208,12 +208,12 @@ FROM build AS ffmpeg
|
|||||||
# renovate: datasource=repology depName=ffmpeg packageName=openpkg_current/ffmpeg
|
# renovate: datasource=repology depName=ffmpeg packageName=openpkg_current/ffmpeg
|
||||||
ARG FFMPEG_VERSION=8.0
|
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://github.com/FFmpeg/FFmpeg/archive/refs/tags
|
||||||
|
|
||||||
WORKDIR /usr/local/ffmpeg/src
|
WORKDIR /usr/local/ffmpeg/src
|
||||||
# Download and extract ffmpeg source code
|
# Download and extract ffmpeg source code
|
||||||
ADD ${FFMPEG_URL}/ffmpeg-${FFMPEG_VERSION}.tar.xz /usr/local/ffmpeg/src/
|
ADD ${FFMPEG_URL}/n${FFMPEG_VERSION}.tar.gz /usr/local/ffmpeg/src/
|
||||||
RUN tar xf ffmpeg-${FFMPEG_VERSION}.tar.xz;
|
RUN tar xf n${FFMPEG_VERSION}.tar.gz && mv FFmpeg-n${FFMPEG_VERSION} ffmpeg-${FFMPEG_VERSION};
|
||||||
|
|
||||||
WORKDIR /usr/local/ffmpeg/src/ffmpeg-${FFMPEG_VERSION}
|
WORKDIR /usr/local/ffmpeg/src/ffmpeg-${FFMPEG_VERSION}
|
||||||
|
|
||||||
|
|||||||
36
Gemfile
36
Gemfile
@@ -13,7 +13,7 @@ gem 'haml-rails', '~>3.0'
|
|||||||
gem 'pg', '~> 1.5'
|
gem 'pg', '~> 1.5'
|
||||||
gem 'pghero'
|
gem 'pghero'
|
||||||
|
|
||||||
gem 'aws-sdk-core', '< 3.216.0', require: false # TODO: https://github.com/mastodon/mastodon/pull/34173#issuecomment-2733378873
|
gem 'aws-sdk-core', require: false
|
||||||
gem 'aws-sdk-s3', '~> 1.123', require: false
|
gem 'aws-sdk-s3', '~> 1.123', require: false
|
||||||
gem 'blurhash', '~> 0.1'
|
gem 'blurhash', '~> 0.1'
|
||||||
gem 'fog-core', '<= 2.6.0'
|
gem 'fog-core', '<= 2.6.0'
|
||||||
@@ -24,7 +24,7 @@ gem 'ruby-vips', '~> 2.2', require: false
|
|||||||
|
|
||||||
gem 'active_model_serializers', '~> 0.10'
|
gem 'active_model_serializers', '~> 0.10'
|
||||||
gem 'addressable', '~> 2.8'
|
gem 'addressable', '~> 2.8'
|
||||||
gem 'bootsnap', '~> 1.18.0', require: false
|
gem 'bootsnap', '~> 1.19.0', require: false
|
||||||
gem 'browser'
|
gem 'browser'
|
||||||
gem 'charlock_holmes', '~> 0.7.7'
|
gem 'charlock_holmes', '~> 0.7.7'
|
||||||
gem 'chewy', '~> 7.3'
|
gem 'chewy', '~> 7.3'
|
||||||
@@ -40,7 +40,7 @@ gem 'net-ldap', '~> 0.18'
|
|||||||
gem 'omniauth', '~> 2.0'
|
gem 'omniauth', '~> 2.0'
|
||||||
gem 'omniauth-cas', '~> 3.0.0.beta.1'
|
gem 'omniauth-cas', '~> 3.0.0.beta.1'
|
||||||
gem 'omniauth_openid_connect', '~> 0.8.0'
|
gem 'omniauth_openid_connect', '~> 0.8.0'
|
||||||
gem 'omniauth-rails_csrf_protection', '~> 1.0'
|
gem 'omniauth-rails_csrf_protection', '~> 2.0'
|
||||||
gem 'omniauth-saml', '~> 2.0'
|
gem 'omniauth-saml', '~> 2.0'
|
||||||
|
|
||||||
gem 'color_diff', '~> 0.1'
|
gem 'color_diff', '~> 0.1'
|
||||||
@@ -71,7 +71,7 @@ gem 'oj', '~> 3.14'
|
|||||||
gem 'ox', '~> 2.14'
|
gem 'ox', '~> 2.14'
|
||||||
gem 'parslet'
|
gem 'parslet'
|
||||||
gem 'premailer-rails'
|
gem 'premailer-rails'
|
||||||
gem 'public_suffix', '~> 6.0'
|
gem 'public_suffix', '~> 7.0'
|
||||||
gem 'pundit', '~> 2.3'
|
gem 'pundit', '~> 2.3'
|
||||||
gem 'rack-attack', '~> 6.6'
|
gem 'rack-attack', '~> 6.6'
|
||||||
gem 'rack-cors', require: 'rack/cors'
|
gem 'rack-cors', require: 'rack/cors'
|
||||||
@@ -106,19 +106,19 @@ gem 'opentelemetry-api', '~> 1.7.0'
|
|||||||
|
|
||||||
group :opentelemetry do
|
group :opentelemetry do
|
||||||
gem 'opentelemetry-exporter-otlp', '~> 0.31.0', require: false
|
gem 'opentelemetry-exporter-otlp', '~> 0.31.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-active_job', '~> 0.9.0', require: false
|
gem 'opentelemetry-instrumentation-active_job', '~> 0.10.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.23.0', require: false
|
gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.24.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.23.0', require: false
|
gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.24.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-excon', '~> 0.25.0', require: false
|
gem 'opentelemetry-instrumentation-excon', '~> 0.26.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-faraday', '~> 0.29.0', require: false
|
gem 'opentelemetry-instrumentation-faraday', '~> 0.30.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-http', '~> 0.26.0', require: false
|
gem 'opentelemetry-instrumentation-http', '~> 0.27.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-http_client', '~> 0.25.0', require: false
|
gem 'opentelemetry-instrumentation-http_client', '~> 0.26.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-net_http', '~> 0.25.0', require: false
|
gem 'opentelemetry-instrumentation-net_http', '~> 0.26.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-pg', '~> 0.31.0', require: false
|
gem 'opentelemetry-instrumentation-pg', '~> 0.34.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-rack', '~> 0.28.0', require: false
|
gem 'opentelemetry-instrumentation-rack', '~> 0.29.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-rails', '~> 0.38.0', require: false
|
gem 'opentelemetry-instrumentation-rails', '~> 0.39.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-redis', '~> 0.27.0', require: false
|
gem 'opentelemetry-instrumentation-redis', '~> 0.28.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.27.0', require: false
|
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.28.0', require: false
|
||||||
gem 'opentelemetry-sdk', '~> 1.4', require: false
|
gem 'opentelemetry-sdk', '~> 1.4', require: false
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -138,7 +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
|
gem 'playwright-ruby-client', '1.56.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'
|
||||||
|
|||||||
289
Gemfile.lock
289
Gemfile.lock
@@ -86,27 +86,30 @@ GEM
|
|||||||
securerandom (>= 0.3)
|
securerandom (>= 0.3)
|
||||||
tzinfo (~> 2.0, >= 2.0.5)
|
tzinfo (~> 2.0, >= 2.0.5)
|
||||||
uri (>= 0.13.1)
|
uri (>= 0.13.1)
|
||||||
addressable (2.8.7)
|
addressable (2.8.8)
|
||||||
public_suffix (>= 2.0.2, < 7.0)
|
public_suffix (>= 2.0.2, < 8.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.19.0)
|
annotaterb (4.20.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.4.0)
|
aws-eventstream (1.4.0)
|
||||||
aws-partitions (1.1168.0)
|
aws-partitions (1.1190.0)
|
||||||
aws-sdk-core (3.215.1)
|
aws-sdk-core (3.239.2)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.992.0)
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
aws-sigv4 (~> 1.9)
|
aws-sigv4 (~> 1.9)
|
||||||
|
base64
|
||||||
|
bigdecimal
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.96.0)
|
logger
|
||||||
aws-sdk-core (~> 3, >= 3.210.0)
|
aws-sdk-kms (1.118.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.239.1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-s3 (1.177.0)
|
aws-sdk-s3 (1.206.0)
|
||||||
aws-sdk-core (~> 3, >= 3.210.0)
|
aws-sdk-core (~> 3, >= 3.234.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sigv4 (1.12.1)
|
aws-sigv4 (1.12.1)
|
||||||
@@ -116,7 +119,7 @@ GEM
|
|||||||
base64 (0.3.0)
|
base64 (0.3.0)
|
||||||
bcp47_spec (0.2.1)
|
bcp47_spec (0.2.1)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.20)
|
||||||
benchmark (0.4.1)
|
benchmark (0.5.0)
|
||||||
better_errors (2.10.1)
|
better_errors (2.10.1)
|
||||||
erubi (>= 1.0.0)
|
erubi (>= 1.0.0)
|
||||||
rack (>= 0.9.0)
|
rack (>= 0.9.0)
|
||||||
@@ -126,14 +129,14 @@ GEM
|
|||||||
binding_of_caller (1.0.1)
|
binding_of_caller (1.0.1)
|
||||||
debug_inspector (>= 1.2.0)
|
debug_inspector (>= 1.2.0)
|
||||||
blurhash (0.1.8)
|
blurhash (0.1.8)
|
||||||
bootsnap (1.18.6)
|
bootsnap (1.19.0)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
brakeman (7.0.2)
|
brakeman (7.1.1)
|
||||||
racc
|
racc
|
||||||
browser (6.2.0)
|
browser (6.2.0)
|
||||||
builder (3.3.0)
|
builder (3.3.0)
|
||||||
bundler-audit (0.9.2)
|
bundler-audit (0.9.3)
|
||||||
bundler (>= 1.2.0, < 3)
|
bundler (>= 1.2.0)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
capybara (3.40.0)
|
capybara (3.40.0)
|
||||||
addressable
|
addressable
|
||||||
@@ -164,11 +167,11 @@ 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.4)
|
connection_pool (2.5.5)
|
||||||
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)
|
||||||
crack (1.0.0)
|
crack (1.0.1)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
rexml
|
rexml
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
@@ -179,7 +182,7 @@ GEM
|
|||||||
activerecord (>= 5.a)
|
activerecord (>= 5.a)
|
||||||
database_cleaner-core (~> 2.0)
|
database_cleaner-core (~> 2.0)
|
||||||
database_cleaner-core (2.0.1)
|
database_cleaner-core (2.0.1)
|
||||||
date (3.4.1)
|
date (3.5.0)
|
||||||
debug (1.11.0)
|
debug (1.11.0)
|
||||||
irb (~> 1.10)
|
irb (~> 1.10)
|
||||||
reline (>= 0.3.8)
|
reline (>= 0.3.8)
|
||||||
@@ -190,10 +193,10 @@ GEM
|
|||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
responders
|
responders
|
||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
devise-two-factor (6.1.0)
|
devise-two-factor (6.2.0)
|
||||||
activesupport (>= 7.0, < 8.1)
|
activesupport (>= 7.0, < 8.2)
|
||||||
devise (~> 4.0)
|
devise (~> 4.0)
|
||||||
railties (>= 7.0, < 8.1)
|
railties (>= 7.0, < 8.2)
|
||||||
rotp (~> 6.0)
|
rotp (~> 6.0)
|
||||||
devise_pam_authenticatable2 (9.2.0)
|
devise_pam_authenticatable2 (9.2.0)
|
||||||
devise (>= 4.0.0)
|
devise (>= 4.0.0)
|
||||||
@@ -224,14 +227,14 @@ GEM
|
|||||||
mail (~> 2.7)
|
mail (~> 2.7)
|
||||||
email_validator (2.2.4)
|
email_validator (2.2.4)
|
||||||
activemodel
|
activemodel
|
||||||
erb (5.0.2)
|
erb (5.1.3)
|
||||||
erubi (1.13.1)
|
erubi (1.13.1)
|
||||||
et-orbi (1.4.0)
|
et-orbi (1.4.0)
|
||||||
tzinfo
|
tzinfo
|
||||||
excon (1.3.0)
|
excon (1.3.0)
|
||||||
logger
|
logger
|
||||||
fabrication (3.0.0)
|
fabrication (3.0.0)
|
||||||
faker (3.5.2)
|
faker (3.5.3)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (2.14.0)
|
faraday (2.14.0)
|
||||||
faraday-net_http (>= 2.0, < 3.5)
|
faraday-net_http (>= 2.0, < 3.5)
|
||||||
@@ -279,7 +282,7 @@ GEM
|
|||||||
rake (>= 13)
|
rake (>= 13)
|
||||||
googleapis-common-protos-types (1.22.0)
|
googleapis-common-protos-types (1.22.0)
|
||||||
google-protobuf (~> 4.26)
|
google-protobuf (~> 4.26)
|
||||||
haml (6.3.0)
|
haml (6.4.0)
|
||||||
temple (>= 0.8.2)
|
temple (>= 0.8.2)
|
||||||
thor
|
thor
|
||||||
tilt
|
tilt
|
||||||
@@ -288,7 +291,7 @@ GEM
|
|||||||
activesupport (>= 5.1)
|
activesupport (>= 5.1)
|
||||||
haml (>= 4.0.6)
|
haml (>= 4.0.6)
|
||||||
railties (>= 5.1)
|
railties (>= 5.1)
|
||||||
haml_lint (0.66.0)
|
haml_lint (0.67.0)
|
||||||
haml (>= 5.0)
|
haml (>= 5.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
rainbow
|
rainbow
|
||||||
@@ -301,8 +304,8 @@ GEM
|
|||||||
highline (3.1.2)
|
highline (3.1.2)
|
||||||
reline
|
reline
|
||||||
hiredis (0.6.3)
|
hiredis (0.6.3)
|
||||||
hiredis-client (0.26.1)
|
hiredis-client (0.26.2)
|
||||||
redis-client (= 0.26.1)
|
redis-client (= 0.26.2)
|
||||||
hkdf (0.3.0)
|
hkdf (0.3.0)
|
||||||
htmlentities (4.3.4)
|
htmlentities (4.3.4)
|
||||||
http (5.3.1)
|
http (5.3.1)
|
||||||
@@ -321,13 +324,14 @@ GEM
|
|||||||
rainbow (>= 2.0.0)
|
rainbow (>= 2.0.0)
|
||||||
i18n (1.14.7)
|
i18n (1.14.7)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
i18n-tasks (1.0.15)
|
i18n-tasks (1.1.2)
|
||||||
activesupport (>= 4.0.2)
|
activesupport (>= 4.0.2)
|
||||||
ast (>= 2.1.0)
|
ast (>= 2.1.0)
|
||||||
erubi
|
erubi
|
||||||
highline (>= 2.0.0)
|
highline (>= 3.0.0)
|
||||||
i18n
|
i18n
|
||||||
parser (>= 3.2.2.1)
|
parser (>= 3.2.2.1)
|
||||||
|
prism
|
||||||
rails-i18n
|
rails-i18n
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
ruby-progressbar (~> 1.8, >= 1.8.1)
|
ruby-progressbar (~> 1.8, >= 1.8.1)
|
||||||
@@ -337,7 +341,7 @@ GEM
|
|||||||
activesupport (>= 3.0)
|
activesupport (>= 3.0)
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
io-console (0.8.1)
|
io-console (0.8.1)
|
||||||
irb (1.15.2)
|
irb (1.15.3)
|
||||||
pp (>= 0.6.0)
|
pp (>= 0.6.0)
|
||||||
rdoc (>= 4.0.0)
|
rdoc (>= 4.0.0)
|
||||||
reline (>= 0.4.2)
|
reline (>= 0.4.2)
|
||||||
@@ -346,7 +350,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.15.1)
|
json (2.16.0)
|
||||||
json-canonicalization (1.0.0)
|
json-canonicalization (1.0.0)
|
||||||
json-jwt (1.17.0)
|
json-jwt (1.17.0)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 4.2)
|
||||||
@@ -426,7 +430,8 @@ GEM
|
|||||||
loofah (2.24.1)
|
loofah (2.24.1)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
mail (2.8.1)
|
mail (2.9.0)
|
||||||
|
logger
|
||||||
mini_mime (>= 0.1.1)
|
mini_mime (>= 0.1.1)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
@@ -442,7 +447,7 @@ GEM
|
|||||||
mime-types-data (3.2025.0924)
|
mime-types-data (3.2025.0924)
|
||||||
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.26.2)
|
||||||
msgpack (1.8.0)
|
msgpack (1.8.0)
|
||||||
multi_json (1.17.0)
|
multi_json (1.17.0)
|
||||||
mutex_m (0.3.0)
|
mutex_m (0.3.0)
|
||||||
@@ -464,7 +469,7 @@ GEM
|
|||||||
nokogiri (1.18.10)
|
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.13)
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.0)
|
||||||
ostruct (>= 0.2)
|
ostruct (>= 0.2)
|
||||||
omniauth (2.1.4)
|
omniauth (2.1.4)
|
||||||
@@ -476,7 +481,7 @@ GEM
|
|||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
nokogiri (~> 1.12)
|
nokogiri (~> 1.12)
|
||||||
omniauth (~> 2.1)
|
omniauth (~> 2.1)
|
||||||
omniauth-rails_csrf_protection (1.0.2)
|
omniauth-rails_csrf_protection (2.0.0)
|
||||||
actionpack (>= 4.2)
|
actionpack (>= 4.2)
|
||||||
omniauth (~> 2.0)
|
omniauth (~> 2.0)
|
||||||
omniauth-saml (2.2.4)
|
omniauth-saml (2.2.4)
|
||||||
@@ -498,74 +503,74 @@ GEM
|
|||||||
tzinfo
|
tzinfo
|
||||||
validate_url
|
validate_url
|
||||||
webfinger (~> 2.0)
|
webfinger (~> 2.0)
|
||||||
openssl (3.3.1)
|
openssl (3.3.2)
|
||||||
openssl-signature_algorithm (1.3.0)
|
openssl-signature_algorithm (1.3.0)
|
||||||
openssl (> 2.0)
|
openssl (> 2.0)
|
||||||
opentelemetry-api (1.7.0)
|
opentelemetry-api (1.7.0)
|
||||||
opentelemetry-common (0.23.0)
|
opentelemetry-common (0.23.0)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-exporter-otlp (0.31.0)
|
opentelemetry-exporter-otlp (0.31.1)
|
||||||
google-protobuf (>= 3.18)
|
google-protobuf (>= 3.18)
|
||||||
googleapis-common-protos-types (~> 1.3)
|
googleapis-common-protos-types (~> 1.3)
|
||||||
opentelemetry-api (~> 1.1)
|
opentelemetry-api (~> 1.1)
|
||||||
opentelemetry-common (~> 0.20)
|
opentelemetry-common (~> 0.20)
|
||||||
opentelemetry-sdk (~> 1.2)
|
opentelemetry-sdk (~> 1.10)
|
||||||
opentelemetry-semantic_conventions
|
opentelemetry-semantic_conventions
|
||||||
opentelemetry-helpers-sql (0.2.0)
|
opentelemetry-helpers-sql (0.3.0)
|
||||||
opentelemetry-api (~> 1.7)
|
opentelemetry-api (~> 1.7)
|
||||||
opentelemetry-helpers-sql-obfuscation (0.3.0)
|
opentelemetry-helpers-sql-processor (0.3.1)
|
||||||
opentelemetry-common (~> 0.21)
|
opentelemetry-common (~> 0.21)
|
||||||
opentelemetry-instrumentation-action_mailer (0.5.0)
|
opentelemetry-instrumentation-action_mailer (0.6.1)
|
||||||
opentelemetry-instrumentation-active_support (~> 0.7)
|
opentelemetry-instrumentation-active_support (~> 0.10)
|
||||||
opentelemetry-instrumentation-action_pack (0.14.1)
|
opentelemetry-instrumentation-action_pack (0.15.1)
|
||||||
opentelemetry-instrumentation-rack (~> 0.21)
|
opentelemetry-instrumentation-rack (~> 0.29)
|
||||||
opentelemetry-instrumentation-action_view (0.10.0)
|
opentelemetry-instrumentation-action_view (0.11.1)
|
||||||
opentelemetry-instrumentation-active_support (~> 0.7)
|
opentelemetry-instrumentation-active_support (~> 0.10)
|
||||||
opentelemetry-instrumentation-active_job (0.9.2)
|
opentelemetry-instrumentation-active_job (0.10.1)
|
||||||
opentelemetry-instrumentation-base (~> 0.24)
|
opentelemetry-instrumentation-base (~> 0.25)
|
||||||
opentelemetry-instrumentation-active_model_serializers (0.23.0)
|
opentelemetry-instrumentation-active_model_serializers (0.24.0)
|
||||||
opentelemetry-instrumentation-active_support (>= 0.7.0)
|
opentelemetry-instrumentation-active_support (>= 0.7.0)
|
||||||
opentelemetry-instrumentation-active_record (0.10.1)
|
opentelemetry-instrumentation-active_record (0.11.1)
|
||||||
opentelemetry-instrumentation-base (~> 0.24)
|
opentelemetry-instrumentation-base (~> 0.25)
|
||||||
opentelemetry-instrumentation-active_storage (0.2.0)
|
opentelemetry-instrumentation-active_storage (0.3.1)
|
||||||
opentelemetry-instrumentation-active_support (~> 0.7)
|
opentelemetry-instrumentation-active_support (~> 0.10)
|
||||||
opentelemetry-instrumentation-active_support (0.9.1)
|
opentelemetry-instrumentation-active_support (0.10.1)
|
||||||
opentelemetry-instrumentation-base (~> 0.24)
|
opentelemetry-instrumentation-base (~> 0.25)
|
||||||
opentelemetry-instrumentation-base (0.24.0)
|
opentelemetry-instrumentation-base (0.25.0)
|
||||||
opentelemetry-api (~> 1.7)
|
opentelemetry-api (~> 1.7)
|
||||||
opentelemetry-common (~> 0.21)
|
opentelemetry-common (~> 0.21)
|
||||||
opentelemetry-registry (~> 0.1)
|
opentelemetry-registry (~> 0.1)
|
||||||
opentelemetry-instrumentation-concurrent_ruby (0.23.1)
|
opentelemetry-instrumentation-concurrent_ruby (0.24.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.24)
|
opentelemetry-instrumentation-base (~> 0.25)
|
||||||
opentelemetry-instrumentation-excon (0.25.2)
|
opentelemetry-instrumentation-excon (0.26.1)
|
||||||
opentelemetry-instrumentation-base (~> 0.24)
|
opentelemetry-instrumentation-base (~> 0.25)
|
||||||
opentelemetry-instrumentation-faraday (0.29.1)
|
opentelemetry-instrumentation-faraday (0.30.1)
|
||||||
opentelemetry-instrumentation-base (~> 0.24)
|
opentelemetry-instrumentation-base (~> 0.25)
|
||||||
opentelemetry-instrumentation-http (0.26.1)
|
opentelemetry-instrumentation-http (0.27.1)
|
||||||
opentelemetry-instrumentation-base (~> 0.24)
|
opentelemetry-instrumentation-base (~> 0.25)
|
||||||
opentelemetry-instrumentation-http_client (0.25.1)
|
opentelemetry-instrumentation-http_client (0.26.1)
|
||||||
opentelemetry-instrumentation-base (~> 0.24)
|
opentelemetry-instrumentation-base (~> 0.25)
|
||||||
opentelemetry-instrumentation-net_http (0.25.1)
|
opentelemetry-instrumentation-net_http (0.26.1)
|
||||||
opentelemetry-instrumentation-base (~> 0.24)
|
opentelemetry-instrumentation-base (~> 0.25)
|
||||||
opentelemetry-instrumentation-pg (0.31.1)
|
opentelemetry-instrumentation-pg (0.34.1)
|
||||||
opentelemetry-helpers-sql
|
opentelemetry-helpers-sql
|
||||||
opentelemetry-helpers-sql-obfuscation
|
opentelemetry-helpers-sql-processor
|
||||||
opentelemetry-instrumentation-base (~> 0.24)
|
opentelemetry-instrumentation-base (~> 0.25)
|
||||||
opentelemetry-instrumentation-rack (0.28.2)
|
opentelemetry-instrumentation-rack (0.29.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.24)
|
opentelemetry-instrumentation-base (~> 0.25)
|
||||||
opentelemetry-instrumentation-rails (0.38.0)
|
opentelemetry-instrumentation-rails (0.39.1)
|
||||||
opentelemetry-instrumentation-action_mailer (~> 0.4)
|
opentelemetry-instrumentation-action_mailer (~> 0.6)
|
||||||
opentelemetry-instrumentation-action_pack (~> 0.13)
|
opentelemetry-instrumentation-action_pack (~> 0.15)
|
||||||
opentelemetry-instrumentation-action_view (~> 0.9)
|
opentelemetry-instrumentation-action_view (~> 0.11)
|
||||||
opentelemetry-instrumentation-active_job (~> 0.8)
|
opentelemetry-instrumentation-active_job (~> 0.10)
|
||||||
opentelemetry-instrumentation-active_record (~> 0.9)
|
opentelemetry-instrumentation-active_record (~> 0.11)
|
||||||
opentelemetry-instrumentation-active_storage (~> 0.1)
|
opentelemetry-instrumentation-active_storage (~> 0.3)
|
||||||
opentelemetry-instrumentation-active_support (~> 0.8)
|
opentelemetry-instrumentation-active_support (~> 0.10)
|
||||||
opentelemetry-instrumentation-concurrent_ruby (~> 0.22)
|
opentelemetry-instrumentation-concurrent_ruby (~> 0.23)
|
||||||
opentelemetry-instrumentation-redis (0.27.1)
|
opentelemetry-instrumentation-redis (0.28.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.24)
|
opentelemetry-instrumentation-base (~> 0.25)
|
||||||
opentelemetry-instrumentation-sidekiq (0.27.1)
|
opentelemetry-instrumentation-sidekiq (0.28.1)
|
||||||
opentelemetry-instrumentation-base (~> 0.24)
|
opentelemetry-instrumentation-base (~> 0.25)
|
||||||
opentelemetry-registry (0.4.0)
|
opentelemetry-registry (0.4.0)
|
||||||
opentelemetry-api (~> 1.1)
|
opentelemetry-api (~> 1.1)
|
||||||
opentelemetry-sdk (1.10.0)
|
opentelemetry-sdk (1.10.0)
|
||||||
@@ -580,7 +585,7 @@ GEM
|
|||||||
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.9.0)
|
parser (3.3.10.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
parslet (2.0.0)
|
parslet (2.0.0)
|
||||||
@@ -589,7 +594,7 @@ GEM
|
|||||||
pg (1.6.2)
|
pg (1.6.2)
|
||||||
pghero (3.7.0)
|
pghero (3.7.0)
|
||||||
activerecord (>= 7.1)
|
activerecord (>= 7.1)
|
||||||
playwright-ruby-client (1.55.0)
|
playwright-ruby-client (1.56.0)
|
||||||
concurrent-ruby (>= 1.1.6)
|
concurrent-ruby (>= 1.1.6)
|
||||||
mime-types (>= 3.0)
|
mime-types (>= 3.0)
|
||||||
pp (0.6.3)
|
pp (0.6.3)
|
||||||
@@ -603,8 +608,8 @@ GEM
|
|||||||
net-smtp
|
net-smtp
|
||||||
premailer (~> 1.7, >= 1.7.9)
|
premailer (~> 1.7, >= 1.7.9)
|
||||||
prettyprint (0.2.0)
|
prettyprint (0.2.0)
|
||||||
prism (1.5.1)
|
prism (1.6.0)
|
||||||
prometheus_exporter (2.3.0)
|
prometheus_exporter (2.3.1)
|
||||||
webrick
|
webrick
|
||||||
propshaft (1.3.1)
|
propshaft (1.3.1)
|
||||||
actionpack (>= 7.0.0)
|
actionpack (>= 7.0.0)
|
||||||
@@ -613,15 +618,15 @@ GEM
|
|||||||
psych (5.2.6)
|
psych (5.2.6)
|
||||||
date
|
date
|
||||||
stringio
|
stringio
|
||||||
public_suffix (6.0.2)
|
public_suffix (7.0.0)
|
||||||
puma (7.0.4)
|
puma (7.1.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.5.2)
|
pundit (2.5.2)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.2.3)
|
rack (3.2.4)
|
||||||
rack-attack (6.7.0)
|
rack-attack (6.8.0)
|
||||||
rack (>= 1.0, < 4)
|
rack (>= 1.0, < 4)
|
||||||
rack-cors (3.0.0)
|
rack-cors (3.0.0)
|
||||||
logger
|
logger
|
||||||
@@ -633,7 +638,7 @@ GEM
|
|||||||
faraday-follow_redirects
|
faraday-follow_redirects
|
||||||
json-jwt (>= 1.11.0)
|
json-jwt (>= 1.11.0)
|
||||||
rack (>= 2.1.0)
|
rack (>= 2.1.0)
|
||||||
rack-protection (4.1.1)
|
rack-protection (4.2.1)
|
||||||
base64 (>= 0.1.0)
|
base64 (>= 0.1.0)
|
||||||
logger (>= 1.6.0)
|
logger (>= 1.6.0)
|
||||||
rack (>= 3.0.0, < 4)
|
rack (>= 3.0.0, < 4)
|
||||||
@@ -667,7 +672,7 @@ GEM
|
|||||||
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.2)
|
rails-i18n (8.1.0)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 8.0.0, < 9)
|
railties (>= 8.0.0, < 9)
|
||||||
railties (8.0.3)
|
railties (8.0.3)
|
||||||
@@ -680,7 +685,7 @@ GEM
|
|||||||
tsort (>= 0.2)
|
tsort (>= 0.2)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.3.0)
|
rake (13.3.1)
|
||||||
rdf (3.3.4)
|
rdf (3.3.4)
|
||||||
bcp47_spec (~> 0.2)
|
bcp47_spec (~> 0.2)
|
||||||
bigdecimal (~> 3.1, >= 3.1.5)
|
bigdecimal (~> 3.1, >= 3.1.5)
|
||||||
@@ -690,7 +695,7 @@ GEM
|
|||||||
readline (~> 0.0)
|
readline (~> 0.0)
|
||||||
rdf-normalize (0.7.0)
|
rdf-normalize (0.7.0)
|
||||||
rdf (~> 3.3)
|
rdf (~> 3.3)
|
||||||
rdoc (6.15.0)
|
rdoc (6.15.1)
|
||||||
erb
|
erb
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
tsort
|
tsort
|
||||||
@@ -698,24 +703,24 @@ GEM
|
|||||||
reline
|
reline
|
||||||
redcarpet (3.6.1)
|
redcarpet (3.6.1)
|
||||||
redis (4.8.1)
|
redis (4.8.1)
|
||||||
redis-client (0.26.1)
|
redis-client (0.26.2)
|
||||||
connection_pool
|
connection_pool
|
||||||
regexp_parser (2.11.3)
|
regexp_parser (2.11.3)
|
||||||
reline (0.6.2)
|
reline (0.6.3)
|
||||||
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.2.0)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 7.0)
|
||||||
railties (>= 5.2)
|
railties (>= 7.0)
|
||||||
rexml (3.4.4)
|
rexml (3.4.4)
|
||||||
rotp (6.3.0)
|
rotp (6.3.0)
|
||||||
rouge (4.6.1)
|
rouge (4.6.1)
|
||||||
rpam2 (4.0.2)
|
rpam2 (4.0.2)
|
||||||
rqrcode (3.1.0)
|
rqrcode (3.1.1)
|
||||||
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.1)
|
||||||
rspec (3.13.1)
|
rspec (3.13.1)
|
||||||
rspec-core (~> 3.13.0)
|
rspec-core (~> 3.13.0)
|
||||||
rspec-expectations (~> 3.13.0)
|
rspec-expectations (~> 3.13.0)
|
||||||
@@ -744,7 +749,7 @@ GEM
|
|||||||
rspec-mocks (~> 3.0)
|
rspec-mocks (~> 3.0)
|
||||||
sidekiq (>= 5, < 9)
|
sidekiq (>= 5, < 9)
|
||||||
rspec-support (3.13.6)
|
rspec-support (3.13.6)
|
||||||
rubocop (1.81.1)
|
rubocop (1.81.7)
|
||||||
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)
|
||||||
@@ -755,7 +760,7 @@ GEM
|
|||||||
rubocop-ast (>= 1.47.1, < 2.0)
|
rubocop-ast (>= 1.47.1, < 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.47.1)
|
rubocop-ast (1.48.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)
|
||||||
@@ -764,20 +769,20 @@ 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.26.0)
|
rubocop-performance (1.26.1)
|
||||||
lint_roller (~> 1.1)
|
lint_roller (~> 1.1)
|
||||||
rubocop (>= 1.75.0, < 2.0)
|
rubocop (>= 1.75.0, < 2.0)
|
||||||
rubocop-ast (>= 1.44.0, < 2.0)
|
rubocop-ast (>= 1.47.1, < 2.0)
|
||||||
rubocop-rails (2.33.4)
|
rubocop-rails (2.33.4)
|
||||||
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.7.0)
|
rubocop-rspec (3.8.0)
|
||||||
lint_roller (~> 1.1)
|
lint_roller (~> 1.1)
|
||||||
rubocop (~> 1.72, >= 1.72.1)
|
rubocop (~> 1.81)
|
||||||
rubocop-rspec_rails (2.31.0)
|
rubocop-rspec_rails (2.32.0)
|
||||||
lint_roller (~> 1.1)
|
lint_roller (~> 1.1)
|
||||||
rubocop (~> 1.72, >= 1.72.1)
|
rubocop (~> 1.72, >= 1.72.1)
|
||||||
rubocop-rspec (~> 3.5)
|
rubocop-rspec (~> 3.5)
|
||||||
@@ -790,7 +795,7 @@ GEM
|
|||||||
ruby-vips (2.2.5)
|
ruby-vips (2.2.5)
|
||||||
ffi (~> 1.12)
|
ffi (~> 1.12)
|
||||||
logger
|
logger
|
||||||
rubyzip (3.1.1)
|
rubyzip (3.2.2)
|
||||||
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.5.0)
|
safety_net_attestation (0.5.0)
|
||||||
@@ -802,9 +807,9 @@ GEM
|
|||||||
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 (7.0.1)
|
||||||
activesupport (>= 5.2.0)
|
activesupport (>= 7.1)
|
||||||
sidekiq (8.0.8)
|
sidekiq (8.0.10)
|
||||||
connection_pool (>= 2.5.0)
|
connection_pool (>= 2.5.0)
|
||||||
json (>= 2.9.0)
|
json (>= 2.9.0)
|
||||||
logger (>= 1.6.2)
|
logger (>= 1.6.2)
|
||||||
@@ -821,9 +826,9 @@ GEM
|
|||||||
thor (>= 1.0, < 3.0)
|
thor (>= 1.0, < 3.0)
|
||||||
simple-navigation (4.4.0)
|
simple-navigation (4.4.0)
|
||||||
activesupport (>= 2.3.2)
|
activesupport (>= 2.3.2)
|
||||||
simple_form (5.3.1)
|
simple_form (5.4.0)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 7.0)
|
||||||
activemodel (>= 5.2)
|
activemodel (>= 7.0)
|
||||||
simplecov (0.22.0)
|
simplecov (0.22.0)
|
||||||
docile (~> 1.1)
|
docile (~> 1.1)
|
||||||
simplecov-html (~> 0.11)
|
simplecov-html (~> 0.11)
|
||||||
@@ -834,9 +839,9 @@ GEM
|
|||||||
stackprof (0.2.27)
|
stackprof (0.2.27)
|
||||||
starry (0.2.0)
|
starry (0.2.0)
|
||||||
base64
|
base64
|
||||||
stoplight (5.3.8)
|
stoplight (5.6.0)
|
||||||
zeitwerk
|
zeitwerk
|
||||||
stringio (3.1.7)
|
stringio (3.1.8)
|
||||||
strong_migrations (2.5.1)
|
strong_migrations (2.5.1)
|
||||||
activerecord (>= 7.1)
|
activerecord (>= 7.1)
|
||||||
swd (2.0.3)
|
swd (2.0.3)
|
||||||
@@ -850,7 +855,7 @@ GEM
|
|||||||
unicode-display_width (>= 1.1.1, < 4)
|
unicode-display_width (>= 1.1.1, < 4)
|
||||||
terrapin (1.1.1)
|
terrapin (1.1.1)
|
||||||
climate_control
|
climate_control
|
||||||
test-prof (1.4.4)
|
test-prof (1.5.0)
|
||||||
thor (1.4.0)
|
thor (1.4.0)
|
||||||
tilt (2.6.1)
|
tilt (2.6.1)
|
||||||
timeout (0.4.3)
|
timeout (0.4.3)
|
||||||
@@ -882,7 +887,7 @@ GEM
|
|||||||
unicode-display_width (3.2.0)
|
unicode-display_width (3.2.0)
|
||||||
unicode-emoji (~> 4.1)
|
unicode-emoji (~> 4.1)
|
||||||
unicode-emoji (4.1.0)
|
unicode-emoji (4.1.0)
|
||||||
uri (1.0.4)
|
uri (1.1.1)
|
||||||
useragent (0.16.11)
|
useragent (0.16.11)
|
||||||
validate_url (1.0.15)
|
validate_url (1.0.15)
|
||||||
activemodel (>= 3.0.0)
|
activemodel (>= 3.0.0)
|
||||||
@@ -898,7 +903,7 @@ GEM
|
|||||||
zeitwerk (~> 2.2)
|
zeitwerk (~> 2.2)
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
webauthn (3.4.2)
|
webauthn (3.4.3)
|
||||||
android_key_attestation (~> 0.3.0)
|
android_key_attestation (~> 0.3.0)
|
||||||
bindata (~> 2.4)
|
bindata (~> 2.4)
|
||||||
cbor (~> 0.5.9)
|
cbor (~> 0.5.9)
|
||||||
@@ -910,7 +915,7 @@ GEM
|
|||||||
activesupport
|
activesupport
|
||||||
faraday (~> 2.0)
|
faraday (~> 2.0)
|
||||||
faraday-follow_redirects
|
faraday-follow_redirects
|
||||||
webmock (3.25.1)
|
webmock (3.26.1)
|
||||||
addressable (>= 2.8.0)
|
addressable (>= 2.8.0)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
@@ -932,12 +937,12 @@ DEPENDENCIES
|
|||||||
active_model_serializers (~> 0.10)
|
active_model_serializers (~> 0.10)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
annotaterb (~> 4.13)
|
annotaterb (~> 4.13)
|
||||||
aws-sdk-core (< 3.216.0)
|
aws-sdk-core
|
||||||
aws-sdk-s3 (~> 1.123)
|
aws-sdk-s3 (~> 1.123)
|
||||||
better_errors (~> 2.9)
|
better_errors (~> 2.9)
|
||||||
binding_of_caller (~> 1.0)
|
binding_of_caller (~> 1.0)
|
||||||
blurhash (~> 0.1)
|
blurhash (~> 0.1)
|
||||||
bootsnap (~> 1.18.0)
|
bootsnap (~> 1.19.0)
|
||||||
brakeman (~> 7.0)
|
brakeman (~> 7.0)
|
||||||
browser
|
browser
|
||||||
bundler-audit (~> 0.9)
|
bundler-audit (~> 0.9)
|
||||||
@@ -1004,34 +1009,34 @@ DEPENDENCIES
|
|||||||
oj (~> 3.14)
|
oj (~> 3.14)
|
||||||
omniauth (~> 2.0)
|
omniauth (~> 2.0)
|
||||||
omniauth-cas (~> 3.0.0.beta.1)
|
omniauth-cas (~> 3.0.0.beta.1)
|
||||||
omniauth-rails_csrf_protection (~> 1.0)
|
omniauth-rails_csrf_protection (~> 2.0)
|
||||||
omniauth-saml (~> 2.0)
|
omniauth-saml (~> 2.0)
|
||||||
omniauth_openid_connect (~> 0.8.0)
|
omniauth_openid_connect (~> 0.8.0)
|
||||||
opentelemetry-api (~> 1.7.0)
|
opentelemetry-api (~> 1.7.0)
|
||||||
opentelemetry-exporter-otlp (~> 0.31.0)
|
opentelemetry-exporter-otlp (~> 0.31.0)
|
||||||
opentelemetry-instrumentation-active_job (~> 0.9.0)
|
opentelemetry-instrumentation-active_job (~> 0.10.0)
|
||||||
opentelemetry-instrumentation-active_model_serializers (~> 0.23.0)
|
opentelemetry-instrumentation-active_model_serializers (~> 0.24.0)
|
||||||
opentelemetry-instrumentation-concurrent_ruby (~> 0.23.0)
|
opentelemetry-instrumentation-concurrent_ruby (~> 0.24.0)
|
||||||
opentelemetry-instrumentation-excon (~> 0.25.0)
|
opentelemetry-instrumentation-excon (~> 0.26.0)
|
||||||
opentelemetry-instrumentation-faraday (~> 0.29.0)
|
opentelemetry-instrumentation-faraday (~> 0.30.0)
|
||||||
opentelemetry-instrumentation-http (~> 0.26.0)
|
opentelemetry-instrumentation-http (~> 0.27.0)
|
||||||
opentelemetry-instrumentation-http_client (~> 0.25.0)
|
opentelemetry-instrumentation-http_client (~> 0.26.0)
|
||||||
opentelemetry-instrumentation-net_http (~> 0.25.0)
|
opentelemetry-instrumentation-net_http (~> 0.26.0)
|
||||||
opentelemetry-instrumentation-pg (~> 0.31.0)
|
opentelemetry-instrumentation-pg (~> 0.34.0)
|
||||||
opentelemetry-instrumentation-rack (~> 0.28.0)
|
opentelemetry-instrumentation-rack (~> 0.29.0)
|
||||||
opentelemetry-instrumentation-rails (~> 0.38.0)
|
opentelemetry-instrumentation-rails (~> 0.39.0)
|
||||||
opentelemetry-instrumentation-redis (~> 0.27.0)
|
opentelemetry-instrumentation-redis (~> 0.28.0)
|
||||||
opentelemetry-instrumentation-sidekiq (~> 0.27.0)
|
opentelemetry-instrumentation-sidekiq (~> 0.28.0)
|
||||||
opentelemetry-sdk (~> 1.4)
|
opentelemetry-sdk (~> 1.4)
|
||||||
ox (~> 2.14)
|
ox (~> 2.14)
|
||||||
parslet
|
parslet
|
||||||
pg (~> 1.5)
|
pg (~> 1.5)
|
||||||
pghero
|
pghero
|
||||||
playwright-ruby-client (= 1.55.0)
|
playwright-ruby-client (= 1.56.0)
|
||||||
premailer-rails
|
premailer-rails
|
||||||
prometheus_exporter (~> 2.2)
|
prometheus_exporter (~> 2.2)
|
||||||
propshaft
|
propshaft
|
||||||
public_suffix (~> 6.0)
|
public_suffix (~> 7.0)
|
||||||
puma (~> 7.0)
|
puma (~> 7.0)
|
||||||
pundit (~> 2.3)
|
pundit (~> 2.3)
|
||||||
rack-attack (~> 6.6)
|
rack-attack (~> 6.6)
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ Mastodon is a **free, open-source social network server** based on [ActivityPub]
|
|||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- **Ruby** 3.2+
|
- **Ruby** 3.2+
|
||||||
- **PostgreSQL** 13+
|
- **PostgreSQL** 14+
|
||||||
- **Redis** 7.0+
|
- **Redis** 7.0+
|
||||||
- **Node.js** 20+
|
- **Node.js** 20+
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
|
|||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ------- | ---------------- |
|
| ------- | ---------------- |
|
||||||
|
| 4.5.x | Yes |
|
||||||
| 4.4.x | Yes |
|
| 4.4.x | Yes |
|
||||||
| 4.3.x | Yes |
|
| 4.3.x | Until 2026-05-06 |
|
||||||
| 4.2.x | Until 2026-01-08 |
|
| 4.2.x | Until 2026-01-08 |
|
||||||
| < 4.2 | No |
|
| < 4.2 | No |
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class ActivityPub::LikesController < ActivityPub::BaseController
|
|||||||
def set_status
|
def set_status
|
||||||
@status = @account.statuses.find(params[:status_id])
|
@status = @account.statuses.find(params[:status_id])
|
||||||
authorize @status, :show?
|
authorize @status, :show?
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class ActivityPub::QuoteAuthorizationsController < ActivityPub::BaseController
|
|||||||
before_action :set_quote_authorization
|
before_action :set_quote_authorization
|
||||||
|
|
||||||
def show
|
def show
|
||||||
expires_in 30.seconds, public: true if @quote.status.distributable? && public_fetch_mode?
|
expires_in 30.seconds, public: true if @quote.quoted_status.distributable? && public_fetch_mode?
|
||||||
render json: @quote, serializer: ActivityPub::QuoteAuthorizationSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
render json: @quote, serializer: ActivityPub::QuoteAuthorizationSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -23,8 +23,8 @@ class ActivityPub::QuoteAuthorizationsController < ActivityPub::BaseController
|
|||||||
@quote = Quote.accepted.where(quoted_account: @account).find(params[:id])
|
@quote = Quote.accepted.where(quoted_account: @account).find(params[:id])
|
||||||
return not_found unless @quote.status.present? && @quote.quoted_status.present?
|
return not_found unless @quote.status.present? && @quote.quoted_status.present?
|
||||||
|
|
||||||
authorize @quote.status, :show?
|
authorize @quote.quoted_status, :show?
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
|
|||||||
def set_status
|
def set_status
|
||||||
@status = @account.statuses.find(params[:status_id])
|
@status = @account.statuses.find(params[:status_id])
|
||||||
authorize @status, :show?
|
authorize @status, :show?
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class ActivityPub::SharesController < ActivityPub::BaseController
|
|||||||
def set_status
|
def set_status
|
||||||
@status = @account.statuses.find(params[:status_id])
|
@status = @account.statuses.find(params[:status_id])
|
||||||
authorize @status, :show?
|
authorize @status, :show?
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,15 @@ module Admin
|
|||||||
def index
|
def index
|
||||||
authorize :custom_emoji, :index?
|
authorize :custom_emoji, :index?
|
||||||
|
|
||||||
|
# If filtering by local emojis, remove by_domain filter.
|
||||||
|
params.delete(:by_domain) if params[:local].present?
|
||||||
|
|
||||||
|
# If filtering by domain, ensure remote filter is set.
|
||||||
|
if params[:by_domain].present?
|
||||||
|
params.delete(:local)
|
||||||
|
params[:remote] = '1'
|
||||||
|
end
|
||||||
|
|
||||||
@custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page])
|
@custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page])
|
||||||
@form = Form::CustomEmojiBatch.new
|
@form = Form::CustomEmojiBatch.new
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ module Admin
|
|||||||
|
|
||||||
@site_upload.destroy!
|
@site_upload.destroy!
|
||||||
|
|
||||||
redirect_back fallback_location: admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg')
|
redirect_back_or_to admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg')
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::AnnualReportsController < Api::BaseController
|
class Api::V1::AnnualReportsController < Api::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
|
include AsyncRefreshesConcern
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index
|
|
||||||
|
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, except: [:read, :generate]
|
||||||
|
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:read, :generate]
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
before_action :set_annual_report, except: :index
|
before_action :set_annual_report, only: [:show, :read]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
with_read_replica do
|
with_read_replica do
|
||||||
@@ -28,14 +30,59 @@ class Api::V1::AnnualReportsController < Api::BaseController
|
|||||||
relationships: @relationships
|
relationships: @relationships
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def state
|
||||||
|
render json: { state: report_state }
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate
|
||||||
|
return render_empty unless year == AnnualReport.current_campaign
|
||||||
|
return render_empty if GeneratedAnnualReport.exists?(account_id: current_account.id, year: year)
|
||||||
|
|
||||||
|
async_refresh = AsyncRefresh.new(refresh_key)
|
||||||
|
|
||||||
|
if async_refresh.running?
|
||||||
|
add_async_refresh_header(async_refresh, retry_seconds: 2)
|
||||||
|
return head 202
|
||||||
|
end
|
||||||
|
|
||||||
|
add_async_refresh_header(AsyncRefresh.create(refresh_key), retry_seconds: 2)
|
||||||
|
|
||||||
|
GenerateAnnualReportWorker.perform_async(current_account.id, year)
|
||||||
|
|
||||||
|
head 202
|
||||||
|
end
|
||||||
|
|
||||||
def read
|
def read
|
||||||
@annual_report.view!
|
@annual_report.view!
|
||||||
render_empty
|
render_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def refresh_key
|
||||||
|
"wrapstodon:#{current_account.id}:#{year}"
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def report_state
|
||||||
|
return 'available' if GeneratedAnnualReport.exists?(account_id: current_account.id, year: year)
|
||||||
|
|
||||||
|
async_refresh = AsyncRefresh.new(refresh_key)
|
||||||
|
|
||||||
|
if async_refresh.running?
|
||||||
|
add_async_refresh_header(async_refresh, retry_seconds: 2)
|
||||||
|
'generating'
|
||||||
|
elsif AnnualReport.current_campaign == year && AnnualReport.new(current_account, year).eligible?
|
||||||
|
'eligible'
|
||||||
|
else
|
||||||
|
'ineligible'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def year
|
||||||
|
params[:id]&.to_i
|
||||||
|
end
|
||||||
|
|
||||||
def set_annual_report
|
def set_annual_report
|
||||||
@annual_report = GeneratedAnnualReport.find_by!(account_id: current_account.id, year: params[:id])
|
@annual_report = GeneratedAnnualReport.find_by!(account_id: current_account.id, year: year)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class Api::V1::Polls::VotesController < Api::BaseController
|
|||||||
def set_poll
|
def set_poll
|
||||||
@poll = Poll.find(params[:poll_id])
|
@poll = Poll.find(params[:poll_id])
|
||||||
authorize @poll.status, :show?
|
authorize @poll.status, :show?
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class Api::V1::PollsController < Api::BaseController
|
|||||||
def set_poll
|
def set_poll
|
||||||
@poll = Poll.find(params[:id])
|
@poll = Poll.find(params[:id])
|
||||||
authorize @poll.status, :show?
|
authorize @poll.status, :show?
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class Api::V1::Statuses::BaseController < Api::BaseController
|
|||||||
def set_status
|
def set_status
|
||||||
@status = Status.find(params[:status_id])
|
@status = Status.find(params[:status_id])
|
||||||
authorize @status, :show?
|
authorize @status, :show?
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class Api::V1::Statuses::BookmarksController < Api::V1::Statuses::BaseController
|
|||||||
bookmark&.destroy!
|
bookmark&.destroy!
|
||||||
|
|
||||||
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, bookmarks_map: { @status.id => false })
|
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, bookmarks_map: { @status.id => false })
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class Api::V1::Statuses::FavouritesController < Api::V1::Statuses::BaseControlle
|
|||||||
|
|
||||||
relationships = StatusRelationshipsPresenter.new([@status], current_account.id, favourites_map: { @status.id => false }, attributes_map: { @status.id => { favourites_count: count } })
|
relationships = StatusRelationshipsPresenter.new([@status], current_account.id, favourites_map: { @status.id => false }, attributes_map: { @status.id => { favourites_count: count } })
|
||||||
render json: @status, serializer: REST::StatusSerializer, relationships: relationships
|
render json: @status, serializer: REST::StatusSerializer, relationships: relationships
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class Api::V1::Statuses::ReblogsController < Api::V1::Statuses::BaseController
|
|||||||
|
|
||||||
relationships = StatusRelationshipsPresenter.new([@status], current_account.id, reblogs_map: { @reblog.id => false }, attributes_map: { @reblog.id => { reblogs_count: count } })
|
relationships = StatusRelationshipsPresenter.new([@status], current_account.id, reblogs_map: { @reblog.id => false }, attributes_map: { @reblog.id => { reblogs_count: count } })
|
||||||
render json: @reblog, serializer: REST::StatusSerializer, relationships: relationships
|
render json: @reblog, serializer: REST::StatusSerializer, relationships: relationships
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ class Api::V1::Statuses::ReblogsController < Api::V1::Statuses::BaseController
|
|||||||
def set_reblog
|
def set_reblog
|
||||||
@reblog = Status.find(params[:status_id])
|
@reblog = Status.find(params[:status_id])
|
||||||
authorize @reblog, :show?
|
authorize @reblog, :show?
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
if async_refresh.running?
|
if async_refresh.running?
|
||||||
add_async_refresh_header(async_refresh)
|
add_async_refresh_header(async_refresh)
|
||||||
elsif !current_account.nil? && @status.should_fetch_replies?
|
elsif !current_account.nil? && @status.should_fetch_replies?
|
||||||
add_async_refresh_header(AsyncRefresh.create(refresh_key))
|
add_async_refresh_header(AsyncRefresh.create(refresh_key, count_results: true))
|
||||||
|
|
||||||
WorkerBatch.new.within do |batch|
|
WorkerBatch.new.within do |batch|
|
||||||
batch.connect(refresh_key, threshold: 1.0)
|
batch.connect(refresh_key, threshold: 1.0)
|
||||||
@@ -128,10 +128,11 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
@status = Status.where(account: current_account).find(params[:id])
|
@status = Status.where(account: current_account).find(params[:id])
|
||||||
authorize @status, :destroy?
|
authorize @status, :destroy?
|
||||||
|
|
||||||
|
json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true
|
||||||
|
|
||||||
@status.discard_with_reblogs
|
@status.discard_with_reblogs
|
||||||
StatusPin.find_by(status: @status)&.destroy
|
StatusPin.find_by(status: @status)&.destroy
|
||||||
@status.account.statuses_count = @status.account.statuses_count - 1
|
@status.account.statuses_count = @status.account.statuses_count - 1
|
||||||
json = render_to_body json: @status, serializer: REST::StatusSerializer, source_requested: true
|
|
||||||
|
|
||||||
RemovalWorker.perform_async(@status.id, { 'redraft' => !truthy_param?(:delete_media) })
|
RemovalWorker.perform_async(@status.id, { 'redraft' => !truthy_param?(:delete_media) })
|
||||||
|
|
||||||
@@ -147,7 +148,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
def set_status
|
def set_status
|
||||||
@status = Status.find(params[:id])
|
@status = Status.find(params[:id])
|
||||||
authorize @status, :show?
|
authorize @status, :show?
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -159,7 +160,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def set_quoted_status
|
def set_quoted_status
|
||||||
@quoted_status = Status.find(status_params[:quoted_status_id]) if status_params[:quoted_status_id].present?
|
@quoted_status = Status.find(status_params[:quoted_status_id])&.proper if status_params[:quoted_status_id].present?
|
||||||
authorize(@quoted_status, :quote?) if @quoted_status.present?
|
authorize(@quoted_status, :quote?) if @quoted_status.present?
|
||||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
# TODO: distinguish between non-existing and non-quotable posts
|
# TODO: distinguish between non-existing and non-quotable posts
|
||||||
|
|||||||
114
app/controllers/api/v1_alpha/collections_controller.rb
Normal file
114
app/controllers/api/v1_alpha/collections_controller.rb
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1Alpha::CollectionsController < Api::BaseController
|
||||||
|
include Authorization
|
||||||
|
|
||||||
|
DEFAULT_COLLECTIONS_LIMIT = 40
|
||||||
|
|
||||||
|
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
|
||||||
|
render json: { error: ValidationErrorFormatter.new(e).as_json }, status: 422
|
||||||
|
end
|
||||||
|
|
||||||
|
before_action :check_feature_enabled
|
||||||
|
|
||||||
|
before_action -> { authorize_if_got_token! :read, :'read:collections' }, only: [:index, :show]
|
||||||
|
before_action -> { doorkeeper_authorize! :write, :'write:collections' }, only: [:create, :update, :destroy]
|
||||||
|
|
||||||
|
before_action :require_user!, only: [:create, :update, :destroy]
|
||||||
|
|
||||||
|
before_action :set_account, only: [:index]
|
||||||
|
before_action :set_collections, only: [:index]
|
||||||
|
before_action :set_collection, only: [:show, :update, :destroy]
|
||||||
|
|
||||||
|
after_action :insert_pagination_headers, only: [:index]
|
||||||
|
|
||||||
|
after_action :verify_authorized
|
||||||
|
|
||||||
|
def index
|
||||||
|
cache_if_unauthenticated!
|
||||||
|
authorize Collection, :index?
|
||||||
|
|
||||||
|
render json: @collections, each_serializer: REST::BaseCollectionSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
cache_if_unauthenticated!
|
||||||
|
authorize @collection, :show?
|
||||||
|
|
||||||
|
render json: @collection, serializer: REST::CollectionSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize Collection, :create?
|
||||||
|
|
||||||
|
@collection = CreateCollectionService.new.call(collection_creation_params, current_user.account)
|
||||||
|
|
||||||
|
render json: @collection, serializer: REST::CollectionSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
authorize @collection, :update?
|
||||||
|
|
||||||
|
@collection.update!(collection_update_params) # TODO: Create a service for this to federate changes
|
||||||
|
|
||||||
|
render json: @collection, serializer: REST::CollectionSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize @collection, :destroy?
|
||||||
|
|
||||||
|
@collection.destroy
|
||||||
|
|
||||||
|
head 200
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_account
|
||||||
|
@account = Account.find(params[:account_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_collections
|
||||||
|
@collections = @account.collections
|
||||||
|
.with_tag
|
||||||
|
.order(created_at: :desc)
|
||||||
|
.offset(offset_param)
|
||||||
|
.limit(limit_param(DEFAULT_COLLECTIONS_LIMIT))
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_collection
|
||||||
|
@collection = Collection.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def collection_creation_params
|
||||||
|
params.permit(:name, :description, :sensitive, :discoverable, :tag_name, account_ids: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def collection_update_params
|
||||||
|
params.permit(:name, :description, :sensitive, :discoverable, :tag_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_feature_enabled
|
||||||
|
raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled?
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_path
|
||||||
|
return unless records_continue?
|
||||||
|
|
||||||
|
api_v1_alpha_account_collections_url(@account, pagination_params(offset: offset_param + limit_param(DEFAULT_COLLECTIONS_LIMIT)))
|
||||||
|
end
|
||||||
|
|
||||||
|
def prev_path
|
||||||
|
return if offset_param.zero?
|
||||||
|
|
||||||
|
api_v1_alpha_account_collections_url(@account, pagination_params(offset: offset_param - limit_param(DEFAULT_COLLECTIONS_LIMIT)))
|
||||||
|
end
|
||||||
|
|
||||||
|
def records_continue?
|
||||||
|
((offset_param * limit_param(DEFAULT_COLLECTIONS_LIMIT)) + @collections.size) < @account.collections.size
|
||||||
|
end
|
||||||
|
|
||||||
|
def offset_param
|
||||||
|
params[:offset].to_i
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -30,7 +30,7 @@ class Api::Web::EmbedsController < Api::Web::BaseController
|
|||||||
def set_status
|
def set_status
|
||||||
@status = Status.find(params[:id])
|
@status = Status.find(params[:id])
|
||||||
authorize @status, :show?
|
authorize @status, :show?
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||||||
@accept_token = session[:accept_token] = SecureRandom.hex
|
@accept_token = session[:accept_token] = SecureRandom.hex
|
||||||
@invite_code = invite_code
|
@invite_code = invite_code
|
||||||
|
|
||||||
set_locale { render :rules }
|
render :rules
|
||||||
end
|
end
|
||||||
|
|
||||||
def is_flashing_format? # rubocop:disable Naming/PredicatePrefix
|
def is_flashing_format? # rubocop:disable Naming/PredicatePrefix
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class AuthorizeInteractionsController < ApplicationController
|
|||||||
def set_resource
|
def set_resource
|
||||||
@resource = located_resource
|
@resource = located_resource
|
||||||
authorize(@resource, :show?) if @resource.is_a?(Status)
|
authorize(@resource, :show?) if @resource.is_a?(Status)
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class FollowerAccountsController < ApplicationController
|
|||||||
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
|
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
|
||||||
|
|
||||||
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||||
|
before_action :protect_hidden_collections, if: -> { request.format.json? }
|
||||||
|
|
||||||
skip_around_action :set_locale, if: -> { request.format == :json }
|
skip_around_action :set_locale, if: -> { request.format == :json }
|
||||||
skip_before_action :require_functional!, unless: :limited_federation_mode?
|
skip_before_action :require_functional!, unless: :limited_federation_mode?
|
||||||
@@ -18,8 +19,6 @@ class FollowerAccountsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
raise Mastodon::NotPermittedError if page_requested? && @account.hide_collections?
|
|
||||||
|
|
||||||
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
|
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
|
||||||
|
|
||||||
render json: collection_presenter,
|
render json: collection_presenter,
|
||||||
@@ -41,6 +40,10 @@ class FollowerAccountsController < ApplicationController
|
|||||||
@follows = scope.recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
|
@follows = scope.recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def protect_hidden_collections
|
||||||
|
raise Mastodon::NotPermittedError if page_requested? && @account.hide_collections?
|
||||||
|
end
|
||||||
|
|
||||||
def page_requested?
|
def page_requested?
|
||||||
params[:page].present?
|
params[:page].present?
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class FollowingAccountsController < ApplicationController
|
|||||||
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
|
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
|
||||||
|
|
||||||
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||||
|
before_action :protect_hidden_collections, if: -> { request.format.json? }
|
||||||
|
|
||||||
skip_around_action :set_locale, if: -> { request.format == :json }
|
skip_around_action :set_locale, if: -> { request.format == :json }
|
||||||
skip_before_action :require_functional!, unless: :limited_federation_mode?
|
skip_before_action :require_functional!, unless: :limited_federation_mode?
|
||||||
@@ -18,11 +19,6 @@ class FollowingAccountsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
if page_requested? && @account.hide_collections?
|
|
||||||
forbidden
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
|
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
|
||||||
|
|
||||||
render json: collection_presenter,
|
render json: collection_presenter,
|
||||||
@@ -44,6 +40,10 @@ class FollowingAccountsController < ApplicationController
|
|||||||
@follows = scope.recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
|
@follows = scope.recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def protect_hidden_collections
|
||||||
|
raise Mastodon::NotPermittedError if page_requested? && @account.hide_collections?
|
||||||
|
end
|
||||||
|
|
||||||
def page_requested?
|
def page_requested?
|
||||||
params[:page].present?
|
params[:page].present?
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class MediaController < ApplicationController
|
|||||||
|
|
||||||
def verify_permitted_status!
|
def verify_permitted_status!
|
||||||
authorize @media_attachment.status, :show?
|
authorize @media_attachment.status, :show?
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class StatusesController < ApplicationController
|
|||||||
def set_status
|
def set_status
|
||||||
@status = @account.statuses.find(params[:id])
|
@status = @account.statuses.find(params[:id])
|
||||||
authorize @status, :show?
|
authorize @status, :show?
|
||||||
rescue Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
not_found
|
not_found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
23
app/controllers/wrapstodon_controller.rb
Normal file
23
app/controllers/wrapstodon_controller.rb
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class WrapstodonController < ApplicationController
|
||||||
|
include WebAppControllerConcern
|
||||||
|
include Authorization
|
||||||
|
include AccountOwnedConcern
|
||||||
|
|
||||||
|
vary_by 'Accept, Accept-Language, Cookie'
|
||||||
|
|
||||||
|
before_action :set_generated_annual_report
|
||||||
|
|
||||||
|
skip_before_action :require_functional!, only: :show, unless: :limited_federation_mode?
|
||||||
|
|
||||||
|
def show
|
||||||
|
expires_in 10.seconds, public: true if current_account.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_generated_annual_report
|
||||||
|
@generated_annual_report = GeneratedAnnualReport.find_by!(account: @account, year: params[:year], share_key: params[:share_key])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -113,6 +113,7 @@ module ApplicationHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def material_symbol(icon, attributes = {})
|
def material_symbol(icon, attributes = {})
|
||||||
|
whitespace = attributes.delete(:whitespace) { true }
|
||||||
safe_join(
|
safe_join(
|
||||||
[
|
[
|
||||||
inline_svg_tag(
|
inline_svg_tag(
|
||||||
@@ -121,7 +122,7 @@ module ApplicationHelper
|
|||||||
role: :img,
|
role: :img,
|
||||||
data: attributes[:data]
|
data: attributes[:data]
|
||||||
),
|
),
|
||||||
' ',
|
whitespace ? ' ' : '',
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -152,11 +153,9 @@ module ApplicationHelper
|
|||||||
tag.meta(content: content, property: property)
|
tag.meta(content: content, property: property)
|
||||||
end
|
end
|
||||||
|
|
||||||
def body_classes
|
def html_classes
|
||||||
output = []
|
output = []
|
||||||
output << content_for(:body_classes)
|
output << content_for(:html_classes)
|
||||||
output << "flavour-#{current_flavour.parameterize}"
|
|
||||||
output << "skin-#{current_skin.parameterize}"
|
|
||||||
output << 'system-font' if current_account&.user&.setting_system_font_ui
|
output << 'system-font' if current_account&.user&.setting_system_font_ui
|
||||||
output << 'custom-scrollbars' unless current_account&.user&.setting_system_scrollbars_ui
|
output << 'custom-scrollbars' unless current_account&.user&.setting_system_scrollbars_ui
|
||||||
output << (current_account&.user&.setting_reduce_motion ? 'reduce-motion' : 'no-reduce-motion')
|
output << (current_account&.user&.setting_reduce_motion ? 'reduce-motion' : 'no-reduce-motion')
|
||||||
@@ -164,6 +163,12 @@ module ApplicationHelper
|
|||||||
output.compact_blank.join(' ')
|
output.compact_blank.join(' ')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def body_classes
|
||||||
|
output = []
|
||||||
|
output << content_for(:body_classes)
|
||||||
|
output.compact_blank.join(' ')
|
||||||
|
end
|
||||||
|
|
||||||
def cdn_host
|
def cdn_host
|
||||||
Rails.configuration.action_controller.asset_host
|
Rails.configuration.action_controller.asset_host
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ module LanguagesHelper
|
|||||||
cy: ['Welsh', 'Cymraeg'].freeze,
|
cy: ['Welsh', 'Cymraeg'].freeze,
|
||||||
da: ['Danish', 'dansk'].freeze,
|
da: ['Danish', 'dansk'].freeze,
|
||||||
de: ['German', 'Deutsch'].freeze,
|
de: ['German', 'Deutsch'].freeze,
|
||||||
dv: ['Divehi', 'Dhivehi'].freeze,
|
dv: ['Divehi', 'ދިވެހި'].freeze,
|
||||||
dz: ['Dzongkha', 'རྫོང་ཁ'].freeze,
|
dz: ['Dzongkha', 'རྫོང་ཁ'].freeze,
|
||||||
ee: ['Ewe', 'Eʋegbe'].freeze,
|
ee: ['Ewe', 'Eʋegbe'].freeze,
|
||||||
el: ['Greek', 'Ελληνικά'].freeze,
|
el: ['Greek', 'Ελληνικά'].freeze,
|
||||||
@@ -100,7 +100,7 @@ module LanguagesHelper
|
|||||||
lo: ['Lao', 'ລາວ'].freeze,
|
lo: ['Lao', 'ລາວ'].freeze,
|
||||||
lt: ['Lithuanian', 'lietuvių kalba'].freeze,
|
lt: ['Lithuanian', 'lietuvių kalba'].freeze,
|
||||||
lu: ['Luba-Katanga', 'Tshiluba'].freeze,
|
lu: ['Luba-Katanga', 'Tshiluba'].freeze,
|
||||||
lv: ['Latvian', 'latviešu valoda'].freeze,
|
lv: ['Latvian', 'Latviski'].freeze,
|
||||||
mg: ['Malagasy', 'fiteny malagasy'].freeze,
|
mg: ['Malagasy', 'fiteny malagasy'].freeze,
|
||||||
mh: ['Marshallese', 'Kajin M̧ajeļ'].freeze,
|
mh: ['Marshallese', 'Kajin M̧ajeļ'].freeze,
|
||||||
mi: ['Māori', 'te reo Māori'].freeze,
|
mi: ['Māori', 'te reo Māori'].freeze,
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ module StatusesHelper
|
|||||||
status.preloadable_poll.options.map { |o| "[ ] #{o}" }.join("\n")
|
status.preloadable_poll.options.map { |o| "[ ] #{o}" }.join("\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def status_classnames(status, is_quote)
|
||||||
|
if is_quote
|
||||||
|
'status--is-quote'
|
||||||
|
elsif status.quote.present?
|
||||||
|
'status--has-quote'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def status_description(status)
|
def status_description(status)
|
||||||
components = [[media_summary(status), status_text_summary(status)].compact_blank.join(' · ')]
|
components = [[media_summary(status), status_text_summary(status)].compact_blank.join(' · ')]
|
||||||
|
|
||||||
|
|||||||
16
app/helpers/wrapstodon_helper.rb
Normal file
16
app/helpers/wrapstodon_helper.rb
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module WrapstodonHelper
|
||||||
|
def render_wrapstodon_share_data(report)
|
||||||
|
json = ActiveModelSerializers::SerializableResource.new(
|
||||||
|
AnnualReportsPresenter.new([report]),
|
||||||
|
serializer: REST::AnnualReportsSerializer,
|
||||||
|
scope: nil,
|
||||||
|
scope_name: :current_user
|
||||||
|
).to_json
|
||||||
|
|
||||||
|
# rubocop:disable Rails/OutputSafety
|
||||||
|
content_tag(:script, json_escape(json).html_safe, type: 'application/json', id: 'wrapstodon-data')
|
||||||
|
# rubocop:enable Rails/OutputSafety
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,31 +1,49 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class DateOfBirthInput < SimpleForm::Inputs::Base
|
class DateOfBirthInput < SimpleForm::Inputs::Base
|
||||||
OPTIONS = [
|
OPTIONS = {
|
||||||
{ autocomplete: 'bday-day', maxlength: 2, pattern: '[0-9]+', placeholder: 'DD' }.freeze,
|
day: { autocomplete: 'bday-day', maxlength: 2, pattern: '[0-9]+', placeholder: 'DD' },
|
||||||
{ autocomplete: 'bday-month', maxlength: 2, pattern: '[0-9]+', placeholder: 'MM' }.freeze,
|
month: { autocomplete: 'bday-month', maxlength: 2, pattern: '[0-9]+', placeholder: 'MM' },
|
||||||
{ autocomplete: 'bday-year', maxlength: 4, pattern: '[0-9]+', placeholder: 'YYYY' }.freeze,
|
year: { autocomplete: 'bday-year', maxlength: 4, pattern: '[0-9]+', placeholder: 'YYYY' },
|
||||||
].freeze
|
}.freeze
|
||||||
|
|
||||||
def input(wrapper_options = nil)
|
def input(wrapper_options = nil)
|
||||||
merged_input_options = merge_wrapper_options(input_html_options, wrapper_options)
|
merged_input_options = merge_wrapper_options(input_html_options, wrapper_options)
|
||||||
merged_input_options[:inputmode] = 'numeric'
|
merged_input_options[:inputmode] = 'numeric'
|
||||||
|
|
||||||
values = (object.public_send(attribute_name) || '').split('.')
|
safe_join(
|
||||||
|
ordered_options.map do |option|
|
||||||
safe_join(Array.new(3) do |index|
|
options = merged_input_options
|
||||||
options = merged_input_options.merge(OPTIONS[index]).merge id: generate_id(index), 'aria-label': I18n.t("simple_form.labels.user.date_of_birth_#{index + 1}i"), value: values[index]
|
.merge(OPTIONS[option])
|
||||||
@builder.text_field("#{attribute_name}(#{index + 1}i)", options)
|
.merge(
|
||||||
end)
|
id: generate_id(option),
|
||||||
|
'aria-label': I18n.t("simple_form.labels.user.date_of_birth_#{param_for(option)}"),
|
||||||
|
value: values[option]
|
||||||
|
)
|
||||||
|
@builder.text_field("#{attribute_name}(#{param_for(option)})", options)
|
||||||
|
end
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def label_target
|
def label_target
|
||||||
"#{attribute_name}_1i"
|
"#{attribute_name}_#{param_for(ordered_options.first)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def generate_id(index)
|
def ordered_options
|
||||||
"#{object_name}_#{attribute_name}_#{index + 1}i"
|
I18n.t('date.order').map(&:to_sym)
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_id(option)
|
||||||
|
"#{object_name}_#{attribute_name}_#{param_for(option)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def param_for(option)
|
||||||
|
"#{ActionView::Helpers::DateTimeSelector::POSITION[option]}i"
|
||||||
|
end
|
||||||
|
|
||||||
|
def values
|
||||||
|
Date._parse((object.public_send(attribute_name) || '').to_s).transform_keys(mon: :month, mday: :day)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
import Rails from '@rails/ujs';
|
|
||||||
import { decode, ValidationError } from 'blurhash';
|
import { decode, ValidationError } from 'blurhash';
|
||||||
|
import { on } from 'delegated-events';
|
||||||
|
|
||||||
import ready from '../mastodon/ready';
|
import ready from '../mastodon/ready';
|
||||||
|
|
||||||
@@ -24,10 +24,9 @@ const setAnnouncementEndsAttributes = (target: HTMLInputElement) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Rails.delegate(
|
on(
|
||||||
document,
|
|
||||||
'input[type="datetime-local"]#announcement_starts_at',
|
|
||||||
'change',
|
'change',
|
||||||
|
'input[type="datetime-local"]#announcement_starts_at',
|
||||||
({ target }) => {
|
({ target }) => {
|
||||||
if (target instanceof HTMLInputElement)
|
if (target instanceof HTMLInputElement)
|
||||||
setAnnouncementEndsAttributes(target);
|
setAnnouncementEndsAttributes(target);
|
||||||
@@ -63,7 +62,7 @@ const hideSelectAll = () => {
|
|||||||
if (hiddenField) hiddenField.value = '0';
|
if (hiddenField) hiddenField.value = '0';
|
||||||
};
|
};
|
||||||
|
|
||||||
Rails.delegate(document, '#batch_checkbox_all', 'change', ({ target }) => {
|
on('change', '#batch_checkbox_all', ({ target }) => {
|
||||||
if (!(target instanceof HTMLInputElement)) return;
|
if (!(target instanceof HTMLInputElement)) return;
|
||||||
|
|
||||||
const selectAllMatchingElement = document.querySelector(
|
const selectAllMatchingElement = document.querySelector(
|
||||||
@@ -85,7 +84,7 @@ Rails.delegate(document, '#batch_checkbox_all', 'change', ({ target }) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Rails.delegate(document, '.batch-table__select-all button', 'click', () => {
|
on('click', '.batch-table__select-all button', () => {
|
||||||
const hiddenField = document.querySelector<HTMLInputElement>(
|
const hiddenField = document.querySelector<HTMLInputElement>(
|
||||||
'#select_all_matching',
|
'#select_all_matching',
|
||||||
);
|
);
|
||||||
@@ -113,7 +112,7 @@ Rails.delegate(document, '.batch-table__select-all button', 'click', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Rails.delegate(document, batchCheckboxClassName, 'change', () => {
|
on('change', batchCheckboxClassName, () => {
|
||||||
const checkAllElement = document.querySelector<HTMLInputElement>(
|
const checkAllElement = document.querySelector<HTMLInputElement>(
|
||||||
'input#batch_checkbox_all',
|
'input#batch_checkbox_all',
|
||||||
);
|
);
|
||||||
@@ -140,14 +139,9 @@ Rails.delegate(document, batchCheckboxClassName, 'change', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Rails.delegate(
|
on('change', '.filter-subset--with-select select', ({ target }) => {
|
||||||
document,
|
|
||||||
'.filter-subset--with-select select',
|
|
||||||
'change',
|
|
||||||
({ target }) => {
|
|
||||||
if (target instanceof HTMLSelectElement) target.form?.submit();
|
if (target instanceof HTMLSelectElement) target.form?.submit();
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const onDomainBlockSeverityChange = (target: HTMLSelectElement) => {
|
const onDomainBlockSeverityChange = (target: HTMLSelectElement) => {
|
||||||
const rejectMediaDiv = document.querySelector(
|
const rejectMediaDiv = document.querySelector(
|
||||||
@@ -168,11 +162,11 @@ const onDomainBlockSeverityChange = (target: HTMLSelectElement) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Rails.delegate(document, '#domain_block_severity', 'change', ({ target }) => {
|
on('change', '#domain_block_severity', ({ target }) => {
|
||||||
if (target instanceof HTMLSelectElement) onDomainBlockSeverityChange(target);
|
if (target instanceof HTMLSelectElement) onDomainBlockSeverityChange(target);
|
||||||
});
|
});
|
||||||
|
|
||||||
const onEnableBootstrapTimelineAccountsChange = (target: HTMLInputElement) => {
|
function onEnableBootstrapTimelineAccountsChange(target: HTMLInputElement) {
|
||||||
const bootstrapTimelineAccountsField =
|
const bootstrapTimelineAccountsField =
|
||||||
document.querySelector<HTMLInputElement>(
|
document.querySelector<HTMLInputElement>(
|
||||||
'#form_admin_settings_bootstrap_timeline_accounts',
|
'#form_admin_settings_bootstrap_timeline_accounts',
|
||||||
@@ -194,12 +188,11 @@ const onEnableBootstrapTimelineAccountsChange = (target: HTMLInputElement) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
Rails.delegate(
|
on(
|
||||||
document,
|
|
||||||
'#form_admin_settings_enable_bootstrap_timeline_accounts',
|
|
||||||
'change',
|
'change',
|
||||||
|
'#form_admin_settings_enable_bootstrap_timeline_accounts',
|
||||||
({ target }) => {
|
({ target }) => {
|
||||||
if (target instanceof HTMLInputElement)
|
if (target instanceof HTMLInputElement)
|
||||||
onEnableBootstrapTimelineAccountsChange(target);
|
onEnableBootstrapTimelineAccountsChange(target);
|
||||||
@@ -239,11 +232,11 @@ const onChangeRegistrationMode = (target: HTMLSelectElement) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const convertUTCDateTimeToLocal = (value: string) => {
|
function convertUTCDateTimeToLocal(value: string) {
|
||||||
const date = new Date(value + 'Z');
|
const date = new Date(value + 'Z');
|
||||||
const twoChars = (x: number) => x.toString().padStart(2, '0');
|
const twoChars = (x: number) => x.toString().padStart(2, '0');
|
||||||
return `${date.getFullYear()}-${twoChars(date.getMonth() + 1)}-${twoChars(date.getDate())}T${twoChars(date.getHours())}:${twoChars(date.getMinutes())}`;
|
return `${date.getFullYear()}-${twoChars(date.getMonth() + 1)}-${twoChars(date.getDate())}T${twoChars(date.getHours())}:${twoChars(date.getMinutes())}`;
|
||||||
};
|
}
|
||||||
|
|
||||||
function convertLocalDatetimeToUTC(value: string) {
|
function convertLocalDatetimeToUTC(value: string) {
|
||||||
const date = new Date(value);
|
const date = new Date(value);
|
||||||
@@ -251,14 +244,9 @@ function convertLocalDatetimeToUTC(value: string) {
|
|||||||
return fullISO8601.slice(0, fullISO8601.indexOf('T') + 6);
|
return fullISO8601.slice(0, fullISO8601.indexOf('T') + 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
Rails.delegate(
|
on('change', '#form_admin_settings_registrations_mode', ({ target }) => {
|
||||||
document,
|
|
||||||
'#form_admin_settings_registrations_mode',
|
|
||||||
'change',
|
|
||||||
({ target }) => {
|
|
||||||
if (target instanceof HTMLSelectElement) onChangeRegistrationMode(target);
|
if (target instanceof HTMLSelectElement) onChangeRegistrationMode(target);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
async function mountReactComponent(element: Element) {
|
async function mountReactComponent(element: Element) {
|
||||||
const componentName = element.getAttribute('data-admin-component');
|
const componentName = element.getAttribute('data-admin-component');
|
||||||
@@ -305,7 +293,7 @@ ready(() => {
|
|||||||
if (registrationMode) onChangeRegistrationMode(registrationMode);
|
if (registrationMode) onChangeRegistrationMode(registrationMode);
|
||||||
|
|
||||||
const checkAllElement = document.querySelector<HTMLInputElement>(
|
const checkAllElement = document.querySelector<HTMLInputElement>(
|
||||||
'input#batch_checkbox_all',
|
'#batch_checkbox_all',
|
||||||
);
|
);
|
||||||
if (checkAllElement) {
|
if (checkAllElement) {
|
||||||
const allCheckboxes = Array.from(
|
const allCheckboxes = Array.from(
|
||||||
@@ -318,7 +306,7 @@ ready(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document
|
document
|
||||||
.querySelector('a#add-instance-button')
|
.querySelector<HTMLAnchorElement>('a#add-instance-button')
|
||||||
?.addEventListener('click', (e) => {
|
?.addEventListener('click', (e) => {
|
||||||
const domain = document.querySelector<HTMLInputElement>(
|
const domain = document.querySelector<HTMLInputElement>(
|
||||||
'input[type="text"]#by_domain',
|
'input[type="text"]#by_domain',
|
||||||
@@ -342,7 +330,7 @@ ready(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Rails.delegate(document, 'form', 'submit', ({ target }) => {
|
on('submit', 'form', ({ target }) => {
|
||||||
if (target instanceof HTMLFormElement)
|
if (target instanceof HTMLFormElement)
|
||||||
target
|
target
|
||||||
.querySelectorAll<HTMLInputElement>('input[type="datetime-local"]')
|
.querySelectorAll<HTMLInputElement>('input[type="datetime-local"]')
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { IntlMessageFormat } from 'intl-messageformat';
|
|||||||
import type { MessageDescriptor, PrimitiveType } from 'react-intl';
|
import type { MessageDescriptor, PrimitiveType } from 'react-intl';
|
||||||
import { defineMessages } from 'react-intl';
|
import { defineMessages } from 'react-intl';
|
||||||
|
|
||||||
import Rails from '@rails/ujs';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { on } from 'delegated-events';
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
|
|
||||||
import { timeAgoString } from '../mastodon/components/relative_timestamp';
|
import { timeAgoString } from '../mastodon/components/relative_timestamp';
|
||||||
@@ -70,7 +70,7 @@ function loaded() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
document.querySelectorAll('.emojify').forEach((content) => {
|
document.querySelectorAll('.emojify').forEach((content) => {
|
||||||
content.innerHTML = emojify(content.innerHTML, {}, true); // Force emojify as public doesn't load the new emoji system.
|
content.innerHTML = emojify(content.innerHTML);
|
||||||
});
|
});
|
||||||
|
|
||||||
document
|
document
|
||||||
@@ -175,10 +175,9 @@ function loaded() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Rails.delegate(
|
on(
|
||||||
document,
|
|
||||||
'input#user_account_attributes_username',
|
|
||||||
'input',
|
'input',
|
||||||
|
'input#user_account_attributes_username',
|
||||||
throttle(
|
throttle(
|
||||||
({ target }) => {
|
({ target }) => {
|
||||||
if (!(target instanceof HTMLInputElement)) return;
|
if (!(target instanceof HTMLInputElement)) return;
|
||||||
@@ -202,11 +201,7 @@ function loaded() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Rails.delegate(
|
on('input', '#user_password,#user_password_confirmation', () => {
|
||||||
document,
|
|
||||||
'#user_password,#user_password_confirmation',
|
|
||||||
'input',
|
|
||||||
() => {
|
|
||||||
const password = document.querySelector<HTMLInputElement>(
|
const password = document.querySelector<HTMLInputElement>(
|
||||||
'input#user_password',
|
'input#user_password',
|
||||||
);
|
);
|
||||||
@@ -215,10 +210,7 @@ function loaded() {
|
|||||||
);
|
);
|
||||||
if (!confirmation || !password) return;
|
if (!confirmation || !password) return;
|
||||||
|
|
||||||
if (
|
if (confirmation.value && confirmation.value.length > password.maxLength) {
|
||||||
confirmation.value &&
|
|
||||||
confirmation.value.length > password.maxLength
|
|
||||||
) {
|
|
||||||
confirmation.setCustomValidity(
|
confirmation.setCustomValidity(
|
||||||
formatMessage(messages.passwordExceedsLength),
|
formatMessage(messages.passwordExceedsLength),
|
||||||
);
|
);
|
||||||
@@ -229,15 +221,10 @@ function loaded() {
|
|||||||
} else {
|
} else {
|
||||||
confirmation.setCustomValidity('');
|
confirmation.setCustomValidity('');
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rails.delegate(
|
on('change', '#edit_profile input[type=file]', ({ target }) => {
|
||||||
document,
|
|
||||||
'#edit_profile input[type=file]',
|
|
||||||
'change',
|
|
||||||
({ target }) => {
|
|
||||||
if (!(target instanceof HTMLInputElement)) return;
|
if (!(target instanceof HTMLInputElement)) return;
|
||||||
|
|
||||||
const avatar = document.querySelector<HTMLImageElement>(
|
const avatar = document.querySelector<HTMLImageElement>(
|
||||||
@@ -252,10 +239,9 @@ Rails.delegate(
|
|||||||
const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
|
const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
|
||||||
|
|
||||||
if (url) avatar.src = url;
|
if (url) avatar.src = url;
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
Rails.delegate(document, '.input-copy input', 'click', ({ target }) => {
|
on('click', '.input-copy input', ({ target }) => {
|
||||||
if (!(target instanceof HTMLInputElement)) return;
|
if (!(target instanceof HTMLInputElement)) return;
|
||||||
|
|
||||||
target.focus();
|
target.focus();
|
||||||
@@ -263,7 +249,7 @@ Rails.delegate(document, '.input-copy input', 'click', ({ target }) => {
|
|||||||
target.setSelectionRange(0, target.value.length);
|
target.setSelectionRange(0, target.value.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
|
on('click', '.input-copy button', ({ target }) => {
|
||||||
if (!(target instanceof HTMLButtonElement)) return;
|
if (!(target instanceof HTMLButtonElement)) return;
|
||||||
|
|
||||||
const input = target.parentNode?.querySelector<HTMLInputElement>(
|
const input = target.parentNode?.querySelector<HTMLInputElement>(
|
||||||
@@ -312,22 +298,22 @@ const toggleSidebar = () => {
|
|||||||
sidebar.classList.toggle('visible');
|
sidebar.classList.toggle('visible');
|
||||||
};
|
};
|
||||||
|
|
||||||
Rails.delegate(document, '.sidebar__toggle__icon', 'click', () => {
|
on('click', '.sidebar__toggle__icon', () => {
|
||||||
toggleSidebar();
|
toggleSidebar();
|
||||||
});
|
});
|
||||||
|
|
||||||
Rails.delegate(document, '.sidebar__toggle__icon', 'keydown', (e) => {
|
on('keydown', '.sidebar__toggle__icon', (e) => {
|
||||||
if (e.key === ' ' || e.key === 'Enter') {
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
toggleSidebar();
|
toggleSidebar();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Rails.delegate(document, 'img.custom-emoji', 'mouseover', ({ target }) => {
|
on('mouseover', 'img.custom-emoji', ({ target }) => {
|
||||||
if (target instanceof HTMLImageElement && target.dataset.original)
|
if (target instanceof HTMLImageElement && target.dataset.original)
|
||||||
target.src = target.dataset.original;
|
target.src = target.dataset.original;
|
||||||
});
|
});
|
||||||
Rails.delegate(document, 'img.custom-emoji', 'mouseout', ({ target }) => {
|
on('mouseout', 'img.custom-emoji', ({ target }) => {
|
||||||
if (target instanceof HTMLImageElement && target.dataset.static)
|
if (target instanceof HTMLImageElement && target.dataset.static)
|
||||||
target.src = target.dataset.static;
|
target.src = target.dataset.static;
|
||||||
});
|
});
|
||||||
@@ -376,11 +362,7 @@ const setInputHint = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Rails.delegate(
|
on('change', '#account_statuses_cleanup_policy_enabled', ({ target }) => {
|
||||||
document,
|
|
||||||
'#account_statuses_cleanup_policy_enabled',
|
|
||||||
'change',
|
|
||||||
({ target }) => {
|
|
||||||
if (!(target instanceof HTMLInputElement) || !target.form) return;
|
if (!(target instanceof HTMLInputElement) || !target.form) return;
|
||||||
|
|
||||||
target.form
|
target.form
|
||||||
@@ -390,8 +372,7 @@ Rails.delegate(
|
|||||||
.forEach((input) => {
|
.forEach((input) => {
|
||||||
setInputDisabled(input, !target.checked);
|
setInputDisabled(input, !target.checked);
|
||||||
});
|
});
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const updateDefaultQuotePrivacyFromPrivacy = (
|
const updateDefaultQuotePrivacyFromPrivacy = (
|
||||||
privacySelect: EventTarget | null,
|
privacySelect: EventTarget | null,
|
||||||
@@ -414,18 +395,13 @@ const updateDefaultQuotePrivacyFromPrivacy = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Rails.delegate(
|
on('change', '#user_settings_attributes_default_privacy', ({ target }) => {
|
||||||
document,
|
|
||||||
'#user_settings_attributes_default_privacy',
|
|
||||||
'change',
|
|
||||||
({ target }) => {
|
|
||||||
updateDefaultQuotePrivacyFromPrivacy(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', () => {
|
on('submit', '#registration_new_user,#new_user', () => {
|
||||||
[
|
[
|
||||||
'user_website',
|
'user_website',
|
||||||
'user_confirm_password',
|
'user_confirm_password',
|
||||||
@@ -439,7 +415,7 @@ Rails.delegate(document, '#registration_new_user,#new_user', 'submit', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Rails.delegate(document, '.rules-list button', 'click', ({ target }) => {
|
on('click', '.rules-list button', ({ target }) => {
|
||||||
if (!(target instanceof HTMLElement)) {
|
if (!(target instanceof HTMLElement)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
63
app/javascript/entrypoints/wrapstodon.tsx
Normal file
63
app/javascript/entrypoints/wrapstodon.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
|
import { Provider as ReduxProvider } from 'react-redux';
|
||||||
|
|
||||||
|
import { importFetchedStatuses } from '@/mastodon/actions/importer';
|
||||||
|
import { hydrateStore } from '@/mastodon/actions/store';
|
||||||
|
import type { ApiAnnualReportResponse } from '@/mastodon/api/annual_report';
|
||||||
|
import { Router } from '@/mastodon/components/router';
|
||||||
|
import { WrapstodonSharedPage } from '@/mastodon/features/annual_report/shared_page';
|
||||||
|
import { IntlProvider, loadLocale } from '@/mastodon/locales';
|
||||||
|
import { loadPolyfills } from '@/mastodon/polyfills';
|
||||||
|
import ready from '@/mastodon/ready';
|
||||||
|
import { setReport } from '@/mastodon/reducers/slices/annual_report';
|
||||||
|
import { store } from '@/mastodon/store';
|
||||||
|
|
||||||
|
function loaded() {
|
||||||
|
const mountNode = document.getElementById('wrapstodon');
|
||||||
|
if (!mountNode) {
|
||||||
|
throw new Error('Mount node not found');
|
||||||
|
}
|
||||||
|
const propsNode = document.getElementById('wrapstodon-data');
|
||||||
|
if (!propsNode) {
|
||||||
|
throw new Error('Initial state prop not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState = JSON.parse(
|
||||||
|
propsNode.textContent,
|
||||||
|
) as ApiAnnualReportResponse;
|
||||||
|
|
||||||
|
const report = initialState.annual_reports[0];
|
||||||
|
if (!report) {
|
||||||
|
throw new Error('Initial state report not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up store
|
||||||
|
store.dispatch(
|
||||||
|
hydrateStore({
|
||||||
|
meta: { locale: document.documentElement.lang },
|
||||||
|
accounts: initialState.accounts,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
store.dispatch(importFetchedStatuses(initialState.statuses));
|
||||||
|
|
||||||
|
store.dispatch(setReport(report));
|
||||||
|
|
||||||
|
const root = createRoot(mountNode);
|
||||||
|
root.render(
|
||||||
|
<IntlProvider>
|
||||||
|
<ReduxProvider store={store}>
|
||||||
|
<Router>
|
||||||
|
<WrapstodonSharedPage />
|
||||||
|
</Router>
|
||||||
|
</ReduxProvider>
|
||||||
|
</IntlProvider>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPolyfills()
|
||||||
|
.then(loadLocale)
|
||||||
|
.then(() => ready(loaded))
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
export const BUNDLE_FETCH_REQUEST = 'BUNDLE_FETCH_REQUEST';
|
|
||||||
export const BUNDLE_FETCH_SUCCESS = 'BUNDLE_FETCH_SUCCESS';
|
|
||||||
export const BUNDLE_FETCH_FAIL = 'BUNDLE_FETCH_FAIL';
|
|
||||||
|
|
||||||
export function fetchBundleRequest(skipLoading) {
|
|
||||||
return {
|
|
||||||
type: BUNDLE_FETCH_REQUEST,
|
|
||||||
skipLoading,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchBundleSuccess(skipLoading) {
|
|
||||||
return {
|
|
||||||
type: BUNDLE_FETCH_SUCCESS,
|
|
||||||
skipLoading,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchBundleFail(error, skipLoading) {
|
|
||||||
return {
|
|
||||||
type: BUNDLE_FETCH_FAIL,
|
|
||||||
error,
|
|
||||||
skipLoading,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,7 @@ import { throttle } from 'lodash';
|
|||||||
|
|
||||||
import api from 'flavours/glitch/api';
|
import api from 'flavours/glitch/api';
|
||||||
import { browserHistory } from 'flavours/glitch/components/router';
|
import { browserHistory } from 'flavours/glitch/components/router';
|
||||||
|
import { countableText } from 'flavours/glitch/features/compose/util/counter';
|
||||||
import { search as emojiSearch } from 'flavours/glitch/features/emoji/emoji_mart_search_light';
|
import { search as emojiSearch } from 'flavours/glitch/features/emoji/emoji_mart_search_light';
|
||||||
import { tagHistory } from 'flavours/glitch/settings';
|
import { tagHistory } from 'flavours/glitch/settings';
|
||||||
import { recoverHashtags } from 'flavours/glitch/utils/hashtag';
|
import { recoverHashtags } from 'flavours/glitch/utils/hashtag';
|
||||||
@@ -57,7 +58,6 @@ export const COMPOSE_ADVANCED_OPTIONS_CHANGE = 'COMPOSE_ADVANCED_OPTIONS_CHANGE'
|
|||||||
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
|
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
|
||||||
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
|
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
|
||||||
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
|
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
|
||||||
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
|
|
||||||
export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
|
export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
|
||||||
export const COMPOSE_CONTENT_TYPE_CHANGE = 'COMPOSE_CONTENT_TYPE_CHANGE';
|
export const COMPOSE_CONTENT_TYPE_CHANGE = 'COMPOSE_CONTENT_TYPE_CHANGE';
|
||||||
export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE';
|
export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE';
|
||||||
@@ -93,6 +93,7 @@ const messages = defineMessages({
|
|||||||
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.' },
|
||||||
|
blankPostError: { id: 'compose.error.blank_post', defaultMessage: 'Post can\'t be blank.' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ensureComposeIsVisible = (getState) => {
|
export const ensureComposeIsVisible = (getState) => {
|
||||||
@@ -197,9 +198,14 @@ export function directCompose(account) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @callback ComposeSuccessCallback
|
||||||
|
* @param {Object} status
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {null | string} overridePrivacy
|
* @param {null | string} overridePrivacy
|
||||||
* @param {undefined | Function} successCallback
|
* @param {undefined | ComposeSuccessCallback} successCallback
|
||||||
*/
|
*/
|
||||||
export function submitCompose(overridePrivacy = null, successCallback = undefined) {
|
export function submitCompose(overridePrivacy = null, successCallback = undefined) {
|
||||||
return function (dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
@@ -210,7 +216,15 @@ export function submitCompose(overridePrivacy = null, successCallback = undefine
|
|||||||
const spoilers = getState().getIn(['compose', 'spoiler']) || getState().getIn(['local_settings', 'always_show_spoilers_field']);
|
const spoilers = getState().getIn(['compose', 'spoiler']) || getState().getIn(['local_settings', 'always_show_spoilers_field']);
|
||||||
const spoiler_text = spoilers ? getState().getIn(['compose', 'spoiler_text'], '') : '';
|
const spoiler_text = spoilers ? getState().getIn(['compose', 'spoiler_text'], '') : '';
|
||||||
|
|
||||||
if (!(status?.length || media.size !== 0 || (hasQuote && spoiler_text?.length))) {
|
const fulltext = `${spoiler_text ?? ''}${countableText(status ?? '')}`;
|
||||||
|
const hasText = fulltext.trim().length > 0;
|
||||||
|
|
||||||
|
if (!(hasText || media.size !== 0 || (hasQuote && spoiler_text?.length))) {
|
||||||
|
dispatch(showAlert({
|
||||||
|
message: messages.blankPostError,
|
||||||
|
}));
|
||||||
|
dispatch(focusCompose());
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -653,6 +667,7 @@ export function fetchComposeSuggestions(token) {
|
|||||||
fetchComposeSuggestionsEmojis(dispatch, getState, token);
|
fetchComposeSuggestionsEmojis(dispatch, getState, token);
|
||||||
break;
|
break;
|
||||||
case '#':
|
case '#':
|
||||||
|
case '#':
|
||||||
fetchComposeSuggestionsTags(dispatch, getState, token);
|
fetchComposeSuggestionsTags(dispatch, getState, token);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -694,11 +709,11 @@ export function selectComposeSuggestion(position, token, suggestion, path) {
|
|||||||
|
|
||||||
dispatch(useEmoji(suggestion));
|
dispatch(useEmoji(suggestion));
|
||||||
} else if (suggestion.type === 'hashtag') {
|
} else if (suggestion.type === 'hashtag') {
|
||||||
completion = `#${suggestion.name}`;
|
completion = token + suggestion.name.slice(token.length - 1);
|
||||||
startPosition = position - 1;
|
startPosition = position - 1;
|
||||||
} else if (suggestion.type === 'account') {
|
} else if (suggestion.type === 'account') {
|
||||||
completion = getState().getIn(['accounts', suggestion.id, 'acct']);
|
completion = `@${getState().getIn(['accounts', suggestion.id, 'acct'])}`;
|
||||||
startPosition = position;
|
startPosition = position - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't want to replace hashtags that vary only in case due to accessibility, but we need to fire off an event so that
|
// We don't want to replace hashtags that vary only in case due to accessibility, but we need to fire off an event so that
|
||||||
@@ -809,13 +824,6 @@ export function changeComposeSpoilerText(text) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function changeComposeVisibility(value) {
|
|
||||||
return {
|
|
||||||
type: COMPOSE_VISIBILITY_CHANGE,
|
|
||||||
value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function insertEmojiCompose(position, emoji, needsSpace) {
|
export function insertEmojiCompose(position, emoji, needsSpace) {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_EMOJI_INSERT,
|
type: COMPOSE_EMOJI_INSERT,
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ import {
|
|||||||
} from 'flavours/glitch/store/typed_functions';
|
} from 'flavours/glitch/store/typed_functions';
|
||||||
|
|
||||||
import type { ApiQuotePolicy } from '../api_types/quotes';
|
import type { ApiQuotePolicy } from '../api_types/quotes';
|
||||||
import type { Status } from '../models/status';
|
import type { Status, StatusVisibility } from '../models/status';
|
||||||
|
import type { RootState } from '../store';
|
||||||
|
|
||||||
import { showAlert } from './alerts';
|
import { showAlert } from './alerts';
|
||||||
import { focusCompose } from './compose';
|
import { changeCompose, focusCompose } from './compose';
|
||||||
import { importFetchedStatuses } from './importer';
|
import { importFetchedStatuses } from './importer';
|
||||||
import { openModal } from './modal';
|
import { openModal } from './modal';
|
||||||
|
|
||||||
@@ -41,6 +42,10 @@ const messages = defineMessages({
|
|||||||
id: 'quote_error.unauthorized',
|
id: 'quote_error.unauthorized',
|
||||||
defaultMessage: 'You are not authorized to quote this post.',
|
defaultMessage: 'You are not authorized to quote this post.',
|
||||||
},
|
},
|
||||||
|
quoteErrorPrivateMention: {
|
||||||
|
id: 'quote_error.private_mentions',
|
||||||
|
defaultMessage: 'Quoting is not allowed with direct mentions.',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & {
|
type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & {
|
||||||
@@ -67,6 +72,39 @@ const simulateModifiedApiResponse = (
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const changeComposeVisibility = createAppThunk(
|
||||||
|
'compose/visibility_change',
|
||||||
|
(visibility: StatusVisibility, { dispatch, getState }) => {
|
||||||
|
if (visibility !== 'direct') {
|
||||||
|
return visibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = getState();
|
||||||
|
const quotedStatusId = state.compose.get('quoted_status_id') as
|
||||||
|
| string
|
||||||
|
| null;
|
||||||
|
if (!quotedStatusId) {
|
||||||
|
return visibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the quoted status
|
||||||
|
dispatch(quoteComposeCancel());
|
||||||
|
const quotedStatus = state.statuses.get(quotedStatusId) as Status | null;
|
||||||
|
if (!quotedStatus) {
|
||||||
|
return visibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the quoted status URL to the compose text
|
||||||
|
const url = quotedStatus.get('url') as string;
|
||||||
|
const text = state.compose.get('text') as string;
|
||||||
|
if (!text.includes(url)) {
|
||||||
|
const newText = text.trim() ? `${text}\n\n${url}` : url;
|
||||||
|
dispatch(changeCompose(newText));
|
||||||
|
}
|
||||||
|
return visibility;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const changeUploadCompose = createDataLoadingThunk(
|
export const changeUploadCompose = createDataLoadingThunk(
|
||||||
'compose/changeUpload',
|
'compose/changeUpload',
|
||||||
async (
|
async (
|
||||||
@@ -130,6 +168,8 @@ export const quoteComposeByStatus = createAppThunk(
|
|||||||
|
|
||||||
if (composeState.get('id')) {
|
if (composeState.get('id')) {
|
||||||
dispatch(showAlert({ message: messages.quoteErrorEdit }));
|
dispatch(showAlert({ message: messages.quoteErrorEdit }));
|
||||||
|
} else if (composeState.get('privacy') === 'direct') {
|
||||||
|
dispatch(showAlert({ message: messages.quoteErrorPrivateMention }));
|
||||||
} else if (composeState.get('poll')) {
|
} else if (composeState.get('poll')) {
|
||||||
dispatch(showAlert({ message: messages.quoteErrorPoll }));
|
dispatch(showAlert({ message: messages.quoteErrorPoll }));
|
||||||
} else if (
|
} else if (
|
||||||
@@ -173,6 +213,17 @@ export const quoteComposeById = createAppThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const composeStateForbidsLink = (composeState: RootState['compose']) => {
|
||||||
|
return (
|
||||||
|
composeState.get('quoted_status_id') ||
|
||||||
|
composeState.get('is_submitting') ||
|
||||||
|
composeState.get('poll') ||
|
||||||
|
composeState.get('is_uploading') ||
|
||||||
|
composeState.get('id') ||
|
||||||
|
composeState.get('privacy') === 'direct'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const pasteLinkCompose = createDataLoadingThunk(
|
export const pasteLinkCompose = createDataLoadingThunk(
|
||||||
'compose/pasteLink',
|
'compose/pasteLink',
|
||||||
async ({ url }: { url: string }) => {
|
async ({ url }: { url: string }) => {
|
||||||
@@ -183,15 +234,12 @@ export const pasteLinkCompose = createDataLoadingThunk(
|
|||||||
limit: 2,
|
limit: 2,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
(data, { dispatch, getState }) => {
|
(data, { dispatch, getState, requestId }) => {
|
||||||
const composeState = getState().compose;
|
const composeState = getState().compose;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
composeState.get('quoted_status_id') ||
|
composeStateForbidsLink(composeState) ||
|
||||||
composeState.get('is_submitting') ||
|
composeState.get('fetching_link') !== requestId // Request has been cancelled
|
||||||
composeState.get('poll') ||
|
|
||||||
composeState.get('is_uploading') ||
|
|
||||||
composeState.get('id')
|
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -207,6 +255,17 @@ export const pasteLinkCompose = createDataLoadingThunk(
|
|||||||
dispatch(quoteComposeById(data.statuses[0].id));
|
dispatch(quoteComposeById(data.statuses[0].id));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
useLoadingBar: false,
|
||||||
|
condition: (_, { getState }) =>
|
||||||
|
!getState().compose.get('fetching_link') &&
|
||||||
|
!composeStateForbidsLink(getState().compose),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ideally this would cancel the action and the HTTP request, but this is good enough
|
||||||
|
export const cancelPasteLinkCompose = createAction(
|
||||||
|
'compose/cancelPasteLinkCompose',
|
||||||
);
|
);
|
||||||
|
|
||||||
export const quoteComposeCancel = createAction('compose/quoteComposeCancel');
|
export const quoteComposeCancel = createAction('compose/quoteComposeCancel');
|
||||||
|
|||||||
@@ -46,11 +46,11 @@ export function importFetchedAccounts(accounts) {
|
|||||||
return importAccounts({ accounts: normalAccounts });
|
return importAccounts({ accounts: normalAccounts });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function importFetchedStatus(status) {
|
export function importFetchedStatus(status, options = {}) {
|
||||||
return importFetchedStatuses([status]);
|
return importFetchedStatuses([status], options);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function importFetchedStatuses(statuses) {
|
export function importFetchedStatuses(statuses, options = {}) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const accounts = [];
|
const accounts = [];
|
||||||
const normalStatuses = [];
|
const normalStatuses = [];
|
||||||
@@ -58,7 +58,7 @@ export function importFetchedStatuses(statuses) {
|
|||||||
const filters = [];
|
const filters = [];
|
||||||
|
|
||||||
function processStatus(status) {
|
function processStatus(status) {
|
||||||
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]), getState().get('local_settings')));
|
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]), { ...options, settings: getState().get('local_settings') }));
|
||||||
pushUnique(accounts, status.account);
|
pushUnique(accounts, status.account);
|
||||||
|
|
||||||
if (status.filtered) {
|
if (status.filtered) {
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import escapeTextContentForBrowser from 'escape-html';
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
|
|
||||||
import { makeEmojiMap } from 'flavours/glitch/models/custom_emoji';
|
|
||||||
|
|
||||||
import emojify from '../../features/emoji/emoji';
|
|
||||||
import { autoHideCW } from '../../utils/content_warning';
|
import { autoHideCW } from '../../utils/content_warning';
|
||||||
|
|
||||||
const domParser = new DOMParser();
|
const domParser = new DOMParser();
|
||||||
@@ -30,9 +27,12 @@ function stripQuoteFallback(text) {
|
|||||||
return wrapper.innerHTML;
|
return wrapper.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeStatus(status, normalOldStatus, settings) {
|
export function normalizeStatus(status, normalOldStatus, { settings, bogusQuotePolicy = false }) {
|
||||||
const normalStatus = { ...status };
|
const normalStatus = { ...status };
|
||||||
|
|
||||||
|
if (bogusQuotePolicy)
|
||||||
|
normalStatus.quote_approval = null;
|
||||||
|
|
||||||
normalStatus.account = status.account.id;
|
normalStatus.account = status.account.id;
|
||||||
|
|
||||||
if (status.reblog && status.reblog.id) {
|
if (status.reblog && status.reblog.id) {
|
||||||
@@ -80,11 +80,10 @@ export function normalizeStatus(status, normalOldStatus, settings) {
|
|||||||
} else {
|
} else {
|
||||||
const spoilerText = normalStatus.spoiler_text || '';
|
const spoilerText = normalStatus.spoiler_text || '';
|
||||||
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
||||||
const emojiMap = makeEmojiMap(normalStatus.emojis);
|
|
||||||
|
|
||||||
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
||||||
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
|
normalStatus.contentHtml = normalStatus.content;
|
||||||
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
|
normalStatus.spoilerHtml = escapeTextContentForBrowser(spoilerText);
|
||||||
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
|
// Remove quote fallback link from the DOM so it doesn't mess with paragraph margins
|
||||||
@@ -105,6 +104,8 @@ export function normalizeStatus(status, normalOldStatus, settings) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (normalOldStatus) {
|
if (normalOldStatus) {
|
||||||
|
normalStatus.quote_approval ||= normalOldStatus.get('quote_approval');
|
||||||
|
|
||||||
const list = normalOldStatus.get('media_attachments');
|
const list = normalOldStatus.get('media_attachments');
|
||||||
if (normalStatus.media_attachments && list) {
|
if (normalStatus.media_attachments && list) {
|
||||||
normalStatus.media_attachments.forEach(item => {
|
normalStatus.media_attachments.forEach(item => {
|
||||||
@@ -120,14 +121,12 @@ export function normalizeStatus(status, normalOldStatus, settings) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeStatusTranslation(translation, status) {
|
export function normalizeStatusTranslation(translation, status) {
|
||||||
const emojiMap = makeEmojiMap(status.get('emojis').toJS());
|
|
||||||
|
|
||||||
const normalTranslation = {
|
const normalTranslation = {
|
||||||
detected_source_language: translation.detected_source_language,
|
detected_source_language: translation.detected_source_language,
|
||||||
language: translation.language,
|
language: translation.language,
|
||||||
provider: translation.provider,
|
provider: translation.provider,
|
||||||
contentHtml: emojify(translation.content, emojiMap),
|
contentHtml: translation.content,
|
||||||
spoilerHtml: emojify(escapeTextContentForBrowser(translation.spoiler_text), emojiMap),
|
spoilerHtml: escapeTextContentForBrowser(translation.spoiler_text),
|
||||||
spoiler_text: translation.spoiler_text,
|
spoiler_text: translation.spoiler_text,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -141,9 +140,8 @@ export function normalizeStatusTranslation(translation, status) {
|
|||||||
|
|
||||||
export function normalizeAnnouncement(announcement) {
|
export function normalizeAnnouncement(announcement) {
|
||||||
const normalAnnouncement = { ...announcement };
|
const normalAnnouncement = { ...announcement };
|
||||||
const emojiMap = makeEmojiMap(normalAnnouncement.emojis);
|
|
||||||
|
|
||||||
normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap);
|
normalAnnouncement.contentHtml = normalAnnouncement.content;
|
||||||
|
|
||||||
return normalAnnouncement;
|
return normalAnnouncement;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { checkAnnualReport } from '@/flavours/glitch/reducers/slices/annual_report';
|
||||||
|
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
|
||||||
import { importFetchedAccount } from './importer';
|
import { importFetchedAccount } from './importer';
|
||||||
@@ -29,6 +31,9 @@ export const fetchServer = () => (dispatch, getState) => {
|
|||||||
.get('/api/v2/instance').then(({ data }) => {
|
.get('/api/v2/instance').then(({ data }) => {
|
||||||
if (data.contact.account) dispatch(importFetchedAccount(data.contact.account));
|
if (data.contact.account) dispatch(importFetchedAccount(data.contact.account));
|
||||||
dispatch(fetchServerSuccess(data));
|
dispatch(fetchServerSuccess(data));
|
||||||
|
if (data.wrapstodon) {
|
||||||
|
void dispatch(checkAnnualReport());
|
||||||
|
}
|
||||||
}).catch(err => dispatch(fetchServerFail(err)));
|
}).catch(err => dispatch(fetchServerFail(err)));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ export function fetchStatus(id, {
|
|||||||
dispatch(fetchStatusSuccess(skipLoading));
|
dispatch(fetchStatusSuccess(skipLoading));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchStatusFail(id, error, skipLoading, parentQuotePostId));
|
dispatch(fetchStatusFail(id, error, skipLoading, parentQuotePostId));
|
||||||
|
if (error.status === 404)
|
||||||
|
dispatch(deleteFromTimelines(id));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -204,8 +206,8 @@ export function deleteStatusFail(id, error) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateStatus = status => dispatch =>
|
export const updateStatus = (status, { bogusQuotePolicy }) => dispatch =>
|
||||||
dispatch(importFetchedStatus(status));
|
dispatch(importFetchedStatus(status, { bogusQuotePolicy }));
|
||||||
|
|
||||||
export function muteStatus(id) {
|
export function muteStatus(id) {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ export function hydrateStore(rawState) {
|
|||||||
|
|
||||||
dispatch(hydrateCompose());
|
dispatch(hydrateCompose());
|
||||||
dispatch(hydrateSearch());
|
dispatch(hydrateSearch());
|
||||||
|
if (rawState.accounts) {
|
||||||
dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
|
dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
|
||||||
|
}
|
||||||
dispatch(saveSettings());
|
dispatch(saveSettings());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,27 +32,38 @@ import {
|
|||||||
const randomUpTo = max =>
|
const randomUpTo = max =>
|
||||||
Math.floor(Math.random() * Math.floor(max));
|
Math.floor(Math.random() * Math.floor(max));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {import('flavours/glitch/store').AppDispatch} Dispatch
|
||||||
|
* @typedef {import('flavours/glitch/store').GetState} GetState
|
||||||
|
* @typedef {import('redux').UnknownAction} UnknownAction
|
||||||
|
* @typedef {function(Dispatch, GetState): Promise<void>} FallbackFunction
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} timelineId
|
* @param {string} timelineId
|
||||||
* @param {string} channelName
|
* @param {string} channelName
|
||||||
* @param {Object.<string, string>} params
|
* @param {Object.<string, string>} params
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
* @param {function(Function, Function): Promise<void>} [options.fallback]
|
* @param {FallbackFunction} [options.fallback]
|
||||||
* @param {function(): void} [options.fillGaps]
|
* @param {function(): UnknownAction} [options.fillGaps]
|
||||||
* @param {function(object): boolean} [options.accept]
|
* @param {function(object): boolean} [options.accept]
|
||||||
* @returns {function(): void}
|
* @returns {function(): void}
|
||||||
*/
|
*/
|
||||||
export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => {
|
export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => {
|
||||||
const { messages } = getLocale();
|
const { messages } = getLocale();
|
||||||
|
|
||||||
|
// Public streams are currently not returning personalized quote policies
|
||||||
|
const bogusQuotePolicy = channelName.startsWith('public') || channelName.startsWith('hashtag');
|
||||||
|
|
||||||
return connectStream(channelName, params, (dispatch, getState) => {
|
return connectStream(channelName, params, (dispatch, getState) => {
|
||||||
|
// @ts-ignore
|
||||||
const locale = getState().getIn(['meta', 'locale']);
|
const locale = getState().getIn(['meta', 'locale']);
|
||||||
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
let pollingId;
|
let pollingId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {function(Function, Function): Promise<void>} fallback
|
* @param {FallbackFunction} fallback
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const useFallback = async fallback => {
|
const useFallback = async fallback => {
|
||||||
@@ -89,11 +100,11 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
|||||||
switch (data.event) {
|
switch (data.event) {
|
||||||
case 'update':
|
case 'update':
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept));
|
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), { accept: options.accept, bogusQuotePolicy }));
|
||||||
break;
|
break;
|
||||||
case 'status.update':
|
case 'status.update':
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
dispatch(updateStatus(JSON.parse(data.payload)));
|
dispatch(updateStatus(JSON.parse(data.payload), { bogusQuotePolicy }));
|
||||||
break;
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
dispatch(deleteFromTimelines(data.payload));
|
dispatch(deleteFromTimelines(data.payload));
|
||||||
@@ -132,7 +143,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Function} dispatch
|
* @param {Dispatch} dispatch
|
||||||
*/
|
*/
|
||||||
async function refreshHomeTimelineAndNotification(dispatch) {
|
async function refreshHomeTimelineAndNotification(dispatch) {
|
||||||
await dispatch(expandHomeTimeline({ maxId: undefined }));
|
await dispatch(expandHomeTimeline({ maxId: undefined }));
|
||||||
@@ -151,7 +162,11 @@ async function refreshHomeTimelineAndNotification(dispatch) {
|
|||||||
* @returns {function(): void}
|
* @returns {function(): void}
|
||||||
*/
|
*/
|
||||||
export const connectUserStream = () =>
|
export const connectUserStream = () =>
|
||||||
connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps });
|
connectTimelineStream('home', 'user', {}, {
|
||||||
|
fallback: refreshHomeTimelineAndNotification,
|
||||||
|
// @ts-expect-error
|
||||||
|
fillGaps: fillHomeTimelineGaps
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
@@ -159,7 +174,10 @@ export const connectUserStream = () =>
|
|||||||
* @returns {function(): void}
|
* @returns {function(): void}
|
||||||
*/
|
*/
|
||||||
export const connectCommunityStream = ({ onlyMedia } = {}) =>
|
export const connectCommunityStream = ({ onlyMedia } = {}) =>
|
||||||
connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia })) });
|
connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, {
|
||||||
|
// @ts-expect-error
|
||||||
|
fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia }))
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
@@ -169,7 +187,10 @@ export const connectCommunityStream = ({ onlyMedia } = {}) =>
|
|||||||
* @returns {function(): void}
|
* @returns {function(): void}
|
||||||
*/
|
*/
|
||||||
export const connectPublicStream = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}) =>
|
export const connectPublicStream = ({ onlyMedia, onlyRemote, allowLocalOnly } = {}) =>
|
||||||
connectTimelineStream(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => fillPublicTimelineGaps({ onlyMedia, onlyRemote, allowLocalOnly }) });
|
connectTimelineStream(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, {}, {
|
||||||
|
// @ts-expect-error
|
||||||
|
fillGaps: () => fillPublicTimelineGaps({ onlyMedia, onlyRemote, allowLocalOnly })
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} columnId
|
* @param {string} columnId
|
||||||
@@ -192,4 +213,7 @@ export const connectDirectStream = () =>
|
|||||||
* @returns {function(): void}
|
* @returns {function(): void}
|
||||||
*/
|
*/
|
||||||
export const connectListStream = listId =>
|
export const connectListStream = listId =>
|
||||||
connectTimelineStream(`list:${listId}`, 'list', { list: listId }, { fillGaps: () => fillListTimelineGaps(listId) });
|
connectTimelineStream(`list:${listId}`, 'list', { list: listId }, {
|
||||||
|
// @ts-expect-error
|
||||||
|
fillGaps: () => fillListTimelineGaps(listId)
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
|
import { reinsertAnnualReport, TIMELINE_WRAPSTODON } from '@/flavours/glitch/reducers/slices/annual_report';
|
||||||
import api, { getLinks } from 'flavours/glitch/api';
|
import api, { getLinks } from 'flavours/glitch/api';
|
||||||
import { compareId } from 'flavours/glitch/compare_id';
|
import { compareId } from 'flavours/glitch/compare_id';
|
||||||
import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
|
import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
|
||||||
@@ -7,7 +8,7 @@ import { toServerSideType } from 'flavours/glitch/utils/filters';
|
|||||||
|
|
||||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||||
import { submitMarkers } from './markers';
|
import { submitMarkers } from './markers';
|
||||||
import {timelineDelete} from './timelines_typed';
|
import { timelineDelete } from './timelines_typed';
|
||||||
|
|
||||||
export { disconnectTimeline } from './timelines_typed';
|
export { disconnectTimeline } from './timelines_typed';
|
||||||
|
|
||||||
@@ -25,15 +26,22 @@ export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
|
|||||||
export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL';
|
export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL';
|
||||||
export const TIMELINE_INSERT = 'TIMELINE_INSERT';
|
export const TIMELINE_INSERT = 'TIMELINE_INSERT';
|
||||||
|
|
||||||
|
// When adding new special markers here, make sure to update TIMELINE_NON_STATUS_MARKERS in actions/timelines_typed.js
|
||||||
export const TIMELINE_SUGGESTIONS = 'inline-follow-suggestions';
|
export const TIMELINE_SUGGESTIONS = 'inline-follow-suggestions';
|
||||||
export const TIMELINE_GAP = null;
|
export const TIMELINE_GAP = null;
|
||||||
|
|
||||||
|
export const TIMELINE_NON_STATUS_MARKERS = [
|
||||||
|
TIMELINE_GAP,
|
||||||
|
TIMELINE_SUGGESTIONS,
|
||||||
|
TIMELINE_WRAPSTODON,
|
||||||
|
];
|
||||||
|
|
||||||
export const loadPending = timeline => ({
|
export const loadPending = timeline => ({
|
||||||
type: TIMELINE_LOAD_PENDING,
|
type: TIMELINE_LOAD_PENDING,
|
||||||
timeline,
|
timeline,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function updateTimeline(timeline, status, accept) {
|
export function updateTimeline(timeline, status, { accept = undefined, bogusQuotePolicy = false } = {}) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
if (typeof accept === 'function' && !accept(status)) {
|
if (typeof accept === 'function' && !accept(status)) {
|
||||||
return;
|
return;
|
||||||
@@ -55,7 +63,7 @@ export function updateTimeline(timeline, status, accept) {
|
|||||||
filtered = filters.length > 0;
|
filtered = filters.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(importFetchedStatus(status));
|
dispatch(importFetchedStatus(status, { bogusQuotePolicy }));
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: TIMELINE_UPDATE,
|
type: TIMELINE_UPDATE,
|
||||||
@@ -135,6 +143,7 @@ export function expandTimeline(timelineId, path, params = {}) {
|
|||||||
|
|
||||||
if (timelineId === 'home') {
|
if (timelineId === 'home') {
|
||||||
dispatch(submitMarkers());
|
dispatch(submitMarkers());
|
||||||
|
dispatch(reinsertAnnualReport())
|
||||||
}
|
}
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
|
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ import { createAction } from '@reduxjs/toolkit';
|
|||||||
|
|
||||||
import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
|
import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
|
||||||
|
|
||||||
|
import { TIMELINE_NON_STATUS_MARKERS } from './timelines';
|
||||||
|
|
||||||
|
export function isNonStatusId(value: unknown) {
|
||||||
|
return TIMELINE_NON_STATUS_MARKERS.includes(value as string | null);
|
||||||
|
}
|
||||||
|
|
||||||
export const disconnectTimeline = createAction(
|
export const disconnectTimeline = createAction(
|
||||||
'timeline/disconnect',
|
'timeline/disconnect',
|
||||||
({ timeline }: { timeline: string }) => ({
|
({ timeline }: { timeline: string }) => ({
|
||||||
|
|||||||
38
app/javascript/flavours/glitch/api/annual_report.ts
Normal file
38
app/javascript/flavours/glitch/api/annual_report.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import api, { apiRequestGet, getAsyncRefreshHeader } from '../api';
|
||||||
|
import type { ApiAccountJSON } from '../api_types/accounts';
|
||||||
|
import type { ApiStatusJSON } from '../api_types/statuses';
|
||||||
|
import type { AnnualReport } from '../models/annual_report';
|
||||||
|
|
||||||
|
export type ApiAnnualReportState =
|
||||||
|
| 'available'
|
||||||
|
| 'generating'
|
||||||
|
| 'eligible'
|
||||||
|
| 'ineligible';
|
||||||
|
|
||||||
|
export const apiGetAnnualReportState = async (year: number) => {
|
||||||
|
const response = await api().get<{ state: ApiAnnualReportState }>(
|
||||||
|
`/api/v1/annual_reports/${year}/state`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
state: response.data.state,
|
||||||
|
refresh: getAsyncRefreshHeader(response),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const apiRequestGenerateAnnualReport = async (year: number) => {
|
||||||
|
const response = await api().post(`/api/v1/annual_reports/${year}/generate`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
refresh: getAsyncRefreshHeader(response),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ApiAnnualReportResponse {
|
||||||
|
annual_reports: AnnualReport[];
|
||||||
|
accounts: ApiAccountJSON[];
|
||||||
|
statuses: ApiStatusJSON[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apiGetAnnualReport = (year: number) =>
|
||||||
|
apiRequestGet<ApiAnnualReportResponse>(`v1/annual_reports/${year}`);
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
// See app/serializers/rest/account_serializer.rb
|
// See app/serializers/rest/custom_emoji_serializer.rb
|
||||||
export interface ApiCustomEmojiJSON {
|
export interface ApiCustomEmojiJSON {
|
||||||
shortcode: string;
|
shortcode: string;
|
||||||
static_url: string;
|
static_url: string;
|
||||||
url: string;
|
url: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
|
featured?: boolean;
|
||||||
visible_in_picker: boolean;
|
visible_in_picker: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export interface ApiPreviewCardJSON {
|
|||||||
html: string;
|
html: string;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
image: string;
|
image: string | null;
|
||||||
image_description: string;
|
image_description: string;
|
||||||
embed_url: string;
|
embed_url: string;
|
||||||
blurhash: string;
|
blurhash: string;
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import Rails from '@rails/ujs';
|
import { setupLinkListeners } from './utils/links';
|
||||||
|
|
||||||
export function start() {
|
export function start() {
|
||||||
try {
|
setupLinkListeners();
|
||||||
Rails.start();
|
|
||||||
} catch {
|
|
||||||
// If called twice
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { useLinks } from 'flavours/glitch/hooks/useLinks';
|
|
||||||
|
|
||||||
import { useAppSelector } from '../store';
|
import { useAppSelector } from '../store';
|
||||||
import { isModernEmojiEnabled } from '../utils/environment';
|
|
||||||
|
|
||||||
import { EmojiHTML } from './emoji/html';
|
import { EmojiHTML } from './emoji/html';
|
||||||
import { useElementHandledLink } from './status/handled_link';
|
import { useElementHandledLink } from './status/handled_link';
|
||||||
@@ -21,22 +16,6 @@ export const AccountBio: React.FC<AccountBioProps> = ({
|
|||||||
accountId,
|
accountId,
|
||||||
showDropdown = false,
|
showDropdown = false,
|
||||||
}) => {
|
}) => {
|
||||||
const handleClick = useLinks(showDropdown);
|
|
||||||
const handleNodeChange = useCallback(
|
|
||||||
(node: HTMLDivElement | null) => {
|
|
||||||
if (
|
|
||||||
!showDropdown ||
|
|
||||||
!node ||
|
|
||||||
node.childNodes.length === 0 ||
|
|
||||||
isModernEmojiEnabled()
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
addDropdownToHashtags(node, accountId);
|
|
||||||
},
|
|
||||||
[showDropdown, accountId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const htmlHandlers = useElementHandledLink({
|
const htmlHandlers = useElementHandledLink({
|
||||||
hashtagAccountId: showDropdown ? accountId : undefined,
|
hashtagAccountId: showDropdown ? accountId : undefined,
|
||||||
});
|
});
|
||||||
@@ -62,30 +41,7 @@ export const AccountBio: React.FC<AccountBioProps> = ({
|
|||||||
htmlString={note}
|
htmlString={note}
|
||||||
extraEmojis={extraEmojis}
|
extraEmojis={extraEmojis}
|
||||||
className={classNames(className, 'translate')}
|
className={classNames(className, 'translate')}
|
||||||
onClickCapture={handleClick}
|
|
||||||
ref={handleNodeChange}
|
|
||||||
{...htmlHandlers}
|
{...htmlHandlers}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -49,7 +49,11 @@ export const Alert: React.FC<{
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
{hasAction && (
|
{hasAction && (
|
||||||
<button className='notification-bar__action' onClick={onActionClick}>
|
<button
|
||||||
|
className='notification-bar__action'
|
||||||
|
onClick={onActionClick}
|
||||||
|
type='button'
|
||||||
|
>
|
||||||
{action}
|
{action}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const AltTextBadge: React.FC<{ description: string }> = ({
|
|||||||
rootClose
|
rootClose
|
||||||
onHide={handleClose}
|
onHide={handleClose}
|
||||||
show={open}
|
show={open}
|
||||||
target={anchorRef.current}
|
target={anchorRef}
|
||||||
placement='top-end'
|
placement='top-end'
|
||||||
flip
|
flip
|
||||||
offset={offset}
|
offset={offset}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => {
|
|||||||
return [null, null];
|
return [null, null];
|
||||||
}
|
}
|
||||||
|
|
||||||
word = word.trim().toLowerCase();
|
word = word.trim();
|
||||||
|
|
||||||
if (word.length > 0) {
|
if (word.length > 0) {
|
||||||
return [left + 1, word];
|
return [left + 1, word];
|
||||||
@@ -61,7 +61,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
autoFocus: true,
|
autoFocus: true,
|
||||||
searchTokens: ['@', ':', '#'],
|
searchTokens: ['@', '@', ':', '#', '#'],
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@@ -159,8 +159,8 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||||||
this.input.focus();
|
this.input.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
componentDidUpdate (prevProps) {
|
||||||
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
|
if (prevProps.suggestions !== this.props.suggestions && this.props.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
|
||||||
this.setState({ suggestionsHidden: false });
|
this.setState({ suggestionsHidden: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
|
|||||||
word = str.slice(left, right + caretPosition);
|
word = str.slice(left, right + caretPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!word || word.trim().length < 3 || ['@', ':', '#'].indexOf(word[0]) === -1) {
|
if (!word || word.trim().length < 3 || ['@', '@', ':', '#', '#'].indexOf(word[0]) === -1) {
|
||||||
return [null, null];
|
return [null, null];
|
||||||
}
|
}
|
||||||
|
|
||||||
word = word.trim().toLowerCase();
|
word = word.trim();
|
||||||
|
|
||||||
if (word.length > 0) {
|
if (word.length > 0) {
|
||||||
return [left + 1, word];
|
return [left + 1, word];
|
||||||
@@ -50,6 +50,7 @@ const AutosuggestTextarea = forwardRef(({
|
|||||||
onKeyUp,
|
onKeyUp,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
onPaste,
|
onPaste,
|
||||||
|
onDrop,
|
||||||
onFocus,
|
onFocus,
|
||||||
autoFocus = true,
|
autoFocus = true,
|
||||||
lang,
|
lang,
|
||||||
@@ -153,6 +154,12 @@ const AutosuggestTextarea = forwardRef(({
|
|||||||
onPaste(e);
|
onPaste(e);
|
||||||
}, [onPaste]);
|
}, [onPaste]);
|
||||||
|
|
||||||
|
const handleDrop = useCallback((e) => {
|
||||||
|
if (onDrop) {
|
||||||
|
onDrop(e);
|
||||||
|
}
|
||||||
|
}, [onDrop]);
|
||||||
|
|
||||||
// Show the suggestions again whenever they change and the textarea is focused
|
// Show the suggestions again whenever they change and the textarea is focused
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (suggestions.size > 0 && textareaRef.current === document.activeElement) {
|
if (suggestions.size > 0 && textareaRef.current === document.activeElement) {
|
||||||
@@ -204,6 +211,7 @@ const AutosuggestTextarea = forwardRef(({
|
|||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onPaste={handlePaste}
|
onPaste={handlePaste}
|
||||||
|
onDrop={handleDrop}
|
||||||
dir='auto'
|
dir='auto'
|
||||||
aria-autocomplete='list'
|
aria-autocomplete='list'
|
||||||
aria-label={placeholder}
|
aria-label={placeholder}
|
||||||
@@ -235,6 +243,7 @@ AutosuggestTextarea.propTypes = {
|
|||||||
onKeyUp: PropTypes.func,
|
onKeyUp: PropTypes.func,
|
||||||
onKeyDown: PropTypes.func,
|
onKeyDown: PropTypes.func,
|
||||||
onPaste: PropTypes.func.isRequired,
|
onPaste: PropTypes.func.isRequired,
|
||||||
|
onDrop: PropTypes.func,
|
||||||
onFocus:PropTypes.func,
|
onFocus:PropTypes.func,
|
||||||
autoFocus: PropTypes.bool,
|
autoFocus: PropTypes.bool,
|
||||||
lang: PropTypes.string,
|
lang: PropTypes.string,
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export const Button: React.FC<Props> = ({
|
|||||||
aria-live={loading !== undefined ? 'polite' : undefined}
|
aria-live={loading !== undefined ? 'polite' : undefined}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
title={title}
|
title={title}
|
||||||
|
// eslint-disable-next-line react/button-has-type -- set correctly via TS
|
||||||
type={type}
|
type={type}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
import { fn, userEvent, expect } from 'storybook/test';
|
||||||
|
|
||||||
|
import type { CarouselProps } from './index';
|
||||||
|
import { Carousel } from './index';
|
||||||
|
|
||||||
|
interface TestSlideProps {
|
||||||
|
id: number;
|
||||||
|
text: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TestSlide: FC<TestSlideProps & { active: boolean }> = ({
|
||||||
|
active,
|
||||||
|
text,
|
||||||
|
color,
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
className='test-slide'
|
||||||
|
style={{
|
||||||
|
backgroundColor: active ? color : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const slides: TestSlideProps[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
text: 'first',
|
||||||
|
color: 'red',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
text: 'second',
|
||||||
|
color: 'pink',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
text: 'third',
|
||||||
|
color: 'orange',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
type StoryProps = Pick<
|
||||||
|
CarouselProps<TestSlideProps>,
|
||||||
|
'items' | 'renderItem' | 'emptyFallback' | 'onChangeSlide'
|
||||||
|
>;
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Components/Carousel',
|
||||||
|
args: {
|
||||||
|
items: slides,
|
||||||
|
renderItem(item, active) {
|
||||||
|
return <TestSlide {...item} active={active} key={item.id} />;
|
||||||
|
},
|
||||||
|
onChangeSlide: fn(),
|
||||||
|
emptyFallback: 'No slides available',
|
||||||
|
},
|
||||||
|
render(args) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Carousel {...args} />
|
||||||
|
<style>
|
||||||
|
{`.test-slide {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
min-height: 100px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
background-color: black;
|
||||||
|
}`}
|
||||||
|
</style>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
emptyFallback: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tags: ['test'],
|
||||||
|
} satisfies Meta<StoryProps>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
async play({ args, canvas }) {
|
||||||
|
const nextButton = await canvas.findByRole('button', { name: /next/i });
|
||||||
|
const slides = await canvas.findAllByRole('group');
|
||||||
|
await expect(slides).toHaveLength(slides.length);
|
||||||
|
|
||||||
|
await userEvent.click(nextButton);
|
||||||
|
await expect(args.onChangeSlide).toHaveBeenCalledWith(1, slides[1]);
|
||||||
|
|
||||||
|
await userEvent.click(nextButton);
|
||||||
|
await expect(args.onChangeSlide).toHaveBeenCalledWith(2, slides[2]);
|
||||||
|
|
||||||
|
// Wrap around
|
||||||
|
await userEvent.click(nextButton);
|
||||||
|
await expect(args.onChangeSlide).toHaveBeenCalledWith(0, slides[0]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DifferentHeights: Story = {
|
||||||
|
args: {
|
||||||
|
items: slides.map((props, index) => ({
|
||||||
|
...props,
|
||||||
|
styles: { height: 100 + index * 100 },
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NoSlides: Story = {
|
||||||
|
args: {
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
244
app/javascript/flavours/glitch/components/carousel/index.tsx
Normal file
244
app/javascript/flavours/glitch/components/carousel/index.tsx
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import type {
|
||||||
|
ComponentPropsWithoutRef,
|
||||||
|
ComponentType,
|
||||||
|
ReactElement,
|
||||||
|
ReactNode,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import type { MessageDescriptor } from 'react-intl';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { usePrevious } from '@dnd-kit/utilities';
|
||||||
|
import { animated, useSpring } from '@react-spring/web';
|
||||||
|
import { useDrag } from '@use-gesture/react';
|
||||||
|
|
||||||
|
import type { CarouselPaginationProps } from './pagination';
|
||||||
|
import { CarouselPagination } from './pagination';
|
||||||
|
|
||||||
|
import './styles.scss';
|
||||||
|
|
||||||
|
const defaultMessages = defineMessages({
|
||||||
|
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
|
||||||
|
next: { id: 'lightbox.next', defaultMessage: 'Next' },
|
||||||
|
current: {
|
||||||
|
id: 'carousel.current',
|
||||||
|
defaultMessage: '<sr>Slide</sr> {current, number} / {max, number}',
|
||||||
|
},
|
||||||
|
slide: {
|
||||||
|
id: 'carousel.slide',
|
||||||
|
defaultMessage: 'Slide {current, number} of {max, number}',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type MessageKeys = keyof typeof defaultMessages;
|
||||||
|
|
||||||
|
export interface CarouselSlideProps {
|
||||||
|
id: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RenderSlideFn<
|
||||||
|
SlideProps extends CarouselSlideProps = CarouselSlideProps,
|
||||||
|
> = (item: SlideProps, active: boolean, index: number) => ReactElement;
|
||||||
|
|
||||||
|
export interface CarouselProps<
|
||||||
|
SlideProps extends CarouselSlideProps = CarouselSlideProps,
|
||||||
|
> {
|
||||||
|
items: SlideProps[];
|
||||||
|
renderItem: RenderSlideFn<SlideProps>;
|
||||||
|
onChangeSlide?: (index: number, ref: Element) => void;
|
||||||
|
paginationComponent?: ComponentType<CarouselPaginationProps> | null;
|
||||||
|
paginationProps?: Partial<CarouselPaginationProps>;
|
||||||
|
messages?: Record<MessageKeys, MessageDescriptor>;
|
||||||
|
emptyFallback?: ReactNode;
|
||||||
|
classNamePrefix?: string;
|
||||||
|
slideClassName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Carousel = <
|
||||||
|
SlideProps extends CarouselSlideProps = CarouselSlideProps,
|
||||||
|
>({
|
||||||
|
items,
|
||||||
|
renderItem,
|
||||||
|
onChangeSlide,
|
||||||
|
paginationComponent: Pagination = CarouselPagination,
|
||||||
|
paginationProps = {},
|
||||||
|
messages = defaultMessages,
|
||||||
|
children,
|
||||||
|
emptyFallback = null,
|
||||||
|
className,
|
||||||
|
classNamePrefix = 'carousel',
|
||||||
|
slideClassName,
|
||||||
|
...wrapperProps
|
||||||
|
}: CarouselProps<SlideProps> & ComponentPropsWithoutRef<'div'>) => {
|
||||||
|
// Handle slide change
|
||||||
|
const [slideIndex, setSlideIndex] = useState(0);
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
// Handle slide heights
|
||||||
|
const [currentSlideHeight, setCurrentSlideHeight] = useState(
|
||||||
|
() => wrapperRef.current?.scrollHeight ?? 0,
|
||||||
|
);
|
||||||
|
const previousSlideHeight = usePrevious(currentSlideHeight);
|
||||||
|
const handleSlideChange = useCallback(
|
||||||
|
(direction: number) => {
|
||||||
|
setSlideIndex((prev) => {
|
||||||
|
const max = items.length - 1;
|
||||||
|
let newIndex = prev + direction;
|
||||||
|
if (newIndex < 0) {
|
||||||
|
newIndex = max;
|
||||||
|
} else if (newIndex > max) {
|
||||||
|
newIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slide = wrapperRef.current?.children[newIndex];
|
||||||
|
if (slide) {
|
||||||
|
setCurrentSlideHeight(slide.scrollHeight);
|
||||||
|
if (slide instanceof HTMLElement) {
|
||||||
|
onChangeSlide?.(newIndex, slide);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newIndex;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[items.length, onChangeSlide],
|
||||||
|
);
|
||||||
|
|
||||||
|
const observerRef = useRef<ResizeObserver | null>(null);
|
||||||
|
observerRef.current ??= new ResizeObserver(() => {
|
||||||
|
handleSlideChange(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapperStyles = useSpring({
|
||||||
|
x: `-${slideIndex * 100}%`,
|
||||||
|
height: currentSlideHeight,
|
||||||
|
// Don't animate from zero to the height of the initial slide
|
||||||
|
immediate: !previousSlideHeight,
|
||||||
|
});
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
// Update slide height when the component mounts
|
||||||
|
if (currentSlideHeight === 0) {
|
||||||
|
handleSlideChange(0);
|
||||||
|
}
|
||||||
|
}, [currentSlideHeight, handleSlideChange]);
|
||||||
|
|
||||||
|
// Handle swiping animations
|
||||||
|
const bind = useDrag(
|
||||||
|
({ swipe: [swipeX] }) => {
|
||||||
|
handleSlideChange(swipeX * -1); // Invert swipe as swiping left loads the next slide.
|
||||||
|
},
|
||||||
|
{ pointer: { capture: false } },
|
||||||
|
);
|
||||||
|
const handlePrev = useCallback(() => {
|
||||||
|
handleSlideChange(-1);
|
||||||
|
// We're focusing on the wrapper as the child slides can potentially be inert.
|
||||||
|
// Because of that, only the active slide can be focused anyway.
|
||||||
|
wrapperRef.current?.focus();
|
||||||
|
}, [handleSlideChange]);
|
||||||
|
const handleNext = useCallback(() => {
|
||||||
|
handleSlideChange(1);
|
||||||
|
wrapperRef.current?.focus();
|
||||||
|
}, [handleSlideChange]);
|
||||||
|
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
return emptyFallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...bind()}
|
||||||
|
aria-roledescription='carousel'
|
||||||
|
role='region'
|
||||||
|
className={classNames(classNamePrefix, className)}
|
||||||
|
{...wrapperProps}
|
||||||
|
>
|
||||||
|
<div className={`${classNamePrefix}__header`}>
|
||||||
|
{children}
|
||||||
|
{Pagination && items.length > 1 && (
|
||||||
|
<Pagination
|
||||||
|
current={slideIndex}
|
||||||
|
max={items.length}
|
||||||
|
onNext={handleNext}
|
||||||
|
onPrev={handlePrev}
|
||||||
|
className={`${classNamePrefix}__pagination`}
|
||||||
|
messages={messages}
|
||||||
|
{...paginationProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<animated.div
|
||||||
|
className={`${classNamePrefix}__slides`}
|
||||||
|
ref={wrapperRef}
|
||||||
|
style={wrapperStyles}
|
||||||
|
aria-label={intl.formatMessage(messages.slide, {
|
||||||
|
current: slideIndex + 1,
|
||||||
|
max: items.length,
|
||||||
|
})}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{items.map((itemsProps, index) => (
|
||||||
|
<CarouselSlideWrapper<SlideProps>
|
||||||
|
item={itemsProps}
|
||||||
|
renderItem={renderItem}
|
||||||
|
observer={observerRef.current}
|
||||||
|
index={index}
|
||||||
|
key={`slide-${itemsProps.id}`}
|
||||||
|
className={classNames(`${classNamePrefix}__slide`, slideClassName, {
|
||||||
|
active: index === slideIndex,
|
||||||
|
})}
|
||||||
|
active={index === slideIndex}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</animated.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type CarouselSlideWrapperProps<SlideProps extends CarouselSlideProps> = {
|
||||||
|
observer: ResizeObserver | null;
|
||||||
|
className: string;
|
||||||
|
active: boolean;
|
||||||
|
item: SlideProps;
|
||||||
|
index: number;
|
||||||
|
} & Pick<CarouselProps<SlideProps>, 'renderItem'>;
|
||||||
|
|
||||||
|
const CarouselSlideWrapper = <SlideProps extends CarouselSlideProps>({
|
||||||
|
observer,
|
||||||
|
className,
|
||||||
|
active,
|
||||||
|
renderItem,
|
||||||
|
item,
|
||||||
|
index,
|
||||||
|
}: CarouselSlideWrapperProps<SlideProps>) => {
|
||||||
|
const handleRef = useCallback(
|
||||||
|
(instance: HTMLDivElement | null) => {
|
||||||
|
if (observer && instance) {
|
||||||
|
observer.observe(instance);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[observer],
|
||||||
|
);
|
||||||
|
|
||||||
|
const children = useMemo(
|
||||||
|
() => renderItem(item, active, index),
|
||||||
|
[renderItem, item, active, index],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={handleRef}
|
||||||
|
className={className}
|
||||||
|
role='group'
|
||||||
|
aria-roledescription='slide'
|
||||||
|
inert={active ? undefined : ''}
|
||||||
|
data-index={index}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import type { FC, MouseEventHandler } from 'react';
|
||||||
|
|
||||||
|
import type { MessageDescriptor } from 'react-intl';
|
||||||
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
||||||
|
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||||
|
|
||||||
|
import { IconButton } from '../icon_button';
|
||||||
|
|
||||||
|
import type { MessageKeys } from './index';
|
||||||
|
|
||||||
|
export interface CarouselPaginationProps {
|
||||||
|
onNext: MouseEventHandler;
|
||||||
|
onPrev: MouseEventHandler;
|
||||||
|
current: number;
|
||||||
|
max: number;
|
||||||
|
className?: string;
|
||||||
|
messages: Record<MessageKeys, MessageDescriptor>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CarouselPagination: FC<CarouselPaginationProps> = ({
|
||||||
|
onNext,
|
||||||
|
onPrev,
|
||||||
|
current,
|
||||||
|
max,
|
||||||
|
className = '',
|
||||||
|
messages,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<IconButton
|
||||||
|
title={intl.formatMessage(messages.previous)}
|
||||||
|
icon='chevron-left'
|
||||||
|
iconComponent={ChevronLeftIcon}
|
||||||
|
onClick={onPrev}
|
||||||
|
/>
|
||||||
|
<span aria-live='polite'>
|
||||||
|
{intl.formatMessage(messages.current, {
|
||||||
|
current: current + 1,
|
||||||
|
max,
|
||||||
|
sr: (chunk) => <span className='sr-only'>{chunk}</span>,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<IconButton
|
||||||
|
title={intl.formatMessage(messages.next)}
|
||||||
|
icon='chevron-right'
|
||||||
|
iconComponent={ChevronRightIcon}
|
||||||
|
onClick={onNext}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
.carousel {
|
||||||
|
gap: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
touch-action: pan-y;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__slides {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__slide {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ export const ColumnBackButton: React.FC<{ onClick?: OnClickCallback }> = ({
|
|||||||
const handleClick = useHandleClick(onClick);
|
const handleClick = useHandleClick(onClick);
|
||||||
|
|
||||||
const component = (
|
const component = (
|
||||||
<button onClick={handleClick} className='column-back-button'>
|
<button onClick={handleClick} className='column-back-button' type='button'>
|
||||||
<Icon
|
<Icon
|
||||||
id='chevron-left'
|
id='chevron-left'
|
||||||
icon={ArrowBackIcon}
|
icon={ArrowBackIcon}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ const BackButton: React.FC<{
|
|||||||
compact: onlyIcon,
|
compact: onlyIcon,
|
||||||
})}
|
})}
|
||||||
aria-label={intl.formatMessage(messages.back)}
|
aria-label={intl.formatMessage(messages.back)}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
id='chevron-left'
|
id='chevron-left'
|
||||||
@@ -172,6 +173,7 @@ export const ColumnHeader: React.FC<Props> = ({
|
|||||||
<button
|
<button
|
||||||
className='text-btn column-header__setting-btn'
|
className='text-btn column-header__setting-btn'
|
||||||
onClick={handlePin}
|
onClick={handlePin}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
<Icon id='times' icon={CloseIcon} />{' '}
|
<Icon id='times' icon={CloseIcon} />{' '}
|
||||||
<FormattedMessage id='column_header.unpin' defaultMessage='Unpin' />
|
<FormattedMessage id='column_header.unpin' defaultMessage='Unpin' />
|
||||||
@@ -185,6 +187,7 @@ export const ColumnHeader: React.FC<Props> = ({
|
|||||||
aria-label={intl.formatMessage(messages.moveLeft)}
|
aria-label={intl.formatMessage(messages.moveLeft)}
|
||||||
className='icon-button column-header__setting-btn'
|
className='icon-button column-header__setting-btn'
|
||||||
onClick={handleMoveLeft}
|
onClick={handleMoveLeft}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
<Icon id='chevron-left' icon={ChevronLeftIcon} />
|
<Icon id='chevron-left' icon={ChevronLeftIcon} />
|
||||||
</button>
|
</button>
|
||||||
@@ -193,6 +196,7 @@ export const ColumnHeader: React.FC<Props> = ({
|
|||||||
aria-label={intl.formatMessage(messages.moveRight)}
|
aria-label={intl.formatMessage(messages.moveRight)}
|
||||||
className='icon-button column-header__setting-btn'
|
className='icon-button column-header__setting-btn'
|
||||||
onClick={handleMoveRight}
|
onClick={handleMoveRight}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
<Icon id='chevron-right' icon={ChevronRightIcon} />
|
<Icon id='chevron-right' icon={ChevronRightIcon} />
|
||||||
</button>
|
</button>
|
||||||
@@ -203,6 +207,7 @@ export const ColumnHeader: React.FC<Props> = ({
|
|||||||
<button
|
<button
|
||||||
className='text-btn column-header__setting-btn'
|
className='text-btn column-header__setting-btn'
|
||||||
onClick={handlePin}
|
onClick={handlePin}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
<Icon id='plus' icon={AddIcon} />{' '}
|
<Icon id='plus' icon={AddIcon} />{' '}
|
||||||
<FormattedMessage id='column_header.pin' defaultMessage='Pin' />
|
<FormattedMessage id='column_header.pin' defaultMessage='Pin' />
|
||||||
@@ -237,6 +242,7 @@ export const ColumnHeader: React.FC<Props> = ({
|
|||||||
collapsed ? messages.show : messages.hide,
|
collapsed ? messages.show : messages.hide,
|
||||||
)}
|
)}
|
||||||
onClick={handleToggleClick}
|
onClick={handleToggleClick}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
<i className='icon-with-badge'>
|
<i className='icon-with-badge'>
|
||||||
<Icon
|
<Icon
|
||||||
@@ -259,7 +265,11 @@ export const ColumnHeader: React.FC<Props> = ({
|
|||||||
<>
|
<>
|
||||||
{backButton}
|
{backButton}
|
||||||
|
|
||||||
<button onClick={handleTitleClick} className='column-header__title'>
|
<button
|
||||||
|
onClick={handleTitleClick}
|
||||||
|
className='column-header__title'
|
||||||
|
type='button'
|
||||||
|
>
|
||||||
{!backButton && (
|
{!backButton && (
|
||||||
<Icon
|
<Icon
|
||||||
id={icon}
|
id={icon}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useState, useEffect, useRef } from 'react';
|
import { useCallback, useState, useRef } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
@@ -12,11 +12,15 @@ export const ColumnSearchHeader: React.FC<{
|
|||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
// Reset the component when it turns from active to inactive.
|
||||||
|
// [More on this pattern](https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes)
|
||||||
|
const [previousActive, setPreviousActive] = useState(active);
|
||||||
|
if (active !== previousActive) {
|
||||||
|
setPreviousActive(active);
|
||||||
if (!active) {
|
if (!active) {
|
||||||
setValue('');
|
setValue('');
|
||||||
}
|
}
|
||||||
}, [active]);
|
}
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export const CopyPasteText: React.FC<{ value: string }> = ({ value }) => {
|
|||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button className='button' onClick={handleButtonClick}>
|
<button className='button' onClick={handleButtonClick} type='button'>
|
||||||
<Icon id='copy' icon={ContentCopyIcon} />{' '}
|
<Icon id='copy' icon={ContentCopyIcon} />{' '}
|
||||||
{copied ? (
|
{copied ? (
|
||||||
<FormattedMessage id='copypaste.copied' defaultMessage='Copied' />
|
<FormattedMessage id='copypaste.copied' defaultMessage='Copied' />
|
||||||
|
|||||||
@@ -9,9 +9,8 @@ import { Skeleton } from '../skeleton';
|
|||||||
import type { DisplayNameProps } from './index';
|
import type { DisplayNameProps } from './index';
|
||||||
|
|
||||||
export const DisplayNameWithoutDomain: FC<
|
export const DisplayNameWithoutDomain: FC<
|
||||||
Omit<DisplayNameProps, 'variant' | 'localDomain'> &
|
Omit<DisplayNameProps, 'variant'> & ComponentPropsWithoutRef<'span'>
|
||||||
ComponentPropsWithoutRef<'span'>
|
> = ({ account, className, children, localDomain: _, ...props }) => {
|
||||||
> = ({ account, className, children, ...props }) => {
|
|
||||||
return (
|
return (
|
||||||
<AnimateEmojiProvider
|
<AnimateEmojiProvider
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import { EmojiHTML } from '../emoji/html';
|
|||||||
import type { DisplayNameProps } from './index';
|
import type { DisplayNameProps } from './index';
|
||||||
|
|
||||||
export const DisplayNameSimple: FC<
|
export const DisplayNameSimple: FC<
|
||||||
Omit<DisplayNameProps, 'variant' | 'localDomain'> &
|
Omit<DisplayNameProps, 'variant'> & ComponentPropsWithoutRef<'span'>
|
||||||
ComponentPropsWithoutRef<'span'>
|
> = ({ account, localDomain: _, ...props }) => {
|
||||||
> = ({ account, ...props }) => {
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export const Dropdown: FC<
|
|||||||
placement='bottom-start'
|
placement='bottom-start'
|
||||||
onHide={handleClose}
|
onHide={handleClose}
|
||||||
flip
|
flip
|
||||||
target={buttonRef.current}
|
target={buttonRef}
|
||||||
popperConfig={{
|
popperConfig={{
|
||||||
strategy: 'fixed',
|
strategy: 'fixed',
|
||||||
modifiers: [matchWidth],
|
modifiers: [matchWidth],
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
closeDropdownMenu,
|
closeDropdownMenu,
|
||||||
} from 'flavours/glitch/actions/dropdown_menu';
|
} from 'flavours/glitch/actions/dropdown_menu';
|
||||||
import { openModal, closeModal } from 'flavours/glitch/actions/modal';
|
import { openModal, closeModal } from 'flavours/glitch/actions/modal';
|
||||||
|
import { fetchStatus } from 'flavours/glitch/actions/statuses';
|
||||||
import { CircularProgress } from 'flavours/glitch/components/circular_progress';
|
import { CircularProgress } from 'flavours/glitch/components/circular_progress';
|
||||||
import { isUserTouching } from 'flavours/glitch/is_mobile';
|
import { isUserTouching } from 'flavours/glitch/is_mobile';
|
||||||
import {
|
import {
|
||||||
@@ -42,16 +43,10 @@ import { IconButton } from './icon_button';
|
|||||||
|
|
||||||
let id = 0;
|
let id = 0;
|
||||||
|
|
||||||
export interface RenderItemFnHandlers {
|
|
||||||
onClick: React.MouseEventHandler;
|
|
||||||
onKeyUp: React.KeyboardEventHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RenderItemFn<Item = MenuItem> = (
|
export type RenderItemFn<Item = MenuItem> = (
|
||||||
item: Item,
|
item: Item,
|
||||||
index: number,
|
index: number,
|
||||||
handlers: RenderItemFnHandlers,
|
onClick: React.MouseEventHandler,
|
||||||
focusRefCallback?: (c: HTMLAnchorElement | HTMLButtonElement | null) => void,
|
|
||||||
) => React.ReactNode;
|
) => React.ReactNode;
|
||||||
|
|
||||||
type ItemClickFn<Item = MenuItem> = (item: Item, index: number) => void;
|
type ItemClickFn<Item = MenuItem> = (item: Item, index: number) => void;
|
||||||
@@ -101,7 +96,6 @@ export const DropdownMenu = <Item = MenuItem,>({
|
|||||||
onItemClick,
|
onItemClick,
|
||||||
}: DropdownMenuProps<Item>) => {
|
}: DropdownMenuProps<Item>) => {
|
||||||
const nodeRef = useRef<HTMLDivElement>(null);
|
const nodeRef = useRef<HTMLDivElement>(null);
|
||||||
const focusedItemRef = useRef<HTMLElement | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleDocumentClick = (e: MouseEvent) => {
|
const handleDocumentClick = (e: MouseEvent) => {
|
||||||
@@ -163,8 +157,11 @@ export const DropdownMenu = <Item = MenuItem,>({
|
|||||||
document.addEventListener('click', handleDocumentClick, { capture: true });
|
document.addEventListener('click', handleDocumentClick, { capture: true });
|
||||||
document.addEventListener('keydown', handleKeyDown, { capture: true });
|
document.addEventListener('keydown', handleKeyDown, { capture: true });
|
||||||
|
|
||||||
if (focusedItemRef.current && openedViaKeyboard) {
|
if (openedViaKeyboard) {
|
||||||
focusedItemRef.current.focus({ preventScroll: true });
|
const firstMenuItem = nodeRef.current?.querySelector<
|
||||||
|
HTMLAnchorElement | HTMLButtonElement
|
||||||
|
>('li:first-child > :is(a, button)');
|
||||||
|
firstMenuItem?.focus({ preventScroll: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -175,13 +172,6 @@ export const DropdownMenu = <Item = MenuItem,>({
|
|||||||
};
|
};
|
||||||
}, [onClose, openedViaKeyboard]);
|
}, [onClose, openedViaKeyboard]);
|
||||||
|
|
||||||
const handleFocusedItemRef = useCallback(
|
|
||||||
(c: HTMLAnchorElement | HTMLButtonElement | null) => {
|
|
||||||
focusedItemRef.current = c as HTMLElement;
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleItemClick = useCallback(
|
const handleItemClick = useCallback(
|
||||||
(e: React.MouseEvent | React.KeyboardEvent) => {
|
(e: React.MouseEvent | React.KeyboardEvent) => {
|
||||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||||
@@ -207,15 +197,6 @@ export const DropdownMenu = <Item = MenuItem,>({
|
|||||||
[onClose, onItemClick, items],
|
[onClose, onItemClick, items],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleItemKeyUp = useCallback(
|
|
||||||
(e: React.KeyboardEvent) => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
handleItemClick(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[handleItemClick],
|
|
||||||
);
|
|
||||||
|
|
||||||
const nativeRenderItem = (option: Item, i: number) => {
|
const nativeRenderItem = (option: Item, i: number) => {
|
||||||
if (!isMenuItem(option)) {
|
if (!isMenuItem(option)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -232,11 +213,10 @@ export const DropdownMenu = <Item = MenuItem,>({
|
|||||||
if (isActionItem(option)) {
|
if (isActionItem(option)) {
|
||||||
element = (
|
element = (
|
||||||
<button
|
<button
|
||||||
ref={i === 0 ? handleFocusedItemRef : undefined}
|
|
||||||
onClick={handleItemClick}
|
onClick={handleItemClick}
|
||||||
onKeyUp={handleItemKeyUp}
|
|
||||||
data-index={i}
|
data-index={i}
|
||||||
aria-disabled={disabled}
|
aria-disabled={disabled}
|
||||||
|
type='button'
|
||||||
>
|
>
|
||||||
<DropdownMenuItemContent item={option} />
|
<DropdownMenuItemContent item={option} />
|
||||||
</button>
|
</button>
|
||||||
@@ -248,9 +228,7 @@ export const DropdownMenu = <Item = MenuItem,>({
|
|||||||
target={option.target ?? '_target'}
|
target={option.target ?? '_target'}
|
||||||
data-method={option.method}
|
data-method={option.method}
|
||||||
rel='noopener'
|
rel='noopener'
|
||||||
ref={i === 0 ? handleFocusedItemRef : undefined}
|
|
||||||
onClick={handleItemClick}
|
onClick={handleItemClick}
|
||||||
onKeyUp={handleItemKeyUp}
|
|
||||||
data-index={i}
|
data-index={i}
|
||||||
>
|
>
|
||||||
<DropdownMenuItemContent item={option} />
|
<DropdownMenuItemContent item={option} />
|
||||||
@@ -258,13 +236,7 @@ export const DropdownMenu = <Item = MenuItem,>({
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
element = (
|
element = (
|
||||||
<Link
|
<Link to={option.to} onClick={handleItemClick} data-index={i}>
|
||||||
to={option.to}
|
|
||||||
ref={i === 0 ? handleFocusedItemRef : undefined}
|
|
||||||
onClick={handleItemClick}
|
|
||||||
onKeyUp={handleItemKeyUp}
|
|
||||||
data-index={i}
|
|
||||||
>
|
|
||||||
<DropdownMenuItemContent item={option} />
|
<DropdownMenuItemContent item={option} />
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
@@ -307,15 +279,7 @@ export const DropdownMenu = <Item = MenuItem,>({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{items.map((option, i) =>
|
{items.map((option, i) =>
|
||||||
renderItemMethod(
|
renderItemMethod(option, i, handleItemClick),
|
||||||
option,
|
|
||||||
i,
|
|
||||||
{
|
|
||||||
onClick: handleItemClick,
|
|
||||||
onKeyUp: handleItemKeyUp,
|
|
||||||
},
|
|
||||||
i === 0 ? handleFocusedItemRef : undefined,
|
|
||||||
),
|
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
@@ -340,6 +304,7 @@ interface DropdownProps<Item extends object | null = MenuItem> {
|
|||||||
*/
|
*/
|
||||||
scrollKey?: string;
|
scrollKey?: string;
|
||||||
status?: ImmutableMap<string, unknown>;
|
status?: ImmutableMap<string, unknown>;
|
||||||
|
needsStatusRefresh?: boolean;
|
||||||
forceDropdown?: boolean;
|
forceDropdown?: boolean;
|
||||||
renderItem?: RenderItemFn<Item>;
|
renderItem?: RenderItemFn<Item>;
|
||||||
renderHeader?: RenderHeaderFn<Item>;
|
renderHeader?: RenderHeaderFn<Item>;
|
||||||
@@ -363,6 +328,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
|
|||||||
placement = 'bottom',
|
placement = 'bottom',
|
||||||
offset = [5, 5],
|
offset = [5, 5],
|
||||||
status,
|
status,
|
||||||
|
needsStatusRefresh,
|
||||||
forceDropdown = false,
|
forceDropdown = false,
|
||||||
renderItem,
|
renderItem,
|
||||||
renderHeader,
|
renderHeader,
|
||||||
@@ -382,6 +348,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
|
|||||||
const prefetchAccountId = status
|
const prefetchAccountId = status
|
||||||
? status.getIn(['account', 'id'])
|
? status.getIn(['account', 'id'])
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const statusId = status?.get('id') as string | undefined;
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
if (buttonRef.current) {
|
if (buttonRef.current) {
|
||||||
@@ -399,7 +366,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
|
|||||||
}, [dispatch, currentId]);
|
}, [dispatch, currentId]);
|
||||||
|
|
||||||
const handleItemClick = useCallback(
|
const handleItemClick = useCallback(
|
||||||
(e: React.MouseEvent | React.KeyboardEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||||
const item = items?.[i];
|
const item = items?.[i];
|
||||||
|
|
||||||
@@ -420,10 +387,20 @@ export const Dropdown = <Item extends object | null = MenuItem>({
|
|||||||
[handleClose, onItemClick, items],
|
[handleClose, onItemClick, items],
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleDropdown = useCallback(
|
const isKeypressRef = useRef(false);
|
||||||
(e: React.MouseEvent | React.KeyboardEvent) => {
|
|
||||||
const { type } = e;
|
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
|
isKeypressRef.current = true;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const unsetIsKeypress = useCallback(() => {
|
||||||
|
isKeypressRef.current = false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleDropdown = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
if (open) {
|
if (open) {
|
||||||
handleClose();
|
handleClose();
|
||||||
} else {
|
} else {
|
||||||
@@ -436,6 +413,15 @@ export const Dropdown = <Item extends object | null = MenuItem>({
|
|||||||
dispatch(fetchRelationships([prefetchAccountId]));
|
dispatch(fetchRelationships([prefetchAccountId]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (needsStatusRefresh && statusId) {
|
||||||
|
dispatch(
|
||||||
|
fetchStatus(statusId, {
|
||||||
|
forceFetch: true,
|
||||||
|
alsoFetchContext: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isUserTouching() && !forceDropdown) {
|
if (isUserTouching() && !forceDropdown) {
|
||||||
dispatch(
|
dispatch(
|
||||||
openModal({
|
openModal({
|
||||||
@@ -450,10 +436,11 @@ export const Dropdown = <Item extends object | null = MenuItem>({
|
|||||||
dispatch(
|
dispatch(
|
||||||
openDropdownMenu({
|
openDropdownMenu({
|
||||||
id: currentId,
|
id: currentId,
|
||||||
keyboard: type !== 'click',
|
keyboard: isKeypressRef.current,
|
||||||
scrollKey,
|
scrollKey,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
isKeypressRef.current = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -468,6 +455,8 @@ export const Dropdown = <Item extends object | null = MenuItem>({
|
|||||||
items,
|
items,
|
||||||
forceDropdown,
|
forceDropdown,
|
||||||
handleClose,
|
handleClose,
|
||||||
|
statusId,
|
||||||
|
needsStatusRefresh,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -484,6 +473,9 @@ export const Dropdown = <Item extends object | null = MenuItem>({
|
|||||||
const buttonProps = {
|
const buttonProps = {
|
||||||
disabled,
|
disabled,
|
||||||
onClick: toggleDropdown,
|
onClick: toggleDropdown,
|
||||||
|
onKeyDown: handleKeyDown,
|
||||||
|
onKeyUp: unsetIsKeypress,
|
||||||
|
onBlur: unsetIsKeypress,
|
||||||
'aria-expanded': open,
|
'aria-expanded': open,
|
||||||
'aria-controls': menuId,
|
'aria-controls': menuId,
|
||||||
ref: buttonRef,
|
ref: buttonRef,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user