mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-16 17:29:14 +00:00
Compare commits
641 Commits
split-comp
...
feature/ex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8375ed1cfd | ||
|
|
3d80ba01d7 | ||
|
|
f02150468b | ||
|
|
169d83f532 | ||
|
|
31dd261375 | ||
|
|
c2b479efec | ||
|
|
a94dc21c79 | ||
|
|
9512db920c | ||
|
|
c89cce0219 | ||
|
|
fa3587645d | ||
|
|
9ed51cecd0 | ||
|
|
514edd3c23 | ||
|
|
a3760b7729 | ||
|
|
cbf00168f1 | ||
|
|
4f9a493d9d | ||
|
|
8c0733a14e | ||
|
|
36a35be2ad | ||
|
|
3783062450 | ||
|
|
15c9c2fd7e | ||
|
|
227dbb6adb | ||
|
|
769f62d96f | ||
|
|
003bfd094e | ||
|
|
fae8dce738 | ||
|
|
b0487488a7 | ||
|
|
f5d6bdd9c0 | ||
|
|
ad8e856a5b | ||
|
|
7ebd6ed03c | ||
|
|
59936b7a98 | ||
|
|
fd9a171129 | ||
|
|
6ba35630bc | ||
|
|
79d8911116 | ||
|
|
d880b3182b | ||
|
|
f9d7b8a94f | ||
|
|
211f0a9513 | ||
|
|
4a527154b7 | ||
|
|
df71eadaae | ||
|
|
323d437a09 | ||
|
|
3278c08c29 | ||
|
|
0284fd723b | ||
|
|
0e0703dbd8 | ||
|
|
7dbcc7ed3d | ||
|
|
83b3a0389c | ||
|
|
af2d793398 | ||
|
|
70592cdaba | ||
|
|
30b5254a5d | ||
|
|
0a207be99d | ||
|
|
500c465226 | ||
|
|
2ea9b164d3 | ||
|
|
b1576c52df | ||
|
|
4612f7caea | ||
|
|
0c547faf92 | ||
|
|
eaaf2170fe | ||
|
|
6f7d00bfdd | ||
|
|
5c2e1869f0 | ||
|
|
0f2af2a974 | ||
|
|
27f8d7069b | ||
|
|
44207b6af6 | ||
|
|
27e55da853 | ||
|
|
3cac5bc2c3 | ||
|
|
29c44fa5fa | ||
|
|
7a9c7d4e0b | ||
|
|
932571fa22 | ||
|
|
c9df53044a | ||
|
|
ab71cf4593 | ||
|
|
c450ddb613 | ||
|
|
15b886a6f0 | ||
|
|
4819e2913d | ||
|
|
72e662bb0d | ||
|
|
7d7844a47f | ||
|
|
f2cbfb2eb3 | ||
|
|
67ad453373 | ||
|
|
3dff74eecf | ||
|
|
3f333a8d31 | ||
|
|
14e1fb8d36 | ||
|
|
514fc908a3 | ||
|
|
b9f7bc149b | ||
|
|
bc077018b8 | ||
|
|
90712d4293 | ||
|
|
6867681c7c | ||
|
|
bdc8b4fd91 | ||
|
|
2ff7146b6d | ||
|
|
c7908e2d09 | ||
|
|
c9d04f1c39 | ||
|
|
9e15eeec63 | ||
|
|
3c45d3963a | ||
|
|
baa8b82179 | ||
|
|
4b460bc571 | ||
|
|
7ca173be47 | ||
|
|
1ae5d49a71 | ||
|
|
a12572e074 | ||
|
|
dabc309ca3 | ||
|
|
1caf11ddcc | ||
|
|
95f018a3d4 | ||
|
|
a4caa7eb62 | ||
|
|
7c2d84910c | ||
|
|
b00cc4b9bd | ||
|
|
dd6ede554f | ||
|
|
6859d4c028 | ||
|
|
7d853b514a | ||
|
|
85c7c42098 | ||
|
|
8185f98872 | ||
|
|
5264496240 | ||
|
|
be75b13d68 | ||
|
|
9417c9bb8f | ||
|
|
11bddd31ce | ||
|
|
dd5cb5085c | ||
|
|
e7adbf572a | ||
|
|
13ffa3c59e | ||
|
|
aec5097d44 | ||
|
|
1646f622a5 | ||
|
|
e0cda4a851 | ||
|
|
d8d2a54741 | ||
|
|
fa21d004c7 | ||
|
|
6994664a13 | ||
|
|
be7ffa2d75 | ||
|
|
e821c00e74 | ||
|
|
9b994c4aee | ||
|
|
4c3dd0b254 | ||
|
|
672df4ecc0 | ||
|
|
aefb4719bc | ||
|
|
4d67bf18fe | ||
|
|
f09a250a7c | ||
|
|
9b50a9dd83 | ||
|
|
2293466edd | ||
|
|
b6f3869f8d | ||
|
|
09cffaaf04 | ||
|
|
334a633c2a | ||
|
|
8b12e3cc7f | ||
|
|
d3f46a77c3 | ||
|
|
a789315361 | ||
|
|
579c7a88e0 | ||
|
|
8538170c2d | ||
|
|
249bdc169c | ||
|
|
9dd8dff683 | ||
|
|
a187dcefa1 | ||
|
|
5d170587e3 | ||
|
|
37fdddd927 | ||
|
|
6ec1aa372d | ||
|
|
2c3544eedd | ||
|
|
d3b6746173 | ||
|
|
2a5d1d5a1b | ||
|
|
e18ed4bbc7 | ||
|
|
6a4e2db661 | ||
|
|
bfa7f9ebf2 | ||
|
|
8cc1ed3c55 | ||
|
|
5e1e466da0 | ||
|
|
cfe39fb58d | ||
|
|
a0294c8880 | ||
|
|
ba8fb2fd0f | ||
|
|
6fd2e8c3c5 | ||
|
|
15963a15c6 | ||
|
|
1b5806b744 | ||
|
|
1b1e025b41 | ||
|
|
ab9f1b6e50 | ||
|
|
b767eb7ff8 | ||
|
|
0b32338e3f | ||
|
|
e482595a5d | ||
|
|
9c04fadec9 | ||
|
|
390bfec6da | ||
|
|
c2980d5b17 | ||
|
|
a75aa62f5b | ||
|
|
8fd8f81ae7 | ||
|
|
921cf3e9c8 | ||
|
|
7dc5035031 | ||
|
|
2305f7c391 | ||
|
|
ff7d02b236 | ||
|
|
1a0df58878 | ||
|
|
74437c6bff | ||
|
|
504737e860 | ||
|
|
af2d22f88c | ||
|
|
667df47168 | ||
|
|
173a970752 | ||
|
|
9a5ae09620 | ||
|
|
f7937d903c | ||
|
|
6b2be5dbfb | ||
|
|
69957ed10a | ||
|
|
d1a78eba15 | ||
|
|
2db9ccaf3e | ||
|
|
cecf204bbb | ||
|
|
fec13735a7 | ||
|
|
7b8f262840 | ||
|
|
3f51a22d3b | ||
|
|
39e7a763ff | ||
|
|
e95bdec7c5 | ||
|
|
fcca31350d | ||
|
|
ee72a39641 | ||
|
|
f59ed3a4fa | ||
|
|
7be620775e | ||
|
|
4c76402ba1 | ||
|
|
9958eba356 | ||
|
|
0827c09c44 | ||
|
|
938cd2875b | ||
|
|
7876aed134 | ||
|
|
ce9a5f358e | ||
|
|
8f527bd588 | ||
|
|
07994eed00 | ||
|
|
bab9afaa09 | ||
|
|
15093f9113 | ||
|
|
f92d991e52 | ||
|
|
26402ee2cb | ||
|
|
f095a9f8a5 | ||
|
|
0d5d11eeff | ||
|
|
0397c58b61 | ||
|
|
884b085f53 | ||
|
|
2a2698e450 | ||
|
|
8ecfdd8795 | ||
|
|
00840f4f2e | ||
|
|
1cebfed23e | ||
|
|
649a20ab46 | ||
|
|
3ac7b353f8 | ||
|
|
21bb4a6c3b | ||
|
|
c2af138113 | ||
|
|
fb8aa2b3ba | ||
|
|
00f9f16f94 | ||
|
|
18f69fb964 | ||
|
|
04c3fb2189 | ||
|
|
7c03e59338 | ||
|
|
b88635202f | ||
|
|
409051c22c | ||
|
|
9caa90025f | ||
|
|
c5157ef07b | ||
|
|
f72ed21cd6 | ||
|
|
da172a8b1b | ||
|
|
cf615abbf9 | ||
|
|
b01a19fe39 | ||
|
|
c66fe2aeba | ||
|
|
fbe1115114 | ||
|
|
e4c761f902 | ||
|
|
2c6a85832c | ||
|
|
829e2e8c5d | ||
|
|
8a716c9e96 | ||
|
|
80393a23d0 | ||
|
|
8d23667536 | ||
|
|
9846806cb5 | ||
|
|
760cfe328f | ||
|
|
c1b086a538 | ||
|
|
696c2c6f2f | ||
|
|
5927b43c0f | ||
|
|
871c0d251a | ||
|
|
11a7507318 | ||
|
|
d63de55ef8 | ||
|
|
72bb3e03fd | ||
|
|
f391a4673a | ||
|
|
143b77e10d | ||
|
|
4cbb638604 | ||
|
|
3534e115e5 | ||
|
|
ea958cae7f | ||
|
|
10e9a9a3f9 | ||
|
|
6e9eda5331 | ||
|
|
4c23544714 | ||
|
|
74e5078795 | ||
|
|
110227ac5e | ||
|
|
f26758dc01 | ||
|
|
23792f5a7c | ||
|
|
fe5b66aa08 | ||
|
|
93d4192a67 | ||
|
|
d5acf4275f | ||
|
|
412ea87306 | ||
|
|
774b8661bc | ||
|
|
c7d2619ab1 | ||
|
|
2edfdab6e6 | ||
|
|
4edf9d849f | ||
|
|
10489b4e4a | ||
|
|
40c45f5dd9 | ||
|
|
efec02f153 | ||
|
|
116b8a6363 | ||
|
|
ad892dbc0c | ||
|
|
075d6a1e13 | ||
|
|
54a04e3658 | ||
|
|
462c30e26c | ||
|
|
2a04bdc87a | ||
|
|
ca7ea1aba9 | ||
|
|
f814661fca | ||
|
|
e33c28a6d8 | ||
|
|
e120d09c98 | ||
|
|
4fcbb1f838 | ||
|
|
a855956185 | ||
|
|
5b9ae7981e | ||
|
|
5f22c0189d | ||
|
|
26d26644ac | ||
|
|
3c6503038e | ||
|
|
96e9ed13de | ||
|
|
6df8bd277b | ||
|
|
4e75f0d889 | ||
|
|
a2aeacbfee | ||
|
|
b7370ac8ba | ||
|
|
ccdd5a9576 | ||
|
|
40be4ea239 | ||
|
|
3d47154c20 | ||
|
|
d0a217eb92 | ||
|
|
81c1303cd6 | ||
|
|
4b8e4dca26 | ||
|
|
10cdad3e7d | ||
|
|
d9a1fb134a | ||
|
|
fdea173237 | ||
|
|
4e1bf082ce | ||
|
|
b1c8a702a4 | ||
|
|
820099813f | ||
|
|
2ebe4ff568 | ||
|
|
61bfce5aa9 | ||
|
|
dd7ef0dc41 | ||
|
|
cb42dd8497 | ||
|
|
dcbc1af38a | ||
|
|
81c41d8681 | ||
|
|
ec3be87a2b | ||
|
|
b42c018bb8 | ||
|
|
c9fd6f386c | ||
|
|
1b5d26735e | ||
|
|
4dc0ddc601 | ||
|
|
7a1ca8b0df | ||
|
|
b8791ae79b | ||
|
|
b9a2ceca35 | ||
|
|
a3e53bd442 | ||
|
|
8eb6d171e6 | ||
|
|
5942347407 | ||
|
|
22db947225 | ||
|
|
5d408fd9aa | ||
|
|
47579ec58c | ||
|
|
3363a05539 | ||
|
|
87f10d476c | ||
|
|
70c5eccc12 | ||
|
|
41c3389d76 | ||
|
|
e7a5a188ef | ||
|
|
eb7fc34708 | ||
|
|
91836d577e | ||
|
|
7de0fa698d | ||
|
|
811d895f7b | ||
|
|
71384b2ef9 | ||
|
|
d1d465347a | ||
|
|
5eba129b0f | ||
|
|
021a83ead4 | ||
|
|
5ee45fa571 | ||
|
|
61a06eb328 | ||
|
|
df605f0f8b | ||
|
|
029786442a | ||
|
|
7b42d14f45 | ||
|
|
f34f33c19e | ||
|
|
9d1f8b9d6a | ||
|
|
400616813e | ||
|
|
724be2d5fe | ||
|
|
76da330155 | ||
|
|
ab60aa2266 | ||
|
|
0bbd5789b5 | ||
|
|
fae71b653a | ||
|
|
dfcd2834f9 | ||
|
|
09e86ef90b | ||
|
|
9ba7d526a0 | ||
|
|
94e233e7b2 | ||
|
|
ac53736814 | ||
|
|
8c0e78ae43 | ||
|
|
26ab702304 | ||
|
|
8b58153583 | ||
|
|
8150689b48 | ||
|
|
7ef8482568 | ||
|
|
559fd08845 | ||
|
|
202942a76f | ||
|
|
c3e355388a | ||
|
|
d4c4820c03 | ||
|
|
e05606c8d0 | ||
|
|
161f72cce3 | ||
|
|
8ccb3b96ab | ||
|
|
e9ee249fd5 | ||
|
|
4b6cd1dfdb | ||
|
|
b9ec3b7e7c | ||
|
|
9b247c3d88 | ||
|
|
c7cc806251 | ||
|
|
82b4cf4acb | ||
|
|
3e7a541e09 | ||
|
|
93aafa8549 | ||
|
|
bb85043f46 | ||
|
|
e1fcad34a9 | ||
|
|
155ba8fd3a | ||
|
|
e44f03bc71 | ||
|
|
b61e3daf98 | ||
|
|
6ff084dbbb | ||
|
|
970297a138 | ||
|
|
29abc9438c | ||
|
|
f91284d230 | ||
|
|
feadf7553d | ||
|
|
ea33cdc30b | ||
|
|
579e85f606 | ||
|
|
ea144ba302 | ||
|
|
4f04981dde | ||
|
|
990cea471e | ||
|
|
0913351dcf | ||
|
|
57a794d8eb | ||
|
|
a5e0cf2450 | ||
|
|
a46ba4a8f5 | ||
|
|
c71874b84c | ||
|
|
9aaf3218d2 | ||
|
|
53b2b1b238 | ||
|
|
634b71ed1d | ||
|
|
3d378ed0b4 | ||
|
|
7e0c00a555 | ||
|
|
f0bb2c6d1e | ||
|
|
13bb1ddc7f | ||
|
|
fdb65dcbee | ||
|
|
4e2f2fab73 | ||
|
|
6e186b9c77 | ||
|
|
ff9d344d4c | ||
|
|
b3c44e95a9 | ||
|
|
8c0dd33ce4 | ||
|
|
12874eafa6 | ||
|
|
afb593b44e | ||
|
|
296bfa23aa | ||
|
|
534da4f24f | ||
|
|
62a9da62a6 | ||
|
|
58eea59864 | ||
|
|
c7de92e0df | ||
|
|
c1633eeb0f | ||
|
|
f93f306053 | ||
|
|
e67fc997dc | ||
|
|
3e01a7e677 | ||
|
|
0f92119ceb | ||
|
|
b7d47c2aef | ||
|
|
6270f9ce34 | ||
|
|
e54cc15cbd | ||
|
|
2654f3be82 | ||
|
|
9004151e34 | ||
|
|
6884dd79ba | ||
|
|
f9075577e4 | ||
|
|
50d38d7605 | ||
|
|
aa803153e2 | ||
|
|
f2233c3e25 | ||
|
|
73890c3cac | ||
|
|
e1798d0eb0 | ||
|
|
4f0b638cda | ||
|
|
bb96ba13cf | ||
|
|
5bf4838e2f | ||
|
|
bdf573d140 | ||
|
|
97a48f237d | ||
|
|
6654c30033 | ||
|
|
f49339ca9c | ||
|
|
cb69e35b3b | ||
|
|
994d948c39 | ||
|
|
f5e228ad2e | ||
|
|
92cb451da8 | ||
|
|
55bee84c97 | ||
|
|
a248be4fce | ||
|
|
8b43d6bf9c | ||
|
|
b8adb4d7fa | ||
|
|
4ba33f99fc | ||
|
|
7905739c2a | ||
|
|
6a6a62f13f | ||
|
|
aa8fa71df6 | ||
|
|
7874c6d630 | ||
|
|
7bf0afb1dc | ||
|
|
2f8bfb3d38 | ||
|
|
4115043dc7 | ||
|
|
7062cb764f | ||
|
|
e82021e0e6 | ||
|
|
8925731c98 | ||
|
|
4c233b4f3a | ||
|
|
b7cf758fbe | ||
|
|
7e5691804d | ||
|
|
9891ff80f9 | ||
|
|
7232cdf7e8 | ||
|
|
9f97c8c750 | ||
|
|
edadc93757 | ||
|
|
a6ea7e282f | ||
|
|
e5c0aa6493 | ||
|
|
02744f29ef | ||
|
|
a31d24ee18 | ||
|
|
6957c5b5c6 | ||
|
|
696bcff6bf | ||
|
|
f52ce92f2b | ||
|
|
c80046a77b | ||
|
|
ebf5a06084 | ||
|
|
23e854cb91 | ||
|
|
de105d64d5 | ||
|
|
07d93716aa | ||
|
|
88b5e0b703 | ||
|
|
32fa312b2a | ||
|
|
1306d637a2 | ||
|
|
462b3752e4 | ||
|
|
029f2c4545 | ||
|
|
b3e7beb7c5 | ||
|
|
a549d1ae6b | ||
|
|
467456f7a1 | ||
|
|
2374d63536 | ||
|
|
117eb3b2bc | ||
|
|
de985a30bc | ||
|
|
06d905f415 | ||
|
|
0ad41be0f3 | ||
|
|
d6f5dbff3e | ||
|
|
1e665a0bf4 | ||
|
|
ef16089c6d | ||
|
|
4b4ea1f929 | ||
|
|
45af29912f | ||
|
|
9075c90c46 | ||
|
|
63a2566007 | ||
|
|
43cad817e8 | ||
|
|
ed4c754fff | ||
|
|
1e0c7a0afc | ||
|
|
3a3b556065 | ||
|
|
9244f6b628 | ||
|
|
ff26b72333 | ||
|
|
6803935c4d | ||
|
|
3757546f1b | ||
|
|
a677ac8384 | ||
|
|
bdbfb10cff | ||
|
|
4d661e1183 | ||
|
|
dd28b557ae | ||
|
|
0e0f18ce7c | ||
|
|
7964bfccdb | ||
|
|
3c515f2cd2 | ||
|
|
852acbd738 | ||
|
|
db73ac92d7 | ||
|
|
6913426e48 | ||
|
|
3ba7c1e725 | ||
|
|
9b74a12045 | ||
|
|
4cd82d442e | ||
|
|
74a0cc6a11 | ||
|
|
311871eefc | ||
|
|
a929f7e6ac | ||
|
|
cf51e07bde | ||
|
|
984d2d4cb6 | ||
|
|
8d6c3cd48a | ||
|
|
0244019ca1 | ||
|
|
604654ccb4 | ||
|
|
3817704806 | ||
|
|
d4c6bf770d | ||
|
|
399f9f4a4e | ||
|
|
f2390e2803 | ||
|
|
dbaa6a0e13 | ||
|
|
7bf7ed6123 | ||
|
|
a390abdefb | ||
|
|
c1bc5e14eb | ||
|
|
0efd7e7406 | ||
|
|
4b911fea03 | ||
|
|
e7edb4d1ee | ||
|
|
d235224692 | ||
|
|
1fcdaafa6f | ||
|
|
f24b81e27f | ||
|
|
e01966f7b8 | ||
|
|
dcb9497148 | ||
|
|
4f2513337f | ||
|
|
015269914e | ||
|
|
bbdcfd6baf | ||
|
|
f0d6550f16 | ||
|
|
8400bee3b1 | ||
|
|
bc1f9dc24b | ||
|
|
cdc349a2d1 | ||
|
|
c2c93f8cd6 | ||
|
|
9fc082ea81 | ||
|
|
4c7a9adb98 | ||
|
|
030e5cec58 | ||
|
|
716f4cb11c | ||
|
|
a5a07da892 | ||
|
|
72108b20e2 | ||
|
|
0a678cf377 | ||
|
|
7a77f7b3bb | ||
|
|
767117f9b0 | ||
|
|
fb7f06a752 | ||
|
|
df74e26baf | ||
|
|
d69fa9e1f4 | ||
|
|
0b4006fc47 | ||
|
|
0ccd47f413 | ||
|
|
02f896c12e | ||
|
|
bb4c3831b2 | ||
|
|
3267e4a785 | ||
|
|
89b988cab5 | ||
|
|
4d42a38954 | ||
|
|
8387b3928e | ||
|
|
afa52e4d63 | ||
|
|
8949aad030 | ||
|
|
c0c7af2194 | ||
|
|
f5382ec085 | ||
|
|
407073d7a2 | ||
|
|
7f4375822a | ||
|
|
719ab720a7 | ||
|
|
b11ac88692 | ||
|
|
c727eae441 | ||
|
|
681c33d1f4 | ||
|
|
7f35947d8e | ||
|
|
68941d4dfa | ||
|
|
1d2616b79b | ||
|
|
d4b097a88c | ||
|
|
902c5cf7ca | ||
|
|
b15f790221 | ||
|
|
a47c2e8890 | ||
|
|
d0aad1ac85 | ||
|
|
21b04af524 | ||
|
|
a3202fd51e | ||
|
|
1cceefce33 | ||
|
|
033f970af3 | ||
|
|
d1c3e35d3f | ||
|
|
a6328fc1b1 | ||
|
|
35b868eeca | ||
|
|
3ea02314b9 | ||
|
|
4715161a93 | ||
|
|
144db8ea1d | ||
|
|
bc4202d00b | ||
|
|
09cfc079b0 | ||
|
|
695439775e | ||
|
|
05cd37097c | ||
|
|
08d021916d | ||
|
|
99f24ab0c7 | ||
|
|
3a526e2369 | ||
|
|
bd915d9398 | ||
|
|
8c45cd0e36 | ||
|
|
3fbf1bf35a | ||
|
|
cd9b2ab2f7 | ||
|
|
de397f3bc1 | ||
|
|
72bd73f605 | ||
|
|
1896a154f5 | ||
|
|
1618b68bfa | ||
|
|
c1f201c49a | ||
|
|
8d224ad23b | ||
|
|
e2685ccc81 | ||
|
|
51e3ac2534 | ||
|
|
75aafc932e | ||
|
|
c42092ba7a | ||
|
|
999170d898 | ||
|
|
37430a3401 | ||
|
|
0fa9dd8527 | ||
|
|
489d162477 | ||
|
|
9008ab3407 | ||
|
|
87b96f8d33 | ||
|
|
a49be27145 | ||
|
|
27b2355738 | ||
|
|
eeb5923e89 | ||
|
|
a9067167bb | ||
|
|
a9a0c854e1 | ||
|
|
0c7c188c45 | ||
|
|
c2753fdfb4 | ||
|
|
c29c20ab3c | ||
|
|
6ce806f913 | ||
|
|
35fda84ba8 | ||
|
|
5770d461b2 | ||
|
|
2e0645c26c | ||
|
|
880a5eb25c | ||
|
|
e48d3bfd01 | ||
|
|
5abb3d8150 | ||
|
|
66b1174d25 | ||
|
|
c45a75ad34 | ||
|
|
3567ac3d3e | ||
|
|
43f868de3d | ||
|
|
f41590912d | ||
|
|
183f993b01 | ||
|
|
e53fbb4a09 |
3
.babelrc
3
.babelrc
@@ -22,7 +22,8 @@
|
|||||||
{
|
{
|
||||||
"messagesDir": "./build/messages"
|
"messagesDir": "./build/messages"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"preval"
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
"development": {
|
"development": {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ public/system
|
|||||||
public/assets
|
public/assets
|
||||||
public/packs
|
public/packs
|
||||||
node_modules
|
node_modules
|
||||||
storybook
|
|
||||||
neo4j
|
neo4j
|
||||||
vendor/bundle
|
vendor/bundle
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -26,11 +26,22 @@ LOCAL_HTTPS=true
|
|||||||
# ALTERNATE_DOMAINS=example1.com,example2.com
|
# ALTERNATE_DOMAINS=example1.com,example2.com
|
||||||
|
|
||||||
# Application secrets
|
# Application secrets
|
||||||
# Generate each with the `rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)
|
# Generate each with the `RAILS_ENV=production bundle exec rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)
|
||||||
PAPERCLIP_SECRET=
|
PAPERCLIP_SECRET=
|
||||||
SECRET_KEY_BASE=
|
SECRET_KEY_BASE=
|
||||||
OTP_SECRET=
|
OTP_SECRET=
|
||||||
|
|
||||||
|
# VAPID keys (used for push notifications
|
||||||
|
# You can generate the keys using the following command (first is the private key, second is the public one)
|
||||||
|
# You should only generate this once per instance. If you later decide to change it, all push subscription will
|
||||||
|
# be invalidated, requiring the users to access the website again to resubscribe.
|
||||||
|
#
|
||||||
|
# Generate with `RAILS_ENV=production bundle exec rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web rake mastodon:webpush:generate_vapid_key` if you use docker compose)
|
||||||
|
#
|
||||||
|
# For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html
|
||||||
|
VAPID_PRIVATE_KEY=
|
||||||
|
VAPID_PUBLIC_KEY=
|
||||||
|
|
||||||
# Registrations
|
# Registrations
|
||||||
# Single user mode will disable registrations and redirect frontpage to the first profile
|
# Single user mode will disable registrations and redirect frontpage to the first profile
|
||||||
# SINGLE_USER_MODE=true
|
# SINGLE_USER_MODE=true
|
||||||
@@ -58,7 +69,7 @@ SMTP_FROM_ADDRESS=notifications@example.com
|
|||||||
#SMTP_CA_FILE=/etc/ssl/certs/ca-certificates.crt
|
#SMTP_CA_FILE=/etc/ssl/certs/ca-certificates.crt
|
||||||
#SMTP_OPENSSL_VERIFY_MODE=peer
|
#SMTP_OPENSSL_VERIFY_MODE=peer
|
||||||
#SMTP_ENABLE_STARTTLS_AUTO=true
|
#SMTP_ENABLE_STARTTLS_AUTO=true
|
||||||
|
#SMTP_TLS=true
|
||||||
|
|
||||||
# Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files.
|
# Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files.
|
||||||
# PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system
|
# PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system
|
||||||
@@ -87,6 +98,15 @@ SMTP_FROM_ADDRESS=notifications@example.com
|
|||||||
# S3_ENDPOINT=
|
# S3_ENDPOINT=
|
||||||
# S3_SIGNATURE_VERSION=
|
# S3_SIGNATURE_VERSION=
|
||||||
|
|
||||||
|
# Swift (optional)
|
||||||
|
# SWIFT_ENABLED=true
|
||||||
|
# SWIFT_USERNAME=
|
||||||
|
# SWIFT_TENANT=
|
||||||
|
# SWIFT_PASSWORD=
|
||||||
|
# SWIFT_AUTH_URL=
|
||||||
|
# SWIFT_CONTAINER=
|
||||||
|
# SWIFT_OBJECT_URL=
|
||||||
|
|
||||||
# Optional alias for S3 if you want to use Cloudfront or Cloudflare in front
|
# Optional alias for S3 if you want to use Cloudfront or Cloudflare in front
|
||||||
# S3_CLOUDFRONT_HOST=
|
# S3_CLOUDFRONT_HOST=
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ rules:
|
|||||||
- warn
|
- warn
|
||||||
- allow:
|
- allow:
|
||||||
- error
|
- error
|
||||||
|
- warn
|
||||||
no-fallthrough: error
|
no-fallthrough: error
|
||||||
no-irregular-whitespace: error
|
no-irregular-whitespace: error
|
||||||
no-mixed-spaces-and-tabs: warn
|
no-mixed-spaces-and-tabs: warn
|
||||||
@@ -112,7 +113,7 @@ rules:
|
|||||||
jsx-a11y/iframe-has-title: warn
|
jsx-a11y/iframe-has-title: warn
|
||||||
jsx-a11y/img-has-alt: warn
|
jsx-a11y/img-has-alt: warn
|
||||||
jsx-a11y/img-redundant-alt: warn
|
jsx-a11y/img-redundant-alt: warn
|
||||||
jsx-a11y/label-has-for: warn
|
jsx-a11y/label-has-for: off
|
||||||
jsx-a11y/mouse-events-have-key-events: warn
|
jsx-a11y/mouse-events-have-key-events: warn
|
||||||
jsx-a11y/no-access-key: warn
|
jsx-a11y/no-access-key: warn
|
||||||
jsx-a11y/no-distracting-elements: warn
|
jsx-a11y/no-distracting-elements: warn
|
||||||
@@ -121,6 +122,6 @@ rules:
|
|||||||
jsx-a11y/onclick-has-focus: warn
|
jsx-a11y/onclick-has-focus: warn
|
||||||
jsx-a11y/onclick-has-role: warn
|
jsx-a11y/onclick-has-role: warn
|
||||||
jsx-a11y/role-has-required-aria-props: warn
|
jsx-a11y/role-has-required-aria-props: warn
|
||||||
jsx-a11y/role-supports-aria-props: warn
|
jsx-a11y/role-supports-aria-props: off
|
||||||
jsx-a11y/scope: warn
|
jsx-a11y/scope: warn
|
||||||
jsx-a11y/tabindex-no-positive: warn
|
jsx-a11y/tabindex-no-positive: warn
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ node_modules/
|
|||||||
public/assets/
|
public/assets/
|
||||||
public/system/
|
public/system/
|
||||||
spec/
|
spec/
|
||||||
storybook/
|
|
||||||
tmp/
|
tmp/
|
||||||
.vagrant/
|
.vagrant/
|
||||||
vendor/bundle/
|
vendor/bundle/
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ plugins:
|
|||||||
- last 2 versions
|
- last 2 versions
|
||||||
- IE >= 11
|
- IE >= 11
|
||||||
- iOS >= 9
|
- iOS >= 9
|
||||||
|
postcss-object-fit-images: {}
|
||||||
|
|||||||
10
.rubocop.yml
10
.rubocop.yml
@@ -10,6 +10,7 @@ AllCops:
|
|||||||
- 'node_modules/**/*'
|
- 'node_modules/**/*'
|
||||||
- 'Vagrantfile'
|
- 'Vagrantfile'
|
||||||
- 'vendor/**/*'
|
- 'vendor/**/*'
|
||||||
|
- 'lib/json_ld/*'
|
||||||
|
|
||||||
Bundler/OrderedGems:
|
Bundler/OrderedGems:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
@@ -27,6 +28,7 @@ Metrics/AbcSize:
|
|||||||
Max: 100
|
Max: 100
|
||||||
|
|
||||||
Metrics/BlockLength:
|
Metrics/BlockLength:
|
||||||
|
Max: 35
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'lib/tasks/**/*'
|
- 'lib/tasks/**/*'
|
||||||
|
|
||||||
@@ -35,10 +37,10 @@ Metrics/BlockNesting:
|
|||||||
|
|
||||||
Metrics/ClassLength:
|
Metrics/ClassLength:
|
||||||
CountComments: false
|
CountComments: false
|
||||||
Max: 200
|
Max: 300
|
||||||
|
|
||||||
Metrics/CyclomaticComplexity:
|
Metrics/CyclomaticComplexity:
|
||||||
Max: 15
|
Max: 25
|
||||||
|
|
||||||
Metrics/LineLength:
|
Metrics/LineLength:
|
||||||
AllowURI: true
|
AllowURI: true
|
||||||
@@ -53,11 +55,11 @@ Metrics/ModuleLength:
|
|||||||
Max: 200
|
Max: 200
|
||||||
|
|
||||||
Metrics/ParameterLists:
|
Metrics/ParameterLists:
|
||||||
Max: 4
|
Max: 5
|
||||||
CountKeywordArgs: true
|
CountKeywordArgs: true
|
||||||
|
|
||||||
Metrics/PerceivedComplexity:
|
Metrics/PerceivedComplexity:
|
||||||
Max: 10
|
Max: 20
|
||||||
|
|
||||||
Rails:
|
Rails:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
|
|||||||
@@ -2,4 +2,3 @@ node_modules/
|
|||||||
.cache/
|
.cache/
|
||||||
docs/
|
docs/
|
||||||
spec/
|
spec/
|
||||||
storybook/
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ cache:
|
|||||||
- node_modules
|
- node_modules
|
||||||
- public/assets
|
- public/assets
|
||||||
- public/packs-test
|
- public/packs-test
|
||||||
|
- tmp/cache/babel-loader
|
||||||
dist: trusty
|
dist: trusty
|
||||||
sudo: required
|
sudo: required
|
||||||
|
|
||||||
|
|||||||
9
Aptfile
9
Aptfile
@@ -1,6 +1,9 @@
|
|||||||
protobuf-compiler
|
|
||||||
libprotobuf-dev
|
|
||||||
ffmpeg
|
ffmpeg
|
||||||
|
libicu-dev
|
||||||
|
libidn11
|
||||||
|
libidn11-dev
|
||||||
|
libpq-dev
|
||||||
|
libprotobuf-dev
|
||||||
libxdamage1
|
libxdamage1
|
||||||
libxfixes3
|
libxfixes3
|
||||||
libicu-dev
|
protobuf-compiler
|
||||||
|
|||||||
15
CODEOWNERS
Normal file
15
CODEOWNERS
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# CODEOWNERS for tootsuite/mastodon
|
||||||
|
|
||||||
|
# Translators
|
||||||
|
# To add translator, copy these lines, replace `fr` with appropriate language code and replace `@żelipapą` with user's GitHub nickname preceded by `@` sign or e-mail address.
|
||||||
|
# /app/javascript/mastodon/locales/fr.json @żelipapą
|
||||||
|
# /app/views/user_mailer/*.fr.html.erb @żelipapą
|
||||||
|
# /app/views/user_mailer/*.fr.text.erb @żelipapą
|
||||||
|
# /config/locales/*.fr.yml @żelipapą
|
||||||
|
# /config/locales/fr.yml @żelipapą
|
||||||
|
|
||||||
|
/app/javascript/mastodon/locales/pl.json @m4sk1n
|
||||||
|
/app/views/user_mailer/*.pl.html.erb @m4sk1n
|
||||||
|
/app/views/user_mailer/*.pl.text.erb @m4sk1n
|
||||||
|
/config/locales/*.pl.yml @m4sk1n
|
||||||
|
/config/locales/pl.yml @m4sk1n
|
||||||
@@ -1,3 +1,36 @@
|
|||||||
|
# Contributing to Mastodon Glitch Edition #
|
||||||
|
|
||||||
|
Thank you for your interest in contributing to the `glitch-soc` project!
|
||||||
|
Here are some guidelines, and ways you can help.
|
||||||
|
|
||||||
|
> (This document is a bit of a work-in-progress, so please bear with us.
|
||||||
|
> If you don't see what you're looking for here, please don't hesitate to reach out!)
|
||||||
|
|
||||||
|
## Planning ##
|
||||||
|
|
||||||
|
Right now a lot of the planning for this project takes place in our development Discord, or through GitHub Issues and Projects.
|
||||||
|
We're working on ways to improve the planning structure and better solicit feedback, and if you feel like you can help in this respect, feel free to give us a holler.
|
||||||
|
|
||||||
|
## Documentation ##
|
||||||
|
|
||||||
|
The documentation for this repository is available at [`glitch-soc/docs`](https://github.com/glitch-soc/docs) (online at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/)).
|
||||||
|
Right now, we've mostly focused on the features that make this fork different from upstream in some manner.
|
||||||
|
Adding screenshots, improving descriptions, and so forth are all ways to help contribute to the project even if you don't know any code.
|
||||||
|
|
||||||
|
## Frontend Development ##
|
||||||
|
|
||||||
|
Check out [the documentation here](https://glitch-soc.github.io/docs/contributing/frontend/) for more information.
|
||||||
|
|
||||||
|
## Backend Development ##
|
||||||
|
|
||||||
|
See the guidelines below.
|
||||||
|
|
||||||
|
- - -
|
||||||
|
|
||||||
|
You should also try to follow the guidelines set out in the original `CONTRIBUTING.md` from `tootsuite/mastodon`, reproduced below.
|
||||||
|
|
||||||
|
<blockquote>
|
||||||
|
|
||||||
CONTRIBUTING
|
CONTRIBUTING
|
||||||
============
|
============
|
||||||
|
|
||||||
@@ -49,3 +82,5 @@ It is expected that you have a working development environment set up (see back-
|
|||||||
* If you are introducing new strings, they must be using localization methods
|
* If you are introducing new strings, they must be using localization methods
|
||||||
|
|
||||||
If the JavaScript or CSS assets won't compile due to a syntax error, it's a good sign that the pull request isn't ready for submission yet.
|
If the JavaScript or CSS assets won't compile due to a syntax error, it's a good sign that the pull request isn't ready for submission yet.
|
||||||
|
|
||||||
|
</blockquote>
|
||||||
|
|||||||
40
Dockerfile
40
Dockerfile
@@ -1,4 +1,4 @@
|
|||||||
FROM ruby:2.4.1-alpine
|
FROM ruby:2.4.1-alpine3.6
|
||||||
|
|
||||||
LABEL maintainer="https://github.com/tootsuite/mastodon" \
|
LABEL maintainer="https://github.com/tootsuite/mastodon" \
|
||||||
description="A GNU Social-compatible microblogging server"
|
description="A GNU Social-compatible microblogging server"
|
||||||
@@ -7,16 +7,19 @@ ENV UID=991 GID=991 \
|
|||||||
RAILS_SERVE_STATIC_FILES=true \
|
RAILS_SERVE_STATIC_FILES=true \
|
||||||
RAILS_ENV=production NODE_ENV=production
|
RAILS_ENV=production NODE_ENV=production
|
||||||
|
|
||||||
|
ARG LIBICONV_VERSION=1.15
|
||||||
|
ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178
|
||||||
|
|
||||||
EXPOSE 3000 4000
|
EXPOSE 3000 4000
|
||||||
|
|
||||||
WORKDIR /mastodon
|
WORKDIR /mastodon
|
||||||
|
|
||||||
RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories \
|
RUN apk -U upgrade \
|
||||||
&& apk -U upgrade \
|
|
||||||
&& apk add -t build-dependencies \
|
&& apk add -t build-dependencies \
|
||||||
build-base \
|
build-base \
|
||||||
libxml2-dev \
|
icu-dev \
|
||||||
libxslt-dev \
|
libidn-dev \
|
||||||
|
libtool \
|
||||||
postgresql-dev \
|
postgresql-dev \
|
||||||
protobuf-dev \
|
protobuf-dev \
|
||||||
python \
|
python \
|
||||||
@@ -25,23 +28,34 @@ RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/reposit
|
|||||||
ffmpeg \
|
ffmpeg \
|
||||||
file \
|
file \
|
||||||
git \
|
git \
|
||||||
icu-dev \
|
icu-libs \
|
||||||
imagemagick@edge \
|
imagemagick \
|
||||||
|
libidn \
|
||||||
libpq \
|
libpq \
|
||||||
libxml2 \
|
nodejs-npm \
|
||||||
libxslt \
|
nodejs \
|
||||||
nodejs-npm@edge \
|
|
||||||
nodejs@edge \
|
|
||||||
protobuf \
|
protobuf \
|
||||||
su-exec \
|
su-exec \
|
||||||
tini \
|
tini \
|
||||||
&& npm install -g npm@3 && npm install -g yarn \
|
yarn \
|
||||||
&& update-ca-certificates \
|
&& update-ca-certificates \
|
||||||
|
&& wget -O libiconv.tar.gz "http://ftp.gnu.org/pub/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz" \
|
||||||
|
&& echo "$LIBICONV_DOWNLOAD_SHA256 *libiconv.tar.gz" | sha256sum -c - \
|
||||||
|
&& mkdir -p /tmp/src \
|
||||||
|
&& tar -xzf libiconv.tar.gz -C /tmp/src \
|
||||||
|
&& rm libiconv.tar.gz \
|
||||||
|
&& cd /tmp/src/libiconv-$LIBICONV_VERSION \
|
||||||
|
&& ./configure --prefix=/usr/local \
|
||||||
|
&& make -j$(getconf _NPROCESSORS_ONLN)\
|
||||||
|
&& make install \
|
||||||
|
&& libtool --finish /usr/local/lib \
|
||||||
|
&& cd /mastodon \
|
||||||
&& rm -rf /tmp/* /var/cache/apk/*
|
&& rm -rf /tmp/* /var/cache/apk/*
|
||||||
|
|
||||||
COPY Gemfile Gemfile.lock package.json yarn.lock /mastodon/
|
COPY Gemfile Gemfile.lock package.json yarn.lock /mastodon/
|
||||||
|
|
||||||
RUN bundle install --deployment --without test development \
|
RUN bundle config build.nokogiri --with-iconv-lib=/usr/local/lib --with-iconv-include=/usr/local/include \
|
||||||
|
&& bundle install -j$(getconf _NPROCESSORS_ONLN) --deployment --without test development \
|
||||||
&& yarn --ignore-optional --pure-lockfile
|
&& yarn --ignore-optional --pure-lockfile
|
||||||
|
|
||||||
COPY . /mastodon
|
COPY . /mastodon
|
||||||
|
|||||||
14
Gemfile
14
Gemfile
@@ -15,6 +15,7 @@ gem 'pghero', '~> 1.7'
|
|||||||
gem 'dotenv-rails', '~> 2.2'
|
gem 'dotenv-rails', '~> 2.2'
|
||||||
|
|
||||||
gem 'aws-sdk', '~> 2.9'
|
gem 'aws-sdk', '~> 2.9'
|
||||||
|
gem 'fog-openstack', '~> 0.1'
|
||||||
gem 'paperclip', '~> 5.1'
|
gem 'paperclip', '~> 5.1'
|
||||||
gem 'paperclip-av-transcoder', '~> 0.6'
|
gem 'paperclip-av-transcoder', '~> 0.6'
|
||||||
|
|
||||||
@@ -22,19 +23,21 @@ gem 'active_model_serializers', '~> 0.10'
|
|||||||
gem 'addressable', '~> 2.5'
|
gem 'addressable', '~> 2.5'
|
||||||
gem 'bootsnap'
|
gem 'bootsnap'
|
||||||
gem 'browser'
|
gem 'browser'
|
||||||
gem 'charlock_holmes', '~> 0.7.3'
|
gem 'charlock_holmes', '~> 0.7.5'
|
||||||
|
gem 'iso-639'
|
||||||
gem 'cld3', '~> 3.1'
|
gem 'cld3', '~> 3.1'
|
||||||
gem 'devise', '~> 4.2'
|
gem 'devise', '~> 4.2'
|
||||||
gem 'devise-two-factor', '~> 3.0'
|
gem 'devise-two-factor', '~> 3.0'
|
||||||
gem 'doorkeeper', '~> 4.2'
|
gem 'doorkeeper', '~> 4.2'
|
||||||
gem 'fast_blank', '~> 1.0'
|
gem 'fast_blank', '~> 1.0'
|
||||||
gem 'goldfinger', '~> 1.2'
|
gem 'goldfinger', '~> 2.0'
|
||||||
gem 'hiredis', '~> 0.6'
|
gem 'hiredis', '~> 0.6'
|
||||||
gem 'redis-namespace', '~> 1.5'
|
gem 'redis-namespace', '~> 1.5'
|
||||||
gem 'htmlentities', '~> 4.3'
|
gem 'htmlentities', '~> 4.3'
|
||||||
gem 'http', '~> 2.2'
|
gem 'http', '~> 2.2'
|
||||||
gem 'http_accept_language', '~> 2.1'
|
gem 'http_accept_language', '~> 2.1'
|
||||||
gem 'httplog', '~> 0.99'
|
gem 'httplog', '~> 0.99'
|
||||||
|
gem 'idn-ruby', require: 'idn'
|
||||||
gem 'kaminari', '~> 1.0'
|
gem 'kaminari', '~> 1.0'
|
||||||
gem 'link_header', '~> 0.0'
|
gem 'link_header', '~> 0.0'
|
||||||
gem 'mime-types', '~> 3.1'
|
gem 'mime-types', '~> 3.1'
|
||||||
@@ -50,6 +53,7 @@ gem 'rack-timeout', '~> 0.4'
|
|||||||
gem 'rails-i18n', '~> 5.0'
|
gem 'rails-i18n', '~> 5.0'
|
||||||
gem 'rails-settings-cached', '~> 0.6'
|
gem 'rails-settings-cached', '~> 0.6'
|
||||||
gem 'redis', '~> 3.3', require: ['redis', 'redis/connection/hiredis']
|
gem 'redis', '~> 3.3', require: ['redis', 'redis/connection/hiredis']
|
||||||
|
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
||||||
gem 'rqrcode', '~> 0.10'
|
gem 'rqrcode', '~> 0.10'
|
||||||
gem 'ruby-oembed', '~> 0.12', require: 'oembed'
|
gem 'ruby-oembed', '~> 0.12', require: 'oembed'
|
||||||
gem 'sanitize', '~> 4.4'
|
gem 'sanitize', '~> 4.4'
|
||||||
@@ -64,6 +68,10 @@ gem 'statsd-instrument', '~> 2.1'
|
|||||||
gem 'twitter-text', '~> 1.14'
|
gem 'twitter-text', '~> 1.14'
|
||||||
gem 'tzinfo-data', '~> 1.2017'
|
gem 'tzinfo-data', '~> 1.2017'
|
||||||
gem 'webpacker', '~> 2.0'
|
gem 'webpacker', '~> 2.0'
|
||||||
|
gem 'webpush'
|
||||||
|
|
||||||
|
gem 'json-ld-preloaded', '~> 2.2.1'
|
||||||
|
gem 'rdf-normalize', '~> 0.3.1'
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem 'fabrication', '~> 2.16'
|
gem 'fabrication', '~> 2.16'
|
||||||
@@ -77,7 +85,7 @@ group :test do
|
|||||||
gem 'capybara', '~> 2.14'
|
gem 'capybara', '~> 2.14'
|
||||||
gem 'climate_control', '~> 0.2'
|
gem 'climate_control', '~> 0.2'
|
||||||
gem 'faker', '~> 1.7'
|
gem 'faker', '~> 1.7'
|
||||||
gem 'microformats2', '~> 3.0'
|
gem 'microformats', '~> 4.0'
|
||||||
gem 'rails-controller-testing', '~> 1.0'
|
gem 'rails-controller-testing', '~> 1.0'
|
||||||
gem 'rspec-sidekiq', '~> 3.0'
|
gem 'rspec-sidekiq', '~> 3.0'
|
||||||
gem 'simplecov', '~> 0.14', require: false
|
gem 'simplecov', '~> 0.14', require: false
|
||||||
|
|||||||
193
Gemfile.lock
193
Gemfile.lock
@@ -1,25 +1,25 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (5.1.2)
|
actioncable (5.1.3)
|
||||||
actionpack (= 5.1.2)
|
actionpack (= 5.1.3)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (~> 0.6.1)
|
websocket-driver (~> 0.6.1)
|
||||||
actionmailer (5.1.2)
|
actionmailer (5.1.3)
|
||||||
actionpack (= 5.1.2)
|
actionpack (= 5.1.3)
|
||||||
actionview (= 5.1.2)
|
actionview (= 5.1.3)
|
||||||
activejob (= 5.1.2)
|
activejob (= 5.1.3)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (5.1.2)
|
actionpack (5.1.3)
|
||||||
actionview (= 5.1.2)
|
actionview (= 5.1.3)
|
||||||
activesupport (= 5.1.2)
|
activesupport (= 5.1.3)
|
||||||
rack (~> 2.0)
|
rack (~> 2.0)
|
||||||
rack-test (~> 0.6.3)
|
rack-test (~> 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||||
actionview (5.1.2)
|
actionview (5.1.3)
|
||||||
activesupport (= 5.1.2)
|
activesupport (= 5.1.3)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
@@ -30,22 +30,22 @@ GEM
|
|||||||
case_transform (>= 0.2)
|
case_transform (>= 0.2)
|
||||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.2)
|
jsonapi-renderer (>= 0.1.1.beta1, < 0.2)
|
||||||
active_record_query_trace (1.5.4)
|
active_record_query_trace (1.5.4)
|
||||||
activejob (5.1.2)
|
activejob (5.1.3)
|
||||||
activesupport (= 5.1.2)
|
activesupport (= 5.1.3)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (5.1.2)
|
activemodel (5.1.3)
|
||||||
activesupport (= 5.1.2)
|
activesupport (= 5.1.3)
|
||||||
activerecord (5.1.2)
|
activerecord (5.1.3)
|
||||||
activemodel (= 5.1.2)
|
activemodel (= 5.1.3)
|
||||||
activesupport (= 5.1.2)
|
activesupport (= 5.1.3)
|
||||||
arel (~> 8.0)
|
arel (~> 8.0)
|
||||||
activesupport (5.1.2)
|
activesupport (5.1.3)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (~> 0.7)
|
i18n (~> 0.7)
|
||||||
minitest (~> 5.1)
|
minitest (~> 5.1)
|
||||||
tzinfo (~> 1.1)
|
tzinfo (~> 1.1)
|
||||||
addressable (2.5.1)
|
addressable (2.5.2)
|
||||||
public_suffix (~> 2.0, >= 2.0.2)
|
public_suffix (>= 2.0.2, < 4.0)
|
||||||
airbrussh (1.3.0)
|
airbrussh (1.3.0)
|
||||||
sshkit (>= 1.6.1, != 1.7.0)
|
sshkit (>= 1.6.1, != 1.7.0)
|
||||||
annotate (2.7.2)
|
annotate (2.7.2)
|
||||||
@@ -57,14 +57,14 @@ GEM
|
|||||||
encryptor (~> 3.0.0)
|
encryptor (~> 3.0.0)
|
||||||
av (0.9.0)
|
av (0.9.0)
|
||||||
cocaine (~> 0.5.3)
|
cocaine (~> 0.5.3)
|
||||||
aws-sdk (2.10.6)
|
aws-sdk (2.10.21)
|
||||||
aws-sdk-resources (= 2.10.6)
|
aws-sdk-resources (= 2.10.21)
|
||||||
aws-sdk-core (2.10.6)
|
aws-sdk-core (2.10.21)
|
||||||
aws-sigv4 (~> 1.0)
|
aws-sigv4 (~> 1.0)
|
||||||
jmespath (~> 1.0)
|
jmespath (~> 1.0)
|
||||||
aws-sdk-resources (2.10.6)
|
aws-sdk-resources (2.10.21)
|
||||||
aws-sdk-core (= 2.10.6)
|
aws-sdk-core (= 2.10.21)
|
||||||
aws-sigv4 (1.0.0)
|
aws-sigv4 (1.0.1)
|
||||||
bcrypt (3.1.11)
|
bcrypt (3.1.11)
|
||||||
better_errors (2.1.1)
|
better_errors (2.1.1)
|
||||||
coderay (>= 1.0.0)
|
coderay (>= 1.0.0)
|
||||||
@@ -72,15 +72,15 @@ GEM
|
|||||||
rack (>= 0.9.0)
|
rack (>= 0.9.0)
|
||||||
binding_of_caller (0.7.2)
|
binding_of_caller (0.7.2)
|
||||||
debug_inspector (>= 0.0.1)
|
debug_inspector (>= 0.0.1)
|
||||||
bootsnap (1.1.1)
|
bootsnap (1.1.2)
|
||||||
msgpack (~> 1.0)
|
msgpack (~> 1.0)
|
||||||
brakeman (3.6.2)
|
brakeman (3.7.2)
|
||||||
browser (2.4.0)
|
browser (2.4.0)
|
||||||
builder (3.2.3)
|
builder (3.2.3)
|
||||||
bullet (5.5.1)
|
bullet (5.5.1)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
uniform_notifier (~> 1.10.0)
|
uniform_notifier (~> 1.10.0)
|
||||||
bundler-audit (0.5.0)
|
bundler-audit (0.6.0)
|
||||||
bundler (~> 1.2)
|
bundler (~> 1.2)
|
||||||
thor (~> 0.18)
|
thor (~> 0.18)
|
||||||
capistrano (3.8.2)
|
capistrano (3.8.2)
|
||||||
@@ -108,7 +108,7 @@ GEM
|
|||||||
xpath (~> 2.0)
|
xpath (~> 2.0)
|
||||||
case_transform (0.2)
|
case_transform (0.2)
|
||||||
activesupport
|
activesupport
|
||||||
charlock_holmes (0.7.3)
|
charlock_holmes (0.7.5)
|
||||||
chunky_png (1.3.8)
|
chunky_png (1.3.8)
|
||||||
cld3 (3.1.3)
|
cld3 (3.1.3)
|
||||||
ffi (>= 1.1.0, < 1.10.0)
|
ffi (>= 1.1.0, < 1.10.0)
|
||||||
@@ -154,21 +154,35 @@ GEM
|
|||||||
erubis (2.7.0)
|
erubis (2.7.0)
|
||||||
et-orbi (1.0.5)
|
et-orbi (1.0.5)
|
||||||
tzinfo
|
tzinfo
|
||||||
|
excon (0.58.0)
|
||||||
execjs (2.7.0)
|
execjs (2.7.0)
|
||||||
fabrication (2.16.1)
|
fabrication (2.16.2)
|
||||||
faker (1.7.3)
|
faker (1.7.3)
|
||||||
i18n (~> 0.5)
|
i18n (~> 0.5)
|
||||||
fast_blank (1.0.0)
|
fast_blank (1.0.0)
|
||||||
ffi (1.9.18)
|
ffi (1.9.18)
|
||||||
|
fog-core (1.45.0)
|
||||||
|
builder
|
||||||
|
excon (~> 0.58)
|
||||||
|
formatador (~> 0.2)
|
||||||
|
fog-json (1.0.2)
|
||||||
|
fog-core (~> 1.0)
|
||||||
|
multi_json (~> 1.10)
|
||||||
|
fog-openstack (0.1.21)
|
||||||
|
fog-core (>= 1.40)
|
||||||
|
fog-json (>= 1.0)
|
||||||
|
ipaddress (>= 0.8)
|
||||||
|
formatador (0.2.5)
|
||||||
fuubar (2.2.0)
|
fuubar (2.2.0)
|
||||||
rspec-core (~> 3.0)
|
rspec-core (~> 3.0)
|
||||||
ruby-progressbar (~> 1.4)
|
ruby-progressbar (~> 1.4)
|
||||||
globalid (0.4.0)
|
globalid (0.4.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
goldfinger (1.2.0)
|
goldfinger (2.0.1)
|
||||||
addressable (~> 2.4)
|
addressable (~> 2.5)
|
||||||
http (~> 2.0)
|
http (~> 2.2)
|
||||||
nokogiri (~> 1.6)
|
nokogiri (~> 1.8)
|
||||||
|
oj (~> 3.0)
|
||||||
hamlit (2.8.4)
|
hamlit (2.8.4)
|
||||||
temple (>= 0.8.0)
|
temple (>= 0.8.0)
|
||||||
thor
|
thor
|
||||||
@@ -178,9 +192,12 @@ GEM
|
|||||||
activesupport (>= 4.0.1)
|
activesupport (>= 4.0.1)
|
||||||
hamlit (>= 1.2.0)
|
hamlit (>= 1.2.0)
|
||||||
railties (>= 4.0.1)
|
railties (>= 4.0.1)
|
||||||
hashdiff (0.3.4)
|
hamster (3.0.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
|
hashdiff (0.3.5)
|
||||||
highline (1.7.8)
|
highline (1.7.8)
|
||||||
hiredis (0.6.1)
|
hiredis (0.6.1)
|
||||||
|
hkdf (0.3.0)
|
||||||
htmlentities (4.3.4)
|
htmlentities (4.3.4)
|
||||||
http (2.2.2)
|
http (2.2.2)
|
||||||
addressable (~> 2.3)
|
addressable (~> 2.3)
|
||||||
@@ -192,11 +209,11 @@ GEM
|
|||||||
http-form_data (1.0.3)
|
http-form_data (1.0.3)
|
||||||
http_accept_language (2.1.1)
|
http_accept_language (2.1.1)
|
||||||
http_parser.rb (0.6.0)
|
http_parser.rb (0.6.0)
|
||||||
httplog (0.99.4)
|
httplog (0.99.7)
|
||||||
colorize
|
colorize
|
||||||
rack
|
rack
|
||||||
i18n (0.8.4)
|
i18n (0.8.6)
|
||||||
i18n-tasks (0.9.15)
|
i18n-tasks (0.9.16)
|
||||||
activesupport (>= 4.0.2)
|
activesupport (>= 4.0.2)
|
||||||
ast (>= 2.1.0)
|
ast (>= 2.1.0)
|
||||||
easy_translate (>= 0.5.0)
|
easy_translate (>= 0.5.0)
|
||||||
@@ -206,9 +223,20 @@ GEM
|
|||||||
parser (>= 2.2.3.0)
|
parser (>= 2.2.3.0)
|
||||||
rainbow (~> 2.2)
|
rainbow (~> 2.2)
|
||||||
terminal-table (>= 1.5.1)
|
terminal-table (>= 1.5.1)
|
||||||
|
idn-ruby (0.1.0)
|
||||||
|
ipaddress (0.8.3)
|
||||||
|
iso-639 (0.2.8)
|
||||||
jmespath (1.3.1)
|
jmespath (1.3.1)
|
||||||
json (2.1.0)
|
json (2.1.0)
|
||||||
jsonapi-renderer (0.1.2)
|
json-ld (2.1.5)
|
||||||
|
multi_json (~> 1.12)
|
||||||
|
rdf (~> 2.2)
|
||||||
|
json-ld-preloaded (2.2.1)
|
||||||
|
json-ld (~> 2.1, >= 2.1.5)
|
||||||
|
multi_json (~> 1.11)
|
||||||
|
rdf (~> 2.2)
|
||||||
|
jsonapi-renderer (0.1.3)
|
||||||
|
jwt (1.5.6)
|
||||||
kaminari (1.0.1)
|
kaminari (1.0.1)
|
||||||
activesupport (>= 4.1.0)
|
activesupport (>= 4.1.0)
|
||||||
kaminari-actionview (= 1.0.1)
|
kaminari-actionview (= 1.0.1)
|
||||||
@@ -238,8 +266,10 @@ GEM
|
|||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
mail (2.6.6)
|
mail (2.6.6)
|
||||||
mime-types (>= 1.16, < 4)
|
mime-types (>= 1.16, < 4)
|
||||||
|
mario-redis-lock (1.2.0)
|
||||||
|
redis (~> 3, >= 3.0.5)
|
||||||
method_source (0.8.2)
|
method_source (0.8.2)
|
||||||
microformats2 (3.1.0)
|
microformats (4.0.7)
|
||||||
json
|
json
|
||||||
nokogiri
|
nokogiri
|
||||||
mime-types (3.1)
|
mime-types (3.1)
|
||||||
@@ -247,7 +277,7 @@ GEM
|
|||||||
mime-types-data (3.2016.0521)
|
mime-types-data (3.2016.0521)
|
||||||
mimemagic (0.3.2)
|
mimemagic (0.3.2)
|
||||||
mini_portile2 (2.2.0)
|
mini_portile2 (2.2.0)
|
||||||
minitest (5.10.2)
|
minitest (5.10.3)
|
||||||
msgpack (1.1.0)
|
msgpack (1.1.0)
|
||||||
multi_json (1.12.1)
|
multi_json (1.12.1)
|
||||||
net-scp (1.2.1)
|
net-scp (1.2.1)
|
||||||
@@ -258,7 +288,7 @@ GEM
|
|||||||
mini_portile2 (~> 2.2.0)
|
mini_portile2 (~> 2.2.0)
|
||||||
nokogumbo (1.4.13)
|
nokogumbo (1.4.13)
|
||||||
nokogiri
|
nokogiri
|
||||||
oj (3.2.0)
|
oj (3.3.4)
|
||||||
openssl (2.0.4)
|
openssl (2.0.4)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ostatus2 (2.0.1)
|
ostatus2 (2.0.1)
|
||||||
@@ -277,14 +307,14 @@ GEM
|
|||||||
av (~> 0.9.0)
|
av (~> 0.9.0)
|
||||||
paperclip (>= 2.5.2)
|
paperclip (>= 2.5.2)
|
||||||
parallel (1.11.2)
|
parallel (1.11.2)
|
||||||
parallel_tests (2.14.1)
|
parallel_tests (2.14.2)
|
||||||
parallel
|
parallel
|
||||||
parser (2.4.0.0)
|
parser (2.4.0.0)
|
||||||
ast (~> 2.2)
|
ast (~> 2.2)
|
||||||
pg (0.21.0)
|
pg (0.21.0)
|
||||||
pghero (1.7.0)
|
pghero (1.7.0)
|
||||||
activerecord
|
activerecord
|
||||||
pkg-config (1.2.3)
|
pkg-config (1.2.4)
|
||||||
powerpack (0.1.1)
|
powerpack (0.1.1)
|
||||||
pry (0.10.4)
|
pry (0.10.4)
|
||||||
coderay (~> 1.1.0)
|
coderay (~> 1.1.0)
|
||||||
@@ -292,7 +322,7 @@ GEM
|
|||||||
slop (~> 3.4)
|
slop (~> 3.4)
|
||||||
pry-rails (0.3.6)
|
pry-rails (0.3.6)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (2.0.5)
|
public_suffix (3.0.0)
|
||||||
puma (3.9.1)
|
puma (3.9.1)
|
||||||
pundit (1.1.0)
|
pundit (1.1.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
@@ -307,17 +337,17 @@ GEM
|
|||||||
rack-test (0.6.3)
|
rack-test (0.6.3)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
rack-timeout (0.4.2)
|
rack-timeout (0.4.2)
|
||||||
rails (5.1.2)
|
rails (5.1.3)
|
||||||
actioncable (= 5.1.2)
|
actioncable (= 5.1.3)
|
||||||
actionmailer (= 5.1.2)
|
actionmailer (= 5.1.3)
|
||||||
actionpack (= 5.1.2)
|
actionpack (= 5.1.3)
|
||||||
actionview (= 5.1.2)
|
actionview (= 5.1.3)
|
||||||
activejob (= 5.1.2)
|
activejob (= 5.1.3)
|
||||||
activemodel (= 5.1.2)
|
activemodel (= 5.1.3)
|
||||||
activerecord (= 5.1.2)
|
activerecord (= 5.1.3)
|
||||||
activesupport (= 5.1.2)
|
activesupport (= 5.1.3)
|
||||||
bundler (>= 1.3.0, < 2.0)
|
bundler (>= 1.3.0)
|
||||||
railties (= 5.1.2)
|
railties (= 5.1.3)
|
||||||
sprockets-rails (>= 2.0.0)
|
sprockets-rails (>= 2.0.0)
|
||||||
rails-controller-testing (1.0.2)
|
rails-controller-testing (1.0.2)
|
||||||
actionpack (~> 5.x, >= 5.0.1)
|
actionpack (~> 5.x, >= 5.0.1)
|
||||||
@@ -331,23 +361,28 @@ GEM
|
|||||||
rails-i18n (5.0.4)
|
rails-i18n (5.0.4)
|
||||||
i18n (~> 0.7)
|
i18n (~> 0.7)
|
||||||
railties (~> 5.0)
|
railties (~> 5.0)
|
||||||
rails-settings-cached (0.6.5)
|
rails-settings-cached (0.6.6)
|
||||||
rails (>= 4.2.0)
|
rails (>= 4.2.0)
|
||||||
railties (5.1.2)
|
railties (5.1.3)
|
||||||
actionpack (= 5.1.2)
|
actionpack (= 5.1.3)
|
||||||
activesupport (= 5.1.2)
|
activesupport (= 5.1.3)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 0.8.7)
|
rake (>= 0.8.7)
|
||||||
thor (>= 0.18.1, < 2.0)
|
thor (>= 0.18.1, < 2.0)
|
||||||
rainbow (2.2.2)
|
rainbow (2.2.2)
|
||||||
rake
|
rake
|
||||||
rake (12.0.0)
|
rake (12.0.0)
|
||||||
|
rdf (2.2.8)
|
||||||
|
hamster (~> 3.0)
|
||||||
|
link_header (~> 0.0, >= 0.0.8)
|
||||||
|
rdf-normalize (0.3.2)
|
||||||
|
rdf (~> 2.0)
|
||||||
redis (3.3.3)
|
redis (3.3.3)
|
||||||
redis-actionpack (5.0.1)
|
redis-actionpack (5.0.1)
|
||||||
actionpack (>= 4.0, < 6)
|
actionpack (>= 4.0, < 6)
|
||||||
redis-rack (>= 1, < 3)
|
redis-rack (>= 1, < 3)
|
||||||
redis-store (>= 1.1.0, < 1.4.0)
|
redis-store (>= 1.1.0, < 1.4.0)
|
||||||
redis-activesupport (5.0.2)
|
redis-activesupport (5.0.3)
|
||||||
activesupport (>= 3, < 6)
|
activesupport (>= 3, < 6)
|
||||||
redis-store (~> 1.3.0)
|
redis-store (~> 1.3.0)
|
||||||
redis-namespace (1.5.3)
|
redis-namespace (1.5.3)
|
||||||
@@ -407,7 +442,7 @@ GEM
|
|||||||
scss_lint (0.54.0)
|
scss_lint (0.54.0)
|
||||||
rake (>= 0.9, < 13)
|
rake (>= 0.9, < 13)
|
||||||
sass (~> 3.4.20)
|
sass (~> 3.4.20)
|
||||||
sidekiq (5.0.3)
|
sidekiq (5.0.4)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
connection_pool (~> 2.2, >= 2.2.0)
|
connection_pool (~> 2.2, >= 2.2.0)
|
||||||
rack-protection (>= 1.5.0)
|
rack-protection (>= 1.5.0)
|
||||||
@@ -415,12 +450,12 @@ GEM
|
|||||||
sidekiq-bulk (0.1.1)
|
sidekiq-bulk (0.1.1)
|
||||||
activesupport
|
activesupport
|
||||||
sidekiq
|
sidekiq
|
||||||
sidekiq-scheduler (2.1.7)
|
sidekiq-scheduler (2.1.8)
|
||||||
redis (~> 3)
|
redis (~> 3)
|
||||||
rufus-scheduler (~> 3.2)
|
rufus-scheduler (~> 3.2)
|
||||||
sidekiq (>= 3)
|
sidekiq (>= 3)
|
||||||
tilt (>= 1.4.0)
|
tilt (>= 1.4.0)
|
||||||
sidekiq-unique-jobs (5.0.8)
|
sidekiq-unique-jobs (5.0.9)
|
||||||
sidekiq (>= 4.0, <= 6.0)
|
sidekiq (>= 4.0, <= 6.0)
|
||||||
thor (~> 0)
|
thor (~> 0)
|
||||||
simple-navigation (4.0.5)
|
simple-navigation (4.0.5)
|
||||||
@@ -444,15 +479,15 @@ GEM
|
|||||||
sshkit (1.13.1)
|
sshkit (1.13.1)
|
||||||
net-scp (>= 1.1.2)
|
net-scp (>= 1.1.2)
|
||||||
net-ssh (>= 2.8.0)
|
net-ssh (>= 2.8.0)
|
||||||
statsd-instrument (2.1.2)
|
statsd-instrument (2.1.4)
|
||||||
temple (0.8.0)
|
temple (0.8.0)
|
||||||
terminal-table (1.8.0)
|
terminal-table (1.8.0)
|
||||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||||
thor (0.19.4)
|
thor (0.20.0)
|
||||||
thread (0.2.2)
|
thread (0.2.2)
|
||||||
thread_safe (0.3.6)
|
thread_safe (0.3.6)
|
||||||
tilt (2.0.7)
|
tilt (2.0.8)
|
||||||
twitter-text (1.14.6)
|
twitter-text (1.14.7)
|
||||||
unf (~> 0.1.0)
|
unf (~> 0.1.0)
|
||||||
tzinfo (1.2.3)
|
tzinfo (1.2.3)
|
||||||
thread_safe (~> 0.1)
|
thread_safe (~> 0.1)
|
||||||
@@ -475,6 +510,9 @@ GEM
|
|||||||
activesupport (>= 4.2)
|
activesupport (>= 4.2)
|
||||||
multi_json (~> 1.2)
|
multi_json (~> 1.2)
|
||||||
railties (>= 4.2)
|
railties (>= 4.2)
|
||||||
|
webpush (0.3.2)
|
||||||
|
hkdf (~> 0.2)
|
||||||
|
jwt
|
||||||
websocket-driver (0.6.5)
|
websocket-driver (0.6.5)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.2)
|
websocket-extensions (0.1.2)
|
||||||
@@ -502,7 +540,7 @@ DEPENDENCIES
|
|||||||
capistrano-rbenv (~> 2.1)
|
capistrano-rbenv (~> 2.1)
|
||||||
capistrano-yarn (~> 2.0)
|
capistrano-yarn (~> 2.0)
|
||||||
capybara (~> 2.14)
|
capybara (~> 2.14)
|
||||||
charlock_holmes (~> 0.7.3)
|
charlock_holmes (~> 0.7.5)
|
||||||
cld3 (~> 3.1)
|
cld3 (~> 3.1)
|
||||||
climate_control (~> 0.2)
|
climate_control (~> 0.2)
|
||||||
devise (~> 4.2)
|
devise (~> 4.2)
|
||||||
@@ -512,8 +550,9 @@ DEPENDENCIES
|
|||||||
fabrication (~> 2.16)
|
fabrication (~> 2.16)
|
||||||
faker (~> 1.7)
|
faker (~> 1.7)
|
||||||
fast_blank (~> 1.0)
|
fast_blank (~> 1.0)
|
||||||
|
fog-openstack (~> 0.1)
|
||||||
fuubar (~> 2.2)
|
fuubar (~> 2.2)
|
||||||
goldfinger (~> 1.2)
|
goldfinger (~> 2.0)
|
||||||
hamlit-rails (~> 0.2)
|
hamlit-rails (~> 0.2)
|
||||||
hiredis (~> 0.6)
|
hiredis (~> 0.6)
|
||||||
htmlentities (~> 4.3)
|
htmlentities (~> 4.3)
|
||||||
@@ -521,12 +560,16 @@ DEPENDENCIES
|
|||||||
http_accept_language (~> 2.1)
|
http_accept_language (~> 2.1)
|
||||||
httplog (~> 0.99)
|
httplog (~> 0.99)
|
||||||
i18n-tasks (~> 0.9)
|
i18n-tasks (~> 0.9)
|
||||||
|
idn-ruby
|
||||||
|
iso-639
|
||||||
|
json-ld-preloaded (~> 2.2.1)
|
||||||
kaminari (~> 1.0)
|
kaminari (~> 1.0)
|
||||||
letter_opener (~> 1.4)
|
letter_opener (~> 1.4)
|
||||||
letter_opener_web (~> 1.3)
|
letter_opener_web (~> 1.3)
|
||||||
link_header (~> 0.0)
|
link_header (~> 0.0)
|
||||||
lograge (~> 0.5)
|
lograge (~> 0.5)
|
||||||
microformats2 (~> 3.0)
|
mario-redis-lock (~> 1.2)
|
||||||
|
microformats (~> 4.0)
|
||||||
mime-types (~> 3.1)
|
mime-types (~> 3.1)
|
||||||
nokogiri (~> 1.7)
|
nokogiri (~> 1.7)
|
||||||
oj (~> 3.0)
|
oj (~> 3.0)
|
||||||
@@ -549,6 +592,7 @@ DEPENDENCIES
|
|||||||
rails-controller-testing (~> 1.0)
|
rails-controller-testing (~> 1.0)
|
||||||
rails-i18n (~> 5.0)
|
rails-i18n (~> 5.0)
|
||||||
rails-settings-cached (~> 0.6)
|
rails-settings-cached (~> 0.6)
|
||||||
|
rdf-normalize (~> 0.3.1)
|
||||||
redis (~> 3.3)
|
redis (~> 3.3)
|
||||||
redis-namespace (~> 1.5)
|
redis-namespace (~> 1.5)
|
||||||
redis-rails (~> 5.0)
|
redis-rails (~> 5.0)
|
||||||
@@ -573,9 +617,10 @@ DEPENDENCIES
|
|||||||
uglifier (~> 3.2)
|
uglifier (~> 3.2)
|
||||||
webmock (~> 3.0)
|
webmock (~> 3.0)
|
||||||
webpacker (~> 2.0)
|
webpacker (~> 2.0)
|
||||||
|
webpush
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 2.4.1p111
|
ruby 2.4.1p111
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
1.15.1
|
1.15.4
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
Mastodon Glitch Edition
|
# Mastodon Glitch Edition #
|
||||||
========
|
|
||||||
Now with automated deploys!
|
> Now with automated deploys!
|
||||||
|
|
||||||
[](https://travis-ci.org/glitch-soc/mastodon)
|
[](https://travis-ci.org/glitch-soc/mastodon)
|
||||||
|
|
||||||
So here's the deal: we all work on this code, and then it runs on dev.glitch.social and anyone who uses that does so absolutely at their own risk. can you dig it?
|
So here's the deal: we all work on this code, and then it runs on dev.glitch.social and anyone who uses that does so absolutely at their own risk. can you dig it?
|
||||||
|
|
||||||
|
- You can view documentation for this project at [glitch-soc.github.io/docs/](https://glitch-soc.github.io/docs/).
|
||||||
|
- And contributing guidelines are available [here](CONTRIBUTING.md) and [here](https://glitch-soc.github.io/docs/contributing/).
|
||||||
|
|||||||
3
Vagrantfile
vendored
3
Vagrantfile
vendored
@@ -35,9 +35,10 @@ sudo apt-get install \
|
|||||||
postgresql-contrib \
|
postgresql-contrib \
|
||||||
protobuf-compiler \
|
protobuf-compiler \
|
||||||
yarn \
|
yarn \
|
||||||
|
libicu-dev \
|
||||||
|
libidn11-dev \
|
||||||
libprotobuf-dev \
|
libprotobuf-dev \
|
||||||
libreadline-dev \
|
libreadline-dev \
|
||||||
libicu-dev \
|
|
||||||
-y
|
-y
|
||||||
|
|
||||||
# Install rvm
|
# Install rvm
|
||||||
|
|||||||
2
app.json
2
app.json
@@ -2,7 +2,7 @@
|
|||||||
"name": "Mastodon",
|
"name": "Mastodon",
|
||||||
"description": "A GNU Social-compatible microblogging server",
|
"description": "A GNU Social-compatible microblogging server",
|
||||||
"repository": "https://github.com/tootsuite/mastodon",
|
"repository": "https://github.com/tootsuite/mastodon",
|
||||||
"logo": "https://github.com/tootsuite/mastodon/raw/master/app/assets/images/logo.png",
|
"logo": "https://github.com/tootsuite.png",
|
||||||
"env": {
|
"env": {
|
||||||
"HEROKU": {
|
"HEROKU": {
|
||||||
"description": "Leave this as true",
|
"description": "Leave this as true",
|
||||||
|
|||||||
@@ -2,26 +2,83 @@
|
|||||||
|
|
||||||
class AccountsController < ApplicationController
|
class AccountsController < ApplicationController
|
||||||
include AccountControllerConcern
|
include AccountControllerConcern
|
||||||
|
include SignatureVerification
|
||||||
|
|
||||||
def show
|
def show
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html do
|
format.html do
|
||||||
@statuses = @account.statuses.permitted_for(@account, current_account).paginate_by_max_id(20, params[:max_id], params[:since_id])
|
@pinned_statuses = []
|
||||||
|
|
||||||
|
if current_account && @account.blocking?(current_account)
|
||||||
|
@statuses = []
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
@pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
|
||||||
|
@statuses = filtered_statuses.paginate_by_max_id(20, params[:max_id], params[:since_id])
|
||||||
@statuses = cache_collection(@statuses, Status)
|
@statuses = cache_collection(@statuses, Status)
|
||||||
|
@next_url = next_url unless @statuses.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
format.atom do
|
format.atom do
|
||||||
@entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
|
@entries = @account.stream_entries.where(hidden: false).with_includes.paginate_by_max_id(20, params[:max_id], params[:since_id])
|
||||||
render xml: AtomSerializer.render(AtomSerializer.new.feed(@account, @entries.to_a))
|
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.feed(@account, @entries.reject { |entry| entry.status.nil? }))
|
||||||
end
|
end
|
||||||
|
|
||||||
format.activitystreams2
|
format.json do
|
||||||
|
render json: @account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def show_pinned_statuses?
|
||||||
|
[replies_requested?, media_requested?, params[:max_id].present?, params[:since_id].present?].none?
|
||||||
|
end
|
||||||
|
|
||||||
|
def filtered_statuses
|
||||||
|
default_statuses.tap do |statuses|
|
||||||
|
statuses.merge!(only_media_scope) if media_requested?
|
||||||
|
statuses.merge!(no_replies_scope) unless replies_requested?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_statuses
|
||||||
|
@account.statuses.where(visibility: [:public, :unlisted])
|
||||||
|
end
|
||||||
|
|
||||||
|
def only_media_scope
|
||||||
|
Status.where(id: account_media_status_ids)
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_media_status_ids
|
||||||
|
@account.media_attachments.attached.reorder(nil).select(:status_id).distinct
|
||||||
|
end
|
||||||
|
|
||||||
|
def no_replies_scope
|
||||||
|
Status.without_replies
|
||||||
|
end
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
@account = Account.find_local!(params[:username])
|
@account = Account.find_local!(params[:username])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def next_url
|
||||||
|
if media_requested?
|
||||||
|
short_account_media_url(@account, max_id: @statuses.last.id)
|
||||||
|
elsif replies_requested?
|
||||||
|
short_account_with_replies_url(@account, max_id: @statuses.last.id)
|
||||||
|
else
|
||||||
|
short_account_url(@account, max_id: @statuses.last.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def media_requested?
|
||||||
|
request.path.ends_with?('/media')
|
||||||
|
end
|
||||||
|
|
||||||
|
def replies_requested?
|
||||||
|
request.path.ends_with?('/with_replies')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
40
app/controllers/activitypub/inboxes_controller.rb
Normal file
40
app/controllers/activitypub/inboxes_controller.rb
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::InboxesController < Api::BaseController
|
||||||
|
include SignatureVerification
|
||||||
|
|
||||||
|
before_action :set_account
|
||||||
|
|
||||||
|
def create
|
||||||
|
if signed_request_account
|
||||||
|
upgrade_account
|
||||||
|
process_payload
|
||||||
|
head 201
|
||||||
|
else
|
||||||
|
head 202
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_account
|
||||||
|
@account = Account.find_local!(params[:account_username]) if params[:account_username]
|
||||||
|
end
|
||||||
|
|
||||||
|
def body
|
||||||
|
@body ||= request.body.read
|
||||||
|
end
|
||||||
|
|
||||||
|
def upgrade_account
|
||||||
|
if signed_request_account.ostatus?
|
||||||
|
signed_request_account.update(last_webfingered_at: nil)
|
||||||
|
ResolveRemoteAccountWorker.perform_async(signed_request_account.acct)
|
||||||
|
end
|
||||||
|
|
||||||
|
Pubsubhubbub::UnsubscribeWorker.perform_async(signed_request_account.id) if signed_request_account.subscribed?
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_payload
|
||||||
|
ActivityPub::ProcessingWorker.perform_async(signed_request_account.id, body.force_encoding('UTF-8'))
|
||||||
|
end
|
||||||
|
end
|
||||||
27
app/controllers/activitypub/outboxes_controller.rb
Normal file
27
app/controllers/activitypub/outboxes_controller.rb
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ActivityPub::OutboxesController < Api::BaseController
|
||||||
|
before_action :set_account
|
||||||
|
|
||||||
|
def show
|
||||||
|
@statuses = @account.statuses.permitted_for(@account, current_account).paginate_by_max_id(20, params[:max_id], params[:since_id])
|
||||||
|
@statuses = cache_collection(@statuses, Status)
|
||||||
|
|
||||||
|
render json: outbox_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_account
|
||||||
|
@account = Account.find_local!(params[:account_username])
|
||||||
|
end
|
||||||
|
|
||||||
|
def outbox_presenter
|
||||||
|
ActivityPub::CollectionPresenter.new(
|
||||||
|
id: account_outbox_url(@account),
|
||||||
|
type: :ordered,
|
||||||
|
size: @account.statuses_count,
|
||||||
|
items: @statuses
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -17,7 +17,7 @@ module Admin
|
|||||||
end
|
end
|
||||||
|
|
||||||
def unsubscribe
|
def unsubscribe
|
||||||
UnsubscribeService.new.call(@account)
|
Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id)
|
||||||
redirect_to admin_account_path(@account.id)
|
redirect_to admin_account_path(@account.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -6,15 +6,26 @@ module Admin
|
|||||||
@instances = ordered_instances
|
@instances = ordered_instances
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def resubscribe
|
||||||
|
params.require(:by_domain)
|
||||||
|
Pubsubhubbub::SubscribeWorker.push_bulk(subscribeable_accounts.pluck(:id))
|
||||||
|
redirect_to admin_instances_path
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def paginated_instances
|
def paginated_instances
|
||||||
Account.remote.by_domain_accounts.page(params[:page])
|
Account.remote.by_domain_accounts.page(params[:page])
|
||||||
end
|
end
|
||||||
|
|
||||||
helper_method :paginated_instances
|
helper_method :paginated_instances
|
||||||
|
|
||||||
def ordered_instances
|
def ordered_instances
|
||||||
paginated_instances.map { |account| Instance.new(account) }
|
paginated_instances.map { |account| Instance.new(account) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def subscribeable_accounts
|
||||||
|
Account.with_followers.remote.where(domain: params[:by_domain])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,7 +5,14 @@ module Admin
|
|||||||
include Authorization
|
include Authorization
|
||||||
|
|
||||||
before_action :set_report
|
before_action :set_report
|
||||||
before_action :set_status
|
before_action :set_status, only: [:update, :destroy]
|
||||||
|
|
||||||
|
def create
|
||||||
|
@form = Form::StatusBatch.new(form_status_batch_params)
|
||||||
|
flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
|
||||||
|
|
||||||
|
redirect_to admin_report_path(@report)
|
||||||
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@status.update(status_params)
|
@status.update(status_params)
|
||||||
@@ -15,7 +22,7 @@ module Admin
|
|||||||
def destroy
|
def destroy
|
||||||
authorize @status, :destroy?
|
authorize @status, :destroy?
|
||||||
RemovalWorker.perform_async(@status.id)
|
RemovalWorker.perform_async(@status.id)
|
||||||
redirect_to admin_report_path(@report)
|
render json: @status
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -24,6 +31,10 @@ module Admin
|
|||||||
params.require(:status).permit(:sensitive)
|
params.require(:status).permit(:sensitive)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def form_status_batch_params
|
||||||
|
params.require(:form_status_batch).permit(:action, status_ids: [])
|
||||||
|
end
|
||||||
|
|
||||||
def set_report
|
def set_report
|
||||||
@report = Report.find(params[:report_id])
|
@report = Report.find(params[:report_id])
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ module Admin
|
|||||||
@reports = filtered_reports.page(params[:page])
|
@reports = filtered_reports.page(params[:page])
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
def show
|
||||||
|
@form = Form::StatusBatch.new
|
||||||
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
process_report
|
process_report
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ module Admin
|
|||||||
closed_registrations_message
|
closed_registrations_message
|
||||||
open_deletion
|
open_deletion
|
||||||
timeline_preview
|
timeline_preview
|
||||||
|
bootstrap_timeline_accounts
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
BOOLEAN_SETTINGS = %w(
|
BOOLEAN_SETTINGS = %w(
|
||||||
|
|||||||
69
app/controllers/admin/statuses_controller.rb
Normal file
69
app/controllers/admin/statuses_controller.rb
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin
|
||||||
|
class StatusesController < BaseController
|
||||||
|
include Authorization
|
||||||
|
|
||||||
|
helper_method :current_params
|
||||||
|
|
||||||
|
before_action :set_account
|
||||||
|
before_action :set_status, only: [:update, :destroy]
|
||||||
|
|
||||||
|
PER_PAGE = 20
|
||||||
|
|
||||||
|
def index
|
||||||
|
@statuses = @account.statuses
|
||||||
|
if params[:media]
|
||||||
|
account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct
|
||||||
|
@statuses.merge!(Status.where(id: account_media_status_ids))
|
||||||
|
end
|
||||||
|
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
|
||||||
|
|
||||||
|
@form = Form::StatusBatch.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@form = Form::StatusBatch.new(form_status_batch_params)
|
||||||
|
flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
|
||||||
|
|
||||||
|
redirect_to admin_account_statuses_path(@account.id, current_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@status.update(status_params)
|
||||||
|
redirect_to admin_account_statuses_path(@account.id, current_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize @status, :destroy?
|
||||||
|
RemovalWorker.perform_async(@status.id)
|
||||||
|
render json: @status
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def status_params
|
||||||
|
params.require(:status).permit(:sensitive)
|
||||||
|
end
|
||||||
|
|
||||||
|
def form_status_batch_params
|
||||||
|
params.require(:form_status_batch).permit(:action, status_ids: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_status
|
||||||
|
@status = @account.statuses.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_account
|
||||||
|
@account = Account.find(params[:account_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_params
|
||||||
|
page = (params[:page] || 1).to_i
|
||||||
|
{
|
||||||
|
media: params[:media],
|
||||||
|
page: page > 1 && page,
|
||||||
|
}.select { |_, value| value.present? }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class Api::ActivityPub::ActivitiesController < Api::BaseController
|
|
||||||
include Authorization
|
|
||||||
|
|
||||||
# before_action :set_follow, only: [:show_follow]
|
|
||||||
before_action :set_status, only: [:show_status]
|
|
||||||
|
|
||||||
respond_to :activitystreams2
|
|
||||||
|
|
||||||
# Show a status in AS2 format, as either an Announce (reblog) or a Create (post) activity.
|
|
||||||
def show_status
|
|
||||||
authorize @status, :show?
|
|
||||||
|
|
||||||
if @status.reblog?
|
|
||||||
render :show_status_announce
|
|
||||||
else
|
|
||||||
render :show_status_create
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_status
|
|
||||||
@status = Status.find(params[:id])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class Api::ActivityPub::NotesController < Api::BaseController
|
|
||||||
include Authorization
|
|
||||||
|
|
||||||
before_action :set_status
|
|
||||||
|
|
||||||
respond_to :activitystreams2
|
|
||||||
|
|
||||||
def show
|
|
||||||
authorize @status, :show?
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_status
|
|
||||||
@status = Status.find(params[:id])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class Api::ActivityPub::OutboxController < Api::BaseController
|
|
||||||
before_action :set_account
|
|
||||||
|
|
||||||
respond_to :activitystreams2
|
|
||||||
|
|
||||||
def show
|
|
||||||
if params[:max_id] || params[:since_id]
|
|
||||||
show_outbox_page
|
|
||||||
else
|
|
||||||
show_base_outbox
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def show_base_outbox
|
|
||||||
@statuses = Status.as_outbox_timeline(@account)
|
|
||||||
@statuses = cache_collection(@statuses)
|
|
||||||
|
|
||||||
set_maps(@statuses)
|
|
||||||
|
|
||||||
set_first_last_page(@statuses)
|
|
||||||
|
|
||||||
render :show
|
|
||||||
end
|
|
||||||
|
|
||||||
def show_outbox_page
|
|
||||||
all_statuses = Status.as_outbox_timeline(@account)
|
|
||||||
@statuses = all_statuses.paginate_by_max_id(limit_param(DEFAULT_STATUSES_LIMIT), params[:max_id], params[:since_id])
|
|
||||||
|
|
||||||
all_statuses = cache_collection(all_statuses)
|
|
||||||
@statuses = cache_collection(@statuses)
|
|
||||||
|
|
||||||
set_maps(@statuses)
|
|
||||||
|
|
||||||
set_first_last_page(all_statuses)
|
|
||||||
|
|
||||||
@next_page_url = api_activitypub_outbox_url(pagination_params(max_id: @statuses.last.id)) unless @statuses.empty?
|
|
||||||
@prev_page_url = api_activitypub_outbox_url(pagination_params(since_id: @statuses.first.id)) unless @statuses.empty?
|
|
||||||
|
|
||||||
@paginated = @next_page_url || @prev_page_url
|
|
||||||
@part_of_url = api_activitypub_outbox_url
|
|
||||||
|
|
||||||
set_pagination_headers(@next_page_url, @prev_page_url)
|
|
||||||
|
|
||||||
render :show_page
|
|
||||||
end
|
|
||||||
|
|
||||||
def cache_collection(raw)
|
|
||||||
super(raw, Status)
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_account
|
|
||||||
@account = Account.find(params[:id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_first_last_page(statuses) # rubocop:disable Style/AccessorMethodName
|
|
||||||
return if statuses.empty?
|
|
||||||
|
|
||||||
@first_page_url = api_activitypub_outbox_url(max_id: statuses.first.id + 1)
|
|
||||||
@last_page_url = api_activitypub_outbox_url(since_id: statuses.last.id - 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
def pagination_params(core_params)
|
|
||||||
params.permit(:local, :limit).merge(core_params)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -17,11 +17,7 @@ class Api::BaseController < ApplicationController
|
|||||||
render json: { error: 'Record not found' }, status: 404
|
render json: { error: 'Record not found' }, status: 404
|
||||||
end
|
end
|
||||||
|
|
||||||
rescue_from Goldfinger::Error do
|
rescue_from HTTP::Error, Mastodon::UnexpectedResponseError do
|
||||||
render json: { error: 'Remote account could not be resolved' }, status: 422
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue_from HTTP::Error do
|
|
||||||
render json: { error: 'Remote data could not be fetched' }, status: 503
|
render json: { error: 'Remote data could not be fetched' }, status: 503
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -47,7 +43,7 @@ class Api::BaseController < ApplicationController
|
|||||||
links = []
|
links = []
|
||||||
links << [next_path, [%w(rel next)]] if next_path
|
links << [next_path, [%w(rel next)]] if next_path
|
||||||
links << [prev_path, [%w(rel prev)]] if prev_path
|
links << [prev_path, [%w(rel prev)]] if prev_path
|
||||||
response.headers['Link'] = LinkHeader.new(links)
|
response.headers['Link'] = LinkHeader.new(links) unless links.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def limit_param(default_limit)
|
def limit_param(default_limit)
|
||||||
@@ -66,11 +62,12 @@ class Api::BaseController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def require_user!
|
def require_user!
|
||||||
current_resource_owner
|
if current_user
|
||||||
set_user_activity
|
set_user_activity
|
||||||
rescue ActiveRecord::RecordNotFound
|
else
|
||||||
render json: { error: 'This method requires an authenticated user' }, status: 422
|
render json: { error: 'This method requires an authenticated user' }, status: 422
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def render_empty
|
def render_empty
|
||||||
render json: {}, status: 200
|
render json: {}, status: 200
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ class Api::OEmbedController < Api::BaseController
|
|||||||
respond_to :json
|
respond_to :json
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@stream_entry = find_stream_entry.stream_entry
|
@status = status_finder.status
|
||||||
render json: @stream_entry, serializer: OEmbedSerializer, width: maxwidth_or_default, height: maxheight_or_default
|
render json: @status, serializer: OEmbedSerializer, width: maxwidth_or_default, height: maxheight_or_default
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def find_stream_entry
|
def status_finder
|
||||||
StreamEntryFinder.new(params[:url])
|
StatusFinder.new(params[:url])
|
||||||
end
|
end
|
||||||
|
|
||||||
def maxwidth_or_default
|
def maxwidth_or_default
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::PushController < Api::BaseController
|
class Api::PushController < Api::BaseController
|
||||||
|
include SignatureVerification
|
||||||
|
|
||||||
def update
|
def update
|
||||||
response, status = process_push_request
|
response, status = process_push_request
|
||||||
render plain: response, status: status
|
render plain: response, status: status
|
||||||
@@ -11,7 +13,7 @@ class Api::PushController < Api::BaseController
|
|||||||
def process_push_request
|
def process_push_request
|
||||||
case hub_mode
|
case hub_mode
|
||||||
when 'subscribe'
|
when 'subscribe'
|
||||||
Pubsubhubbub::SubscribeService.new.call(account_from_topic, hub_callback, hub_secret, hub_lease_seconds)
|
Pubsubhubbub::SubscribeService.new.call(account_from_topic, hub_callback, hub_secret, hub_lease_seconds, verified_domain)
|
||||||
when 'unsubscribe'
|
when 'unsubscribe'
|
||||||
Pubsubhubbub::UnsubscribeService.new.call(account_from_topic, hub_callback)
|
Pubsubhubbub::UnsubscribeService.new.call(account_from_topic, hub_callback)
|
||||||
else
|
else
|
||||||
@@ -57,6 +59,10 @@ class Api::PushController < Api::BaseController
|
|||||||
TagManager.instance.web_domain?(hub_topic_domain)
|
TagManager.instance.web_domain?(hub_topic_domain)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def verified_domain
|
||||||
|
return signed_request_account.domain if signed_request_account
|
||||||
|
end
|
||||||
|
|
||||||
def hub_topic_domain
|
def hub_topic_domain
|
||||||
hub_topic_uri.host + (hub_topic_uri.port ? ":#{hub_topic_uri.port}" : '')
|
hub_topic_uri.host + (hub_topic_uri.port ? ":#{hub_topic_uri.port}" : '')
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class Api::SubscriptionsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def lease_seconds_or_default
|
def lease_seconds_or_default
|
||||||
(params['hub.lease_seconds'] || 86_400).to_i.seconds
|
(params['hub.lease_seconds'] || 1.day).to_i.seconds
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Accounts::CredentialsController < Api::BaseController
|
class Api::V1::Accounts::CredentialsController < Api::BaseController
|
||||||
|
before_action -> { doorkeeper_authorize! :read }, except: [:update]
|
||||||
before_action -> { doorkeeper_authorize! :write }, only: [:update]
|
before_action -> { doorkeeper_authorize! :write }, only: [:update]
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
|
|
||||||
@@ -10,8 +11,9 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
current_account.update!(account_params)
|
|
||||||
@account = current_account
|
@account = current_account
|
||||||
|
UpdateAccountService.new.call(@account, account_params, raise_error: true)
|
||||||
|
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
||||||
render json: @account, serializer: REST::CredentialAccountSerializer
|
render json: @account, serializer: REST::CredentialAccountSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
|||||||
def account_statuses
|
def account_statuses
|
||||||
default_statuses.tap do |statuses|
|
default_statuses.tap do |statuses|
|
||||||
statuses.merge!(only_media_scope) if params[:only_media]
|
statuses.merge!(only_media_scope) if params[:only_media]
|
||||||
|
statuses.merge!(pinned_scope) if params[:pinned]
|
||||||
statuses.merge!(no_replies_scope) if params[:exclude_replies]
|
statuses.merge!(no_replies_scope) if params[:exclude_replies]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -53,6 +54,10 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
|||||||
@account.media_attachments.attached.reorder(nil).select(:status_id).distinct
|
@account.media_attachments.attached.reorder(nil).select(:status_id).distinct
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def pinned_scope
|
||||||
|
@account.pinned_statuses
|
||||||
|
end
|
||||||
|
|
||||||
def no_replies_scope
|
def no_replies_scope
|
||||||
Status.without_replies
|
Status.without_replies
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
|
|
||||||
def follow
|
def follow
|
||||||
FollowService.new.call(current_user.account, @account.acct)
|
FollowService.new.call(current_user.account, @account.acct)
|
||||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
|
||||||
|
options = @account.locked? ? {} : { following_map: { @account.id => true }, requested_map: { @account.id => false } }
|
||||||
|
|
||||||
|
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
|
||||||
end
|
end
|
||||||
|
|
||||||
def block
|
def block
|
||||||
@@ -23,7 +26,7 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def mute
|
def mute
|
||||||
MuteService.new.call(current_user.account, @account)
|
MuteService.new.call(current_user.account, @account, notifications: params[:notifications])
|
||||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -48,7 +51,7 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
@account = Account.find(params[:id])
|
@account = Account.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def relationships
|
def relationships(options = {})
|
||||||
AccountRelationshipsPresenter.new([@account.id], current_user.account_id)
|
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
8
app/controllers/api/v1/extensions_controller.rb
Normal file
8
app/controllers/api/v1/extensions_controller.rb
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
require 'mastodon/extension'
|
||||||
|
|
||||||
|
class Api::V1::ExtensionsController < Api::BaseController
|
||||||
|
def index
|
||||||
|
render json: Mastodon::Extension.all
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -20,9 +20,7 @@ class Api::V1::FavouritesController < Api::BaseController
|
|||||||
|
|
||||||
def cached_favourites
|
def cached_favourites
|
||||||
cache_collection(
|
cache_collection(
|
||||||
Status.where(
|
Status.reorder(nil).joins(:favourites).merge(results),
|
||||||
id: results.map(&:status_id)
|
|
||||||
),
|
|
||||||
Status
|
Status
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ class Api::V1::FollowsController < Api::BaseController
|
|||||||
raise ActiveRecord::RecordNotFound if follow_params[:uri].blank?
|
raise ActiveRecord::RecordNotFound if follow_params[:uri].blank?
|
||||||
|
|
||||||
@account = FollowService.new.call(current_user.account, target_uri).try(:target_account)
|
@account = FollowService.new.call(current_user.account, target_uri).try(:target_account)
|
||||||
|
|
||||||
|
if @account.nil?
|
||||||
|
username, domain = target_uri.split('@')
|
||||||
|
@account = Account.find_remote!(username, domain)
|
||||||
|
end
|
||||||
|
|
||||||
render json: @account, serializer: REST::AccountSerializer
|
render json: @account, serializer: REST::AccountSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,15 @@ class Api::V1::MutesController < Api::BaseController
|
|||||||
respond_to :json
|
respond_to :json
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@accounts = load_accounts
|
@data = @accounts = load_accounts
|
||||||
render json: @accounts, each_serializer: REST::AccountSerializer
|
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def details
|
||||||
|
@data = @mutes = load_mutes
|
||||||
|
render json: @mutes, each_serializer: REST::MuteSerializer
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_accounts
|
def load_accounts
|
||||||
@@ -22,6 +27,10 @@ class Api::V1::MutesController < Api::BaseController
|
|||||||
Account.includes(:muted_by).references(:muted_by)
|
Account.includes(:muted_by).references(:muted_by)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def load_mutes
|
||||||
|
paginated_mutes.includes(:account, :target_account).to_a
|
||||||
|
end
|
||||||
|
|
||||||
def paginated_mutes
|
def paginated_mutes
|
||||||
Mute.where(account: current_account).paginate_by_max_id(
|
Mute.where(account: current_account).paginate_by_max_id(
|
||||||
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
||||||
@@ -36,26 +45,34 @@ class Api::V1::MutesController < Api::BaseController
|
|||||||
|
|
||||||
def next_path
|
def next_path
|
||||||
if records_continue?
|
if records_continue?
|
||||||
api_v1_mutes_url pagination_params(max_id: pagination_max_id)
|
url_for pagination_params(max_id: pagination_max_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def prev_path
|
def prev_path
|
||||||
unless @accounts.empty?
|
unless@data.empty?
|
||||||
api_v1_mutes_url pagination_params(since_id: pagination_since_id)
|
url_for pagination_params(since_id: pagination_since_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_max_id
|
def pagination_max_id
|
||||||
|
if params[:action] == "details"
|
||||||
|
@mutes.last.id
|
||||||
|
else
|
||||||
@accounts.last.muted_by_ids.last
|
@accounts.last.muted_by_ids.last
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def pagination_since_id
|
def pagination_since_id
|
||||||
|
if params[:action] == "details"
|
||||||
|
@mutes.first.id
|
||||||
|
else
|
||||||
@accounts.first.muted_by_ids.first
|
@accounts.first.muted_by_ids.first
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def records_continue?
|
def records_continue?
|
||||||
@accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
|
@data.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_params(core_params)
|
def pagination_params(core_params)
|
||||||
|
|||||||
@@ -24,11 +24,20 @@ class Api::V1::NotificationsController < Api::BaseController
|
|||||||
render_empty
|
render_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
dismiss
|
||||||
|
end
|
||||||
|
|
||||||
def dismiss
|
def dismiss
|
||||||
current_account.notifications.find_by!(id: params[:id]).destroy!
|
current_account.notifications.find_by!(id: params[:id]).destroy!
|
||||||
render_empty
|
render_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy_multiple
|
||||||
|
current_account.notifications.where(id: params[:ids]).destroy_all
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_notifications
|
def load_notifications
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::SearchController < Api::BaseController
|
class Api::V1::SearchController < Api::BaseController
|
||||||
RESULTS_LIMIT = 5
|
RESULTS_LIMIT = 10
|
||||||
|
|
||||||
before_action -> { doorkeeper_authorize! :read }
|
before_action -> { doorkeeper_authorize! :read }
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController
|
|||||||
|
|
||||||
UnfavouriteWorker.perform_async(current_user.account_id, @status.id)
|
UnfavouriteWorker.perform_async(current_user.account_id, @status.id)
|
||||||
|
|
||||||
render json: @status, serializer: REST::StatusSerializer
|
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_user&.account_id, favourites_map: @favourites_map)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
28
app/controllers/api/v1/statuses/pins_controller.rb
Normal file
28
app/controllers/api/v1/statuses/pins_controller.rb
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Statuses::PinsController < Api::BaseController
|
||||||
|
include Authorization
|
||||||
|
|
||||||
|
before_action -> { doorkeeper_authorize! :write }
|
||||||
|
before_action :require_user!
|
||||||
|
before_action :set_status
|
||||||
|
|
||||||
|
respond_to :json
|
||||||
|
|
||||||
|
def create
|
||||||
|
StatusPin.create!(account: current_account, status: @status)
|
||||||
|
render json: @status, serializer: REST::StatusSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
pin = StatusPin.find_by(account: current_account, status: @status)
|
||||||
|
pin&.destroy!
|
||||||
|
render json: @status, serializer: REST::StatusSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_status
|
||||||
|
@status = Status.find(params[:status_id])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -20,7 +20,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
|
|||||||
authorize status_for_destroy, :unreblog?
|
authorize status_for_destroy, :unreblog?
|
||||||
RemovalWorker.perform_async(status_for_destroy.id)
|
RemovalWorker.perform_async(status_for_destroy.id)
|
||||||
|
|
||||||
render json: @status, serializer: REST::StatusSerializer
|
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_user&.account_id, reblogs_map: @reblogs_map)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def card
|
def card
|
||||||
@card = PreviewCard.find_by(status: @status)
|
@card = @status.preview_cards.first
|
||||||
|
|
||||||
if @card.nil?
|
if @card.nil?
|
||||||
render_empty
|
render_empty
|
||||||
|
|||||||
17
app/controllers/api/web/embeds_controller.rb
Normal file
17
app/controllers/api/web/embeds_controller.rb
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::Web::EmbedsController < Api::BaseController
|
||||||
|
respond_to :json
|
||||||
|
|
||||||
|
before_action :require_user!
|
||||||
|
|
||||||
|
def create
|
||||||
|
status = StatusFinder.new(params[:url]).status
|
||||||
|
render json: status, serializer: OEmbedSerializer, width: 400
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
oembed = OEmbed::Providers.get(params[:url])
|
||||||
|
render json: Oj.dump(oembed.fields)
|
||||||
|
rescue OEmbed::NotFound
|
||||||
|
render json: {}, status: :not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
52
app/controllers/api/web/push_subscriptions_controller.rb
Normal file
52
app/controllers/api/web/push_subscriptions_controller.rb
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::Web::PushSubscriptionsController < Api::BaseController
|
||||||
|
respond_to :json
|
||||||
|
|
||||||
|
before_action :require_user!
|
||||||
|
|
||||||
|
def create
|
||||||
|
params.require(:subscription).require(:endpoint)
|
||||||
|
params.require(:subscription).require(:keys).require([:auth, :p256dh])
|
||||||
|
|
||||||
|
active_session = current_session
|
||||||
|
|
||||||
|
unless active_session.web_push_subscription.nil?
|
||||||
|
active_session.web_push_subscription.destroy!
|
||||||
|
active_session.update!(web_push_subscription: nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Mobile devices do not support regular notifications, so we enable push notifications by default
|
||||||
|
alerts_enabled = active_session.detection.device.mobile? || active_session.detection.device.tablet?
|
||||||
|
|
||||||
|
data = {
|
||||||
|
alerts: {
|
||||||
|
follow: alerts_enabled,
|
||||||
|
favourite: alerts_enabled,
|
||||||
|
reblog: alerts_enabled,
|
||||||
|
mention: alerts_enabled,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
web_subscription = ::Web::PushSubscription.create!(
|
||||||
|
endpoint: params[:subscription][:endpoint],
|
||||||
|
key_p256dh: params[:subscription][:keys][:p256dh],
|
||||||
|
key_auth: params[:subscription][:keys][:auth],
|
||||||
|
data: data
|
||||||
|
)
|
||||||
|
|
||||||
|
active_session.update!(web_push_subscription: web_subscription)
|
||||||
|
|
||||||
|
render json: web_subscription.as_payload
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
params.require([:id, :data])
|
||||||
|
|
||||||
|
web_subscription = ::Web::PushSubscription.find(params[:id])
|
||||||
|
|
||||||
|
web_subscription.update!(data: params[:data])
|
||||||
|
|
||||||
|
render json: web_subscription.as_payload
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -43,6 +43,10 @@ class ApplicationController < ActionController::Base
|
|||||||
forbidden if current_user.account.suspended?
|
forbidden if current_user.account.suspended?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def after_sign_out_path_for(_resource_or_scope)
|
||||||
|
new_user_session_path
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def forbidden
|
def forbidden
|
||||||
|
|||||||
@@ -2,4 +2,10 @@
|
|||||||
|
|
||||||
class Auth::ConfirmationsController < Devise::ConfirmationsController
|
class Auth::ConfirmationsController < Devise::ConfirmationsController
|
||||||
layout 'auth'
|
layout 'auth'
|
||||||
|
|
||||||
|
def show
|
||||||
|
super do |user|
|
||||||
|
BootstrapTimelineWorker.perform_async(user.account_id) if user.errors.empty?
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Auth::PasswordsController < Devise::PasswordsController
|
class Auth::PasswordsController < Devise::PasswordsController
|
||||||
|
before_action :check_validity_of_reset_password_token, only: :edit
|
||||||
|
|
||||||
layout 'auth'
|
layout 'auth'
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def check_validity_of_reset_password_token
|
||||||
|
unless reset_password_token_is_valid?
|
||||||
|
flash[:error] = I18n.t('auth.invalid_reset_password_token')
|
||||||
|
redirect_to new_password_path(resource_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset_password_token_is_valid?
|
||||||
|
resource_class.with_reset_password_token(params[:reset_password_token]).present?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AuthorizeFollowsController < ApplicationController
|
class AuthorizeFollowsController < ApplicationController
|
||||||
layout 'public'
|
layout 'modal'
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ module AccountControllerConcern
|
|||||||
[
|
[
|
||||||
webfinger_account_link,
|
webfinger_account_link,
|
||||||
atom_account_url_link,
|
atom_account_url_link,
|
||||||
|
actor_url_link,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -41,6 +42,13 @@ module AccountControllerConcern
|
|||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def actor_url_link
|
||||||
|
[
|
||||||
|
ActivityPub::TagManager.instance.uri_for(@account),
|
||||||
|
[%w(rel alternate), %w(type application/activity+json)],
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
def webfinger_account_url
|
def webfinger_account_url
|
||||||
webfinger_url(resource: @account.to_webfinger_s)
|
webfinger_url(resource: @account.to_webfinger_s)
|
||||||
end
|
end
|
||||||
|
|||||||
106
app/controllers/concerns/signature_verification.rb
Normal file
106
app/controllers/concerns/signature_verification.rb
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Implemented according to HTTP signatures (Draft 6)
|
||||||
|
# <https://tools.ietf.org/html/draft-cavage-http-signatures-06>
|
||||||
|
module SignatureVerification
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
def signed_request?
|
||||||
|
request.headers['Signature'].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def signed_request_account
|
||||||
|
return @signed_request_account if defined?(@signed_request_account)
|
||||||
|
|
||||||
|
unless signed_request?
|
||||||
|
@signed_request_account = nil
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
raw_signature = request.headers['Signature']
|
||||||
|
signature_params = {}
|
||||||
|
|
||||||
|
raw_signature.split(',').each do |part|
|
||||||
|
parsed_parts = part.match(/([a-z]+)="([^"]+)"/i)
|
||||||
|
next if parsed_parts.nil? || parsed_parts.size != 3
|
||||||
|
signature_params[parsed_parts[1]] = parsed_parts[2]
|
||||||
|
end
|
||||||
|
|
||||||
|
if incompatible_signature?(signature_params)
|
||||||
|
@signed_request_account = nil
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
account = account_from_key_id(signature_params['keyId'])
|
||||||
|
|
||||||
|
if account.nil?
|
||||||
|
@signed_request_account = nil
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
signature = Base64.decode64(signature_params['signature'])
|
||||||
|
compare_signed_string = build_signed_string(signature_params['headers'])
|
||||||
|
|
||||||
|
if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
|
||||||
|
@signed_request_account = account
|
||||||
|
@signed_request_account
|
||||||
|
else
|
||||||
|
@signed_request_account = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def request_body
|
||||||
|
@request_body ||= request.raw_post
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def build_signed_string(signed_headers)
|
||||||
|
signed_headers = 'date' if signed_headers.blank?
|
||||||
|
|
||||||
|
signed_headers.split(' ').map do |signed_header|
|
||||||
|
if signed_header == Request::REQUEST_TARGET
|
||||||
|
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
||||||
|
elsif signed_header == 'digest'
|
||||||
|
"digest: #{body_digest}"
|
||||||
|
else
|
||||||
|
"#{signed_header}: #{request.headers[to_header_name(signed_header)]}"
|
||||||
|
end
|
||||||
|
end.join("\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
def matches_time_window?
|
||||||
|
begin
|
||||||
|
time_sent = DateTime.httpdate(request.headers['Date'])
|
||||||
|
rescue ArgumentError
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
(Time.now.utc - time_sent).abs <= 30
|
||||||
|
end
|
||||||
|
|
||||||
|
def body_digest
|
||||||
|
"SHA-256=#{Digest::SHA256.base64digest(request_body)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_header_name(name)
|
||||||
|
name.split(/-/).map(&:capitalize).join('-')
|
||||||
|
end
|
||||||
|
|
||||||
|
def incompatible_signature?(signature_params)
|
||||||
|
signature_params['keyId'].blank? ||
|
||||||
|
signature_params['signature'].blank? ||
|
||||||
|
signature_params['algorithm'].blank? ||
|
||||||
|
signature_params['algorithm'] != 'rsa-sha256'
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_from_key_id(key_id)
|
||||||
|
if key_id.start_with?('acct:')
|
||||||
|
ResolveRemoteAccountService.new.call(key_id.gsub(/\Aacct:/, ''))
|
||||||
|
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
|
||||||
|
account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account)
|
||||||
|
account ||= ActivityPub::FetchRemoteKeyService.new.call(key_id)
|
||||||
|
account
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,5 +5,24 @@ class FollowerAccountsController < ApplicationController
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
@follows = Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
|
@follows = Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.html
|
||||||
|
|
||||||
|
format.json do
|
||||||
|
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def collection_presenter
|
||||||
|
ActivityPub::CollectionPresenter.new(
|
||||||
|
id: account_followers_url(@account),
|
||||||
|
type: :ordered,
|
||||||
|
size: @account.followers_count,
|
||||||
|
items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.account) }
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,5 +5,24 @@ class FollowingAccountsController < ApplicationController
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
@follows = Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
|
@follows = Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.html
|
||||||
|
|
||||||
|
format.json do
|
||||||
|
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def collection_presenter
|
||||||
|
ActivityPub::CollectionPresenter.new(
|
||||||
|
id: account_following_index_url(@account),
|
||||||
|
type: :ordered,
|
||||||
|
size: @account.following_count,
|
||||||
|
items: @follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.target_account) }
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ class HomeController < ApplicationController
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
@body_classes = 'app-body'
|
@body_classes = 'app-body'
|
||||||
|
@frontend = (params[:frontend] and Rails.configuration.x.available_frontends.include? params[:frontend] + '.js') ? params[:frontend] : 'mastodon'
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -22,6 +23,7 @@ class HomeController < ApplicationController
|
|||||||
def initial_state_params
|
def initial_state_params
|
||||||
{
|
{
|
||||||
settings: Web::Setting.find_by(user: current_user)&.data || {},
|
settings: Web::Setting.find_by(user: current_user)&.data || {},
|
||||||
|
push_subscription: current_account.user.web_push_subscription(current_session),
|
||||||
current_account: current_account,
|
current_account: current_account,
|
||||||
token: current_session.token,
|
token: current_session.token,
|
||||||
admin: Account.find_local(Setting.site_contact_username),
|
admin: Account.find_local(Setting.site_contact_username),
|
||||||
|
|||||||
18
app/controllers/intents_controller.rb
Normal file
18
app/controllers/intents_controller.rb
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class IntentsController < ApplicationController
|
||||||
|
def show
|
||||||
|
uri = Addressable::URI.parse(params[:uri])
|
||||||
|
|
||||||
|
if uri.scheme == 'web+mastodon'
|
||||||
|
case uri.host
|
||||||
|
when 'follow'
|
||||||
|
return redirect_to authorize_follow_path(acct: uri.query_values['uri'].gsub(/\Aacct:/, ''))
|
||||||
|
when 'share'
|
||||||
|
return redirect_to share_path(text: uri.query_values['text'])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class RemoteFollowController < ApplicationController
|
class RemoteFollowController < ApplicationController
|
||||||
layout 'public'
|
layout 'modal'
|
||||||
|
|
||||||
before_action :set_account
|
before_action :set_account
|
||||||
before_action :gone, if: :suspended_account?
|
before_action :gone, if: :suspended_account?
|
||||||
|
|||||||
72
app/controllers/settings/applications_controller.rb
Normal file
72
app/controllers/settings/applications_controller.rb
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Settings::ApplicationsController < ApplicationController
|
||||||
|
layout 'admin'
|
||||||
|
|
||||||
|
before_action :authenticate_user!
|
||||||
|
before_action :set_application, only: [:show, :update, :destroy, :regenerate]
|
||||||
|
before_action :prepare_scopes, only: [:create, :update]
|
||||||
|
|
||||||
|
def index
|
||||||
|
@applications = current_user.applications.page(params[:page])
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
@application = Doorkeeper::Application.new(
|
||||||
|
redirect_uri: Doorkeeper.configuration.native_redirect_uri,
|
||||||
|
scopes: 'read write follow'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@application = current_user.applications.build(application_params)
|
||||||
|
|
||||||
|
if @application.save
|
||||||
|
redirect_to settings_applications_path, notice: I18n.t('applications.created')
|
||||||
|
else
|
||||||
|
render :new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
if @application.update(application_params)
|
||||||
|
redirect_to settings_applications_path, notice: I18n.t('generic.changes_saved_msg')
|
||||||
|
else
|
||||||
|
render :show
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@application.destroy
|
||||||
|
redirect_to settings_applications_path, notice: I18n.t('applications.destroyed')
|
||||||
|
end
|
||||||
|
|
||||||
|
def regenerate
|
||||||
|
@access_token = current_user.token_for_app(@application)
|
||||||
|
@access_token.destroy
|
||||||
|
|
||||||
|
redirect_to settings_application_path(@application), notice: I18n.t('applications.token_regenerated')
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_application
|
||||||
|
@application = current_user.applications.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def application_params
|
||||||
|
params.require(:doorkeeper_application).permit(
|
||||||
|
:name,
|
||||||
|
:redirect_uri,
|
||||||
|
:scopes,
|
||||||
|
:website
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def prepare_scopes
|
||||||
|
scopes = params.fetch(:doorkeeper_application, {}).fetch(:scopes, nil)
|
||||||
|
params[:doorkeeper_application][:scopes] = scopes.join(' ') if scopes.is_a? Array
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -35,10 +35,12 @@ class Settings::PreferencesController < ApplicationController
|
|||||||
params.require(:user).permit(
|
params.require(:user).permit(
|
||||||
:setting_default_privacy,
|
:setting_default_privacy,
|
||||||
:setting_default_sensitive,
|
:setting_default_sensitive,
|
||||||
|
:setting_unfollow_modal,
|
||||||
:setting_boost_modal,
|
:setting_boost_modal,
|
||||||
:setting_delete_modal,
|
:setting_delete_modal,
|
||||||
:setting_auto_play_gif,
|
:setting_auto_play_gif,
|
||||||
:setting_system_font_ui,
|
:setting_system_font_ui,
|
||||||
|
:setting_noindex,
|
||||||
notification_emails: %i(follow follow_request reblog favourite mention digest),
|
notification_emails: %i(follow follow_request reblog favourite mention digest),
|
||||||
interactions: %i(must_be_follower must_be_following)
|
interactions: %i(must_be_follower must_be_following)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ class Settings::ProfilesController < ApplicationController
|
|||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
if @account.update(account_params)
|
if UpdateAccountService.new.call(@account, account_params)
|
||||||
|
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
||||||
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg')
|
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg')
|
||||||
else
|
else
|
||||||
render :show
|
render :show
|
||||||
|
|||||||
17
app/controllers/settings/sessions_controller.rb
Normal file
17
app/controllers/settings/sessions_controller.rb
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Settings::SessionsController < ApplicationController
|
||||||
|
before_action :set_session, only: :destroy
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@session.destroy!
|
||||||
|
flash[:notice] = I18n.t('sessions.revoke_success')
|
||||||
|
redirect_to edit_user_registration_path
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_session
|
||||||
|
@session = current_user.session_activations.find(params[:id])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -18,7 +18,7 @@ module Settings
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
if current_user.validate_and_consume_otp!(confirmation_params[:code])
|
if acceptable_code?
|
||||||
current_user.otp_required_for_login = false
|
current_user.otp_required_for_login = false
|
||||||
current_user.save!
|
current_user.save!
|
||||||
redirect_to settings_two_factor_authentication_path
|
redirect_to settings_two_factor_authentication_path
|
||||||
@@ -38,5 +38,10 @@ module Settings
|
|||||||
def verify_otp_required
|
def verify_otp_required
|
||||||
redirect_to settings_two_factor_authentication_path if current_user.otp_required_for_login?
|
redirect_to settings_two_factor_authentication_path if current_user.otp_required_for_login?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def acceptable_code?
|
||||||
|
current_user.validate_and_consume_otp!(confirmation_params[:code]) ||
|
||||||
|
current_user.invalidate_otp_backup_code!(confirmation_params[:code])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
30
app/controllers/shares_controller.rb
Normal file
30
app/controllers/shares_controller.rb
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SharesController < ApplicationController
|
||||||
|
layout 'modal'
|
||||||
|
|
||||||
|
before_action :authenticate_user!
|
||||||
|
before_action :set_body_classes
|
||||||
|
|
||||||
|
def show
|
||||||
|
serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
|
||||||
|
@initial_state_json = serializable_resource.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def initial_state_params
|
||||||
|
{
|
||||||
|
settings: Web::Setting.find_by(user: current_user)&.data || {},
|
||||||
|
push_subscription: current_account.user.web_push_subscription(current_session),
|
||||||
|
current_account: current_account,
|
||||||
|
token: current_session.token,
|
||||||
|
admin: Account.find_local(Setting.site_contact_username),
|
||||||
|
text: params[:text],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_body_classes
|
||||||
|
@body_classes = 'compose-standalone'
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -9,14 +9,32 @@ class StatusesController < ApplicationController
|
|||||||
before_action :set_status
|
before_action :set_status
|
||||||
before_action :set_link_headers
|
before_action :set_link_headers
|
||||||
before_action :check_account_suspension
|
before_action :check_account_suspension
|
||||||
|
before_action :redirect_to_original, only: [:show]
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
respond_to do |format|
|
||||||
|
format.html do
|
||||||
@ancestors = @status.reply? ? cache_collection(@status.ancestors(current_account), Status) : []
|
@ancestors = @status.reply? ? cache_collection(@status.ancestors(current_account), Status) : []
|
||||||
@descendants = cache_collection(@status.descendants(current_account), Status)
|
@descendants = cache_collection(@status.descendants(current_account), Status)
|
||||||
|
|
||||||
render 'stream_entries/show'
|
render 'stream_entries/show'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
format.json do
|
||||||
|
render json: @status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def activity
|
||||||
|
render json: @status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
||||||
|
end
|
||||||
|
|
||||||
|
def embed
|
||||||
|
response.headers['X-Frame-Options'] = 'ALLOWALL'
|
||||||
|
render 'stream_entries/embed', layout: 'embedded'
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
@@ -24,7 +42,12 @@ class StatusesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def set_link_headers
|
def set_link_headers
|
||||||
response.headers['Link'] = LinkHeader.new([[account_stream_entry_url(@account, @status.stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]]])
|
response.headers['Link'] = LinkHeader.new(
|
||||||
|
[
|
||||||
|
[account_stream_entry_url(@account, @status.stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]],
|
||||||
|
[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]],
|
||||||
|
]
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_status
|
def set_status
|
||||||
@@ -41,4 +64,8 @@ class StatusesController < ApplicationController
|
|||||||
def check_account_suspension
|
def check_account_suspension
|
||||||
gone if @account.suspended?
|
gone if @account.suspended?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def redirect_to_original
|
||||||
|
redirect_to ::TagManager.instance.url_for(@status.reblog) if @status.reblog?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
class StreamEntriesController < ApplicationController
|
class StreamEntriesController < ApplicationController
|
||||||
include Authorization
|
include Authorization
|
||||||
|
include SignatureVerification
|
||||||
|
|
||||||
layout 'public'
|
layout 'public'
|
||||||
|
|
||||||
@@ -18,16 +19,13 @@ class StreamEntriesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
format.atom do
|
format.atom do
|
||||||
render xml: AtomSerializer.render(AtomSerializer.new.entry(@stream_entry, true))
|
render xml: OStatus::AtomSerializer.render(OStatus::AtomSerializer.new.entry(@stream_entry, true))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def embed
|
def embed
|
||||||
response.headers['X-Frame-Options'] = 'ALLOWALL'
|
redirect_to embed_short_account_status_url(@account, @stream_entry.activity), status: 301
|
||||||
return gone if @stream_entry.activity.nil?
|
|
||||||
|
|
||||||
render layout: 'embedded'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -37,7 +35,12 @@ class StreamEntriesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def set_link_headers
|
def set_link_headers
|
||||||
response.headers['Link'] = LinkHeader.new([[account_stream_entry_url(@account, @stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]]])
|
response.headers['Link'] = LinkHeader.new(
|
||||||
|
[
|
||||||
|
[account_stream_entry_url(@account, @stream_entry, format: 'atom'), [%w(rel alternate), %w(type application/atom+xml)]],
|
||||||
|
[ActivityPub::TagManager.instance.uri_for(@stream_entry.activity), [%w(rel alternate), %w(type application/activity+json)]],
|
||||||
|
]
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_stream_entry
|
def set_stream_entry
|
||||||
|
|||||||
@@ -5,7 +5,26 @@ class TagsController < ApplicationController
|
|||||||
|
|
||||||
def show
|
def show
|
||||||
@tag = Tag.find_by!(name: params[:id].downcase)
|
@tag = Tag.find_by!(name: params[:id].downcase)
|
||||||
@statuses = @tag.nil? ? [] : Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(20, params[:max_id])
|
@statuses = Status.as_tag_timeline(@tag, current_account, params[:local]).paginate_by_max_id(20, params[:max_id])
|
||||||
@statuses = cache_collection(@statuses, Status)
|
@statuses = cache_collection(@statuses, Status)
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.html
|
||||||
|
|
||||||
|
format.json do
|
||||||
|
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def collection_presenter
|
||||||
|
ActivityPub::CollectionPresenter.new(
|
||||||
|
id: tag_url(@tag),
|
||||||
|
type: :ordered,
|
||||||
|
size: @tag.statuses.count,
|
||||||
|
items: @statuses.map { |s| ActivityPub::TagManager.instance.uri_for(s) }
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Activitystreams2BuilderHelper
|
|
||||||
# Gets a usable name for an account, using display name or username.
|
|
||||||
def account_name(account)
|
|
||||||
account.display_name.presence || account.username
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -5,6 +5,10 @@ module ApplicationHelper
|
|||||||
current_page?(path) ? 'active' : ''
|
current_page?(path) ? 'active' : ''
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def active_link_to(label, path, options = {})
|
||||||
|
link_to label, path, options.merge(class: active_nav_class(path))
|
||||||
|
end
|
||||||
|
|
||||||
def show_landing_strip?
|
def show_landing_strip?
|
||||||
!user_signed_in? && !single_user_mode?
|
!user_signed_in? && !single_user_mode?
|
||||||
end
|
end
|
||||||
|
|||||||
24
app/helpers/emoji_helper.rb
Normal file
24
app/helpers/emoji_helper.rb
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EmojiHelper
|
||||||
|
def emojify(text)
|
||||||
|
return text if text.blank?
|
||||||
|
|
||||||
|
text.gsub(emoji_pattern) do |match|
|
||||||
|
emoji = Emoji.instance.unicode($1) # rubocop:disable Style/PerlBackrefs
|
||||||
|
|
||||||
|
if emoji
|
||||||
|
emoji
|
||||||
|
else
|
||||||
|
match
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def emoji_pattern
|
||||||
|
@emoji_pattern ||=
|
||||||
|
/(?<=[^[:alnum:]:]|\n|^)
|
||||||
|
(#{Emoji.instance.names.map { |name| Regexp.escape(name) }.join('|')})
|
||||||
|
(?=[^[:alnum:]:]|$)/x
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module HttpHelper
|
|
||||||
def http_client(options = {})
|
|
||||||
timeout = { write: 10, connect: 10, read: 10 }.merge(options)
|
|
||||||
|
|
||||||
HTTP.headers(user_agent: user_agent)
|
|
||||||
.timeout(:per_operation, timeout)
|
|
||||||
.follow
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def user_agent
|
|
||||||
@user_agent ||= "#{HTTP::Request::USER_AGENT} (Mastodon/#{Mastodon::Version}; +http://#{Rails.configuration.x.local_domain}/)"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
module InstanceHelper
|
module InstanceHelper
|
||||||
def site_title
|
def site_title
|
||||||
Setting.site_title.to_s
|
Setting.site_title.presence || site_hostname
|
||||||
end
|
end
|
||||||
|
|
||||||
def site_hostname
|
def site_hostname
|
||||||
|
|||||||
52
app/helpers/jsonld_helper.rb
Normal file
52
app/helpers/jsonld_helper.rb
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module JsonLdHelper
|
||||||
|
def equals_or_includes?(haystack, needle)
|
||||||
|
haystack.is_a?(Array) ? haystack.include?(needle) : haystack == needle
|
||||||
|
end
|
||||||
|
|
||||||
|
def first_of_value(value)
|
||||||
|
value.is_a?(Array) ? value.first : value
|
||||||
|
end
|
||||||
|
|
||||||
|
def value_or_id(value)
|
||||||
|
value.is_a?(String) || value.nil? ? value : value['id']
|
||||||
|
end
|
||||||
|
|
||||||
|
def supported_context?(json)
|
||||||
|
!json.nil? && equals_or_includes?(json['@context'], ActivityPub::TagManager::CONTEXT)
|
||||||
|
end
|
||||||
|
|
||||||
|
def canonicalize(json)
|
||||||
|
graph = RDF::Graph.new << JSON::LD::API.toRdf(json)
|
||||||
|
graph.dump(:normalize)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_resource(uri)
|
||||||
|
response = build_request(uri).perform
|
||||||
|
return if response.code != 200
|
||||||
|
body_to_json(response.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
def body_to_json(body)
|
||||||
|
body.is_a?(String) ? Oj.load(body, mode: :strict) : body
|
||||||
|
rescue Oj::ParseError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def merge_context(context, new_context)
|
||||||
|
if context.is_a?(Array)
|
||||||
|
context << new_context
|
||||||
|
else
|
||||||
|
[context, new_context]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def build_request(uri)
|
||||||
|
request = Request.new(:get, uri)
|
||||||
|
request.add_headers('Accept' => 'application/activity+json, application/ld+json')
|
||||||
|
request
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -11,7 +11,15 @@ module RoutingHelper
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def full_asset_url(source)
|
def full_asset_url(source, options = {})
|
||||||
Rails.configuration.x.use_s3 ? source : URI.join(root_url, ActionController::Base.helpers.asset_url(source)).to_s
|
source = ActionController::Base.helpers.asset_url(source, options) unless use_storage?
|
||||||
|
|
||||||
|
URI.join(root_url, source).to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def use_storage?
|
||||||
|
Rails.configuration.x.use_s3 || Rails.configuration.x.use_swift
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ module SettingsHelper
|
|||||||
th: 'ภาษาไทย',
|
th: 'ภาษาไทย',
|
||||||
tr: 'Türkçe',
|
tr: 'Türkçe',
|
||||||
uk: 'Українська',
|
uk: 'Українська',
|
||||||
|
zh: '中文',
|
||||||
'zh-CN': '简体中文',
|
'zh-CN': '简体中文',
|
||||||
'zh-HK': '繁體中文(香港)',
|
'zh-HK': '繁體中文(香港)',
|
||||||
'zh-TW': '繁體中文(臺灣)',
|
'zh-TW': '繁體中文(臺灣)',
|
||||||
@@ -39,6 +40,10 @@ module SettingsHelper
|
|||||||
HUMAN_LOCALES[locale]
|
HUMAN_LOCALES[locale]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def filterable_languages
|
||||||
|
I18n.available_locales.map { |locale| locale.to_s.split('-').first.to_sym }.uniq
|
||||||
|
end
|
||||||
|
|
||||||
def hash_to_object(hash)
|
def hash_to_object(hash)
|
||||||
HashObject.new(hash)
|
HashObject.new(hash)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module StreamEntriesHelper
|
module StreamEntriesHelper
|
||||||
EMBEDDED_CONTROLLER = 'stream_entries'
|
EMBEDDED_CONTROLLER = 'statuses'
|
||||||
EMBEDDED_ACTION = 'embed'
|
EMBEDDED_ACTION = 'embed'
|
||||||
|
|
||||||
def display_name(account)
|
def display_name(account)
|
||||||
|
|||||||
@@ -1,5 +1,60 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
`actions/local_settings`
|
||||||
|
========================
|
||||||
|
|
||||||
|
> For more information on the contents of this file, please contact:
|
||||||
|
>
|
||||||
|
> - kibigo! [@kibi@glitch.social]
|
||||||
|
|
||||||
|
This file provides our Redux actions related to local settings. It
|
||||||
|
consists of the following:
|
||||||
|
|
||||||
|
- __`changesLocalSetting(key, value)` :__
|
||||||
|
Changes the local setting with the given `key` to the given
|
||||||
|
`value`. `key` **MUST** be an array of strings, as required by
|
||||||
|
`Immutable.Map.prototype.getIn()`.
|
||||||
|
|
||||||
|
- __`saveLocalSettings()` :__
|
||||||
|
Saves the local settings to `localStorage` as a JSON object. We
|
||||||
|
shouldn't ever need to call this ourselves.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Constants:
|
||||||
|
----------
|
||||||
|
|
||||||
|
We provide the following constants:
|
||||||
|
|
||||||
|
- __`LOCAL_SETTING_CHANGE` :__
|
||||||
|
This string constant is used to dispatch a setting change to our
|
||||||
|
reducer in `reducers/local_settings`, where the setting is
|
||||||
|
actually changed.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
export const LOCAL_SETTING_CHANGE = 'LOCAL_SETTING_CHANGE';
|
export const LOCAL_SETTING_CHANGE = 'LOCAL_SETTING_CHANGE';
|
||||||
|
|
||||||
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
`changeLocalSetting(key, value)`:
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
Changes the local setting with the given `key` to the given `value`.
|
||||||
|
`key` **MUST** be an array of strings, as required by
|
||||||
|
`Immutable.Map.prototype.getIn()`.
|
||||||
|
|
||||||
|
To accomplish this, we just dispatch a `LOCAL_SETTING_CHANGE` to our
|
||||||
|
reducer in `reducers/local_settings`.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
export function changeLocalSetting(key, value) {
|
export function changeLocalSetting(key, value) {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -12,6 +67,24 @@ export function changeLocalSetting(key, value) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
`saveLocalSettings()`:
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Saves the local settings to `localStorage` as a JSON object.
|
||||||
|
`changeLocalSetting()` calls this whenever it changes a setting. We
|
||||||
|
shouldn't ever need to call this ourselves.
|
||||||
|
|
||||||
|
> __TODO :__
|
||||||
|
> Right now `saveLocalSettings()` doesn't keep track of which user
|
||||||
|
> is currently signed in, but it might be better to give each user
|
||||||
|
> their *own* local settings.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
export function saveLocalSettings() {
|
export function saveLocalSettings() {
|
||||||
return (_, getState) => {
|
return (_, getState) => {
|
||||||
const localSettings = getState().get('local_settings').toJS();
|
const localSettings = getState().get('local_settings').toJS();
|
||||||
|
|||||||
@@ -1,3 +1,45 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
`<AccountHeader>`
|
||||||
|
=================
|
||||||
|
|
||||||
|
> For more information on the contents of this file, please contact:
|
||||||
|
>
|
||||||
|
> - kibigo! [@kibi@glitch.social]
|
||||||
|
|
||||||
|
Original file by @gargron@mastodon.social et al as part of
|
||||||
|
tootsuite/mastodon. We've expanded it in order to handle user bio
|
||||||
|
frontmatter.
|
||||||
|
|
||||||
|
The `<AccountHeader>` component provides the header for account
|
||||||
|
timelines. It is a fairly simple component which mostly just consists
|
||||||
|
of a `render()` method.
|
||||||
|
|
||||||
|
__Props:__
|
||||||
|
|
||||||
|
- __`account` (`ImmutablePropTypes.map`) :__
|
||||||
|
The account to render a header for.
|
||||||
|
|
||||||
|
- __`me` (`PropTypes.number.isRequired`) :__
|
||||||
|
The id of the currently-signed-in account.
|
||||||
|
|
||||||
|
- __`onFollow` (`PropTypes.func.isRequired`) :__
|
||||||
|
The function to call when the user clicks the "follow" button.
|
||||||
|
|
||||||
|
- __`intl` (`PropTypes.object.isRequired`) :__
|
||||||
|
Our internationalization object, inserted by `@injectIntl`.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Imports:
|
||||||
|
--------
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
// Package imports //
|
// Package imports //
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
@@ -14,14 +56,36 @@ import Avatar from '../../../mastodon/components/avatar';
|
|||||||
// Our imports //
|
// Our imports //
|
||||||
import { processBio } from '../../util/bio_metadata';
|
import { processBio } from '../../util/bio_metadata';
|
||||||
|
|
||||||
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Inital setup:
|
||||||
|
-------------
|
||||||
|
|
||||||
|
The `messages` constant is used to define any messages that we need
|
||||||
|
from inside props. In our case, these are the `unfollow`, `follow`, and
|
||||||
|
`requested` messages used in the `title` of our buttons.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
---------------
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
@injectIntl
|
@injectIntl
|
||||||
export default class Header extends ImmutablePureComponent {
|
export default class AccountHeader extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account : ImmutablePropTypes.map,
|
account : ImmutablePropTypes.map,
|
||||||
@@ -30,9 +94,25 @@ export default class Header extends ImmutablePureComponent {
|
|||||||
intl : PropTypes.object.isRequired,
|
intl : PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
### `render()`
|
||||||
|
|
||||||
|
The `render()` function is used to render our component.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, me, intl } = this.props;
|
const { account, me, intl } = this.props;
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
If no `account` is provided, then we can't render a header. Otherwise,
|
||||||
|
we get the `displayName` for the account, if available. If it's blank,
|
||||||
|
then we set the `displayName` to just be the `username` of the account.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -40,17 +120,30 @@ export default class Header extends ImmutablePureComponent {
|
|||||||
let displayName = account.get('display_name');
|
let displayName = account.get('display_name');
|
||||||
let info = '';
|
let info = '';
|
||||||
let actionBtn = '';
|
let actionBtn = '';
|
||||||
let lockedIcon = '';
|
let following = false;
|
||||||
|
|
||||||
if (displayName.length === 0) {
|
if (displayName.length === 0) {
|
||||||
displayName = account.get('username');
|
displayName = account.get('username');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
|
/*
|
||||||
info = <span className='account--follows-info'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>;
|
|
||||||
}
|
Next, we handle the account relationships. If the account follows the
|
||||||
|
user, then we add an `info` message. If the user has requested a
|
||||||
|
follow, then we disable the `actionBtn` and display an hourglass.
|
||||||
|
Otherwise, if the account isn't blocked, we set the `actionBtn` to the
|
||||||
|
appropriate icon.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
if (me !== account.get('id')) {
|
if (me !== account.get('id')) {
|
||||||
|
if (account.getIn(['relationship', 'followed_by'])) {
|
||||||
|
info = (
|
||||||
|
<span className='account--follows-info'>
|
||||||
|
<FormattedMessage id='account.follows_you' defaultMessage='Follows you' />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (account.getIn(['relationship', 'requested'])) {
|
if (account.getIn(['relationship', 'requested'])) {
|
||||||
actionBtn = (
|
actionBtn = (
|
||||||
<div className='account--action-button'>
|
<div className='account--action-button'>
|
||||||
@@ -58,30 +151,60 @@ export default class Header extends ImmutablePureComponent {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (!account.getIn(['relationship', 'blocking'])) {
|
} else if (!account.getIn(['relationship', 'blocking'])) {
|
||||||
|
following = account.getIn(['relationship', 'following']);
|
||||||
actionBtn = (
|
actionBtn = (
|
||||||
<div className='account--action-button'>
|
<div className='account--action-button'>
|
||||||
<IconButton size={26} icon={account.getIn(['relationship', 'following']) ? 'user-times' : 'user-plus'} active={account.getIn(['relationship', 'following'])} title={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />
|
<IconButton
|
||||||
|
size={26}
|
||||||
|
icon={following ? 'user-times' : 'user-plus'}
|
||||||
|
active={following}
|
||||||
|
title={intl.formatMessage(following ? messages.unfollow : messages.follow)}
|
||||||
|
onClick={this.props.onFollow}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (account.get('locked')) {
|
/*
|
||||||
lockedIcon = <i className='fa fa-lock' />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
|
`displayNameHTML` processes the `displayName` and prepares it for
|
||||||
|
insertion into the document. Meanwhile, we extract the `text` and
|
||||||
|
`metadata` from our account's `note` using `processBio()`.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
const displayNameHTML = {
|
||||||
|
__html : emojify(escapeTextContentForBrowser(displayName)),
|
||||||
|
};
|
||||||
const { text, metadata } = processBio(account.get('note'));
|
const { text, metadata } = processBio(account.get('note'));
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Here, we render our component using all the things we've defined above.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='account__header__wrapper'>
|
<div className='account__header__wrapper'>
|
||||||
<div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
|
<div
|
||||||
|
className='account__header'
|
||||||
|
style={{ backgroundImage: `url(${account.get('header')})` }}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<a href={account.get('url')} target='_blank' rel='noopener'>
|
<a href={account.get('url')} target='_blank' rel='noopener'>
|
||||||
<span className='account__header__avatar'><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={90} /></span>
|
<span className='account__header__avatar'>
|
||||||
<span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} />
|
<Avatar account={account} size={90} />
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className='account__header__display-name'
|
||||||
|
dangerouslySetInnerHTML={displayNameHTML}
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
<span className='account__header__username'>@{account.get('acct')} {lockedIcon}</span>
|
<span className='account__header__username'>
|
||||||
|
@{account.get('acct')}
|
||||||
|
{account.get('locked') ? <i className='fa fa-lock' /> : null}
|
||||||
|
</span>
|
||||||
<div className='account__header__content' dangerouslySetInnerHTML={{ __html: emojify(text) }} />
|
<div className='account__header__content' dangerouslySetInnerHTML={{ __html: emojify(text) }} />
|
||||||
|
|
||||||
{info}
|
{info}
|
||||||
@@ -91,6 +214,7 @@ export default class Header extends ImmutablePureComponent {
|
|||||||
|
|
||||||
{metadata.length && (
|
{metadata.length && (
|
||||||
<table className='account__metadata'>
|
<table className='account__metadata'>
|
||||||
|
<tbody>
|
||||||
{(() => {
|
{(() => {
|
||||||
let data = [];
|
let data = [];
|
||||||
for (let i = 0; i < metadata.length; i++) {
|
for (let i = 0; i < metadata.length; i++) {
|
||||||
@@ -103,6 +227,7 @@ export default class Header extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
})()}
|
})()}
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
) || null}
|
) || null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
`<NotificationPurgeButtonsContainer>`
|
||||||
|
=========================
|
||||||
|
|
||||||
|
This container connects `<NotificationPurgeButtons>`s to the Redux store.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Imports:
|
||||||
|
--------
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package imports //
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
// Our imports //
|
||||||
|
import NotificationPurgeButtons from './notification_purge_buttons';
|
||||||
|
import {
|
||||||
|
deleteMarkedNotifications,
|
||||||
|
enterNotificationClearingMode,
|
||||||
|
markAllNotifications,
|
||||||
|
} from '../../../../mastodon/actions/notifications';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
import { openModal } from '../../../../mastodon/actions/modal';
|
||||||
|
|
||||||
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Dispatch mapping:
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
The `mapDispatchToProps()` function maps dispatches to our store to the
|
||||||
|
various props of our component. We only need to provide a dispatch for
|
||||||
|
deleting notifications.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
clearMessage: { id: 'notifications.marked_clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all selected notifications?' },
|
||||||
|
clearConfirm: { id: 'notifications.marked_clear', defaultMessage: 'Clear selected notifications' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
|
onEnterCleaningMode(yes) {
|
||||||
|
dispatch(enterNotificationClearingMode(yes));
|
||||||
|
},
|
||||||
|
|
||||||
|
onDeleteMarked() {
|
||||||
|
dispatch(openModal('CONFIRM', {
|
||||||
|
message: intl.formatMessage(messages.clearMessage),
|
||||||
|
confirm: intl.formatMessage(messages.clearConfirm),
|
||||||
|
onConfirm: () => dispatch(deleteMarkedNotifications()),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
onMarkAll() {
|
||||||
|
dispatch(markAllNotifications(true));
|
||||||
|
},
|
||||||
|
|
||||||
|
onMarkNone() {
|
||||||
|
dispatch(markAllNotifications(false));
|
||||||
|
},
|
||||||
|
|
||||||
|
onInvert() {
|
||||||
|
dispatch(markAllNotifications(null));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
markNewForDelete: state.getIn(['notifications', 'markNewForDelete']),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationPurgeButtons));
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Buttons widget for controlling the notification clearing mode.
|
||||||
|
* In idle state, the cleaning mode button is shown. When the mode is active,
|
||||||
|
* a Confirm and Abort buttons are shown in its place.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// Package imports //
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
|
// Mastodon imports //
|
||||||
|
|
||||||
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
btnAll : { id: 'notification_purge.btn_all', defaultMessage: 'Select\nall' },
|
||||||
|
btnNone : { id: 'notification_purge.btn_none', defaultMessage: 'Select\nnone' },
|
||||||
|
btnInvert : { id: 'notification_purge.btn_invert', defaultMessage: 'Invert\nselection' },
|
||||||
|
btnApply : { id: 'notification_purge.btn_apply', defaultMessage: 'Clear\nselected' },
|
||||||
|
});
|
||||||
|
|
||||||
|
@injectIntl
|
||||||
|
export default class NotificationPurgeButtons extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
onDeleteMarked : PropTypes.func.isRequired,
|
||||||
|
onMarkAll : PropTypes.func.isRequired,
|
||||||
|
onMarkNone : PropTypes.func.isRequired,
|
||||||
|
onInvert : PropTypes.func.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
markNewForDelete: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { intl, markNewForDelete } = this.props;
|
||||||
|
|
||||||
|
//className='active'
|
||||||
|
return (
|
||||||
|
<div className='column-header__notif-cleaning-buttons'>
|
||||||
|
<button onClick={this.props.onMarkAll} className={markNewForDelete ? 'active' : ''}>
|
||||||
|
<b>∀</b><br />{intl.formatMessage(messages.btnAll)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onClick={this.props.onMarkNone} className={!markNewForDelete ? 'active' : ''}>
|
||||||
|
<b>∅</b><br />{intl.formatMessage(messages.btnNone)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onClick={this.props.onInvert}>
|
||||||
|
<b>¬</b><br />{intl.formatMessage(messages.btnInvert)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button onClick={this.props.onDeleteMarked}>
|
||||||
|
<i className='fa fa-trash' /><br />{intl.formatMessage(messages.btnApply)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
// Package imports //
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import Toggle from 'react-toggle';
|
|
||||||
import { injectIntl, defineMessages } from 'react-intl';
|
|
||||||
|
|
||||||
// Mastodon imports //
|
|
||||||
import IconButton from '../../../mastodon/components/icon_button';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
local_only_short: { id: 'advanced-options.local-only.short', defaultMessage: 'Local-only' },
|
|
||||||
local_only_long: { id: 'advanced-options.local-only.long', defaultMessage: 'Do not post to other instances' },
|
|
||||||
advanced_options_icon_title: { id: 'advanced_options.icon_title', defaultMessage: 'Advanced options' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const iconStyle = {
|
|
||||||
height: null,
|
|
||||||
lineHeight: '27px',
|
|
||||||
};
|
|
||||||
|
|
||||||
@injectIntl
|
|
||||||
export default class ComposeAdvancedOptions extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
values: ImmutablePropTypes.contains({
|
|
||||||
do_not_federate: PropTypes.bool.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
onToggleDropdown = () => {
|
|
||||||
this.setState({ open: !this.state.open });
|
|
||||||
};
|
|
||||||
|
|
||||||
onGlobalClick = (e) => {
|
|
||||||
if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) {
|
|
||||||
this.setState({ open: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
window.addEventListener('click', this.onGlobalClick);
|
|
||||||
window.addEventListener('touchstart', this.onGlobalClick);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
window.removeEventListener('click', this.onGlobalClick);
|
|
||||||
window.removeEventListener('touchstart', this.onGlobalClick);
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
|
||||||
open: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClick = (e) => {
|
|
||||||
const option = e.currentTarget.getAttribute('data-index');
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onChange(option);
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleHandler(option) {
|
|
||||||
return () => this.props.onChange(option);
|
|
||||||
}
|
|
||||||
|
|
||||||
setRef = (c) => {
|
|
||||||
this.node = c;
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { open } = this.state;
|
|
||||||
const { intl, values } = this.props;
|
|
||||||
|
|
||||||
const options = [
|
|
||||||
{ icon: 'wifi', shortText: messages.local_only_short, longText: messages.local_only_long, key: 'do_not_federate' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const anyEnabled = values.some((enabled) => enabled);
|
|
||||||
const optionElems = options.map((option) => {
|
|
||||||
const active = values.get(option.key);
|
|
||||||
return (
|
|
||||||
<div role='button' className='advanced-options-dropdown__option' key={option.key} >
|
|
||||||
<div className='advanced-options-dropdown__option__toggle'>
|
|
||||||
<Toggle checked={active} onChange={this.toggleHandler(option.key)} />
|
|
||||||
</div>
|
|
||||||
<div className='advanced-options-dropdown__option__content'>
|
|
||||||
<strong>{intl.formatMessage(option.shortText)}</strong>
|
|
||||||
{intl.formatMessage(option.longText)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (<div ref={this.setRef} className={`advanced-options-dropdown ${open ? 'open' : ''} ${anyEnabled ? 'active' : ''} `}>
|
|
||||||
<div className='advanced-options-dropdown__value'>
|
|
||||||
<IconButton
|
|
||||||
className='advanced-options-dropdown__value'
|
|
||||||
title={intl.formatMessage(messages.advanced_options_icon_title)}
|
|
||||||
icon='ellipsis-h' active={open || anyEnabled}
|
|
||||||
size={18}
|
|
||||||
style={iconStyle}
|
|
||||||
onClick={this.onToggleDropdown}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='advanced-options-dropdown__dropdown'>
|
|
||||||
{optionElems}
|
|
||||||
</div>
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
`<ComposeAdvancedOptionsContainer>`
|
||||||
|
===================================
|
||||||
|
|
||||||
|
This container connects `<ComposeAdvancedOptions>` to the Redux store.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Imports:
|
||||||
|
--------
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package imports //
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
// Mastodon imports //
|
||||||
|
import { toggleComposeAdvancedOption } from '../../../../mastodon/actions/compose';
|
||||||
|
|
||||||
|
// Our imports //
|
||||||
|
import ComposeAdvancedOptions from '.';
|
||||||
|
|
||||||
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
State mapping:
|
||||||
|
--------------
|
||||||
|
|
||||||
|
The `mapStateToProps()` function maps various state properties to the
|
||||||
|
props of our component. The only property we care about is
|
||||||
|
`compose.advanced_options`.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
values: state.getIn(['compose', 'advanced_options']),
|
||||||
|
});
|
||||||
|
|
||||||
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Dispatch mapping:
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
The `mapDispatchToProps()` function maps dispatches to our store to the
|
||||||
|
various props of our component. We just need to provide a dispatch for
|
||||||
|
when an advanced option toggle changes.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
||||||
|
onChange (option) {
|
||||||
|
dispatch(toggleComposeAdvancedOption(option));
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ComposeAdvancedOptions);
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
`<ComposeAdvancedOptions>`
|
||||||
|
==========================
|
||||||
|
|
||||||
|
> For more information on the contents of this file, please contact:
|
||||||
|
>
|
||||||
|
> - surinna [@srn@dev.glitch.social]
|
||||||
|
|
||||||
|
This adds an advanced options dropdown to the toot compose box, for
|
||||||
|
toggles that don't necessarily fit elsewhere.
|
||||||
|
|
||||||
|
__Props:__
|
||||||
|
|
||||||
|
- __`values` (`ImmutablePropTypes.contains(…).isRequired`) :__
|
||||||
|
An Immutable map with the following values:
|
||||||
|
|
||||||
|
- __`do_not_federate` (`PropTypes.bool.isRequired`) :__
|
||||||
|
Specifies whether or not to federate the status.
|
||||||
|
|
||||||
|
- __`onChange` (`PropTypes.func.isRequired`) :__
|
||||||
|
The function to call when a toggle is changed. We pass this from
|
||||||
|
our container to the toggle.
|
||||||
|
|
||||||
|
- __`intl` (`PropTypes.object.isRequired`) :__
|
||||||
|
Our internationalization object, inserted by `@injectIntl`.
|
||||||
|
|
||||||
|
__State:__
|
||||||
|
|
||||||
|
- __`open` :__
|
||||||
|
This tells whether the dropdown is currently open or closed.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Imports:
|
||||||
|
--------
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package imports //
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
// Mastodon imports //
|
||||||
|
import IconButton from '../../../../mastodon/components/icon_button';
|
||||||
|
|
||||||
|
// Our imports //
|
||||||
|
import ComposeAdvancedOptionsToggle from './toggle';
|
||||||
|
|
||||||
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Inital setup:
|
||||||
|
-------------
|
||||||
|
|
||||||
|
The `messages` constant is used to define any messages that we need
|
||||||
|
from inside props. These are the various titles and labels on our
|
||||||
|
toggles.
|
||||||
|
|
||||||
|
`iconStyle` styles the icon used for the dropdown button.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
local_only_short :
|
||||||
|
{ id: 'advanced-options.local-only.short', defaultMessage: 'Local-only' },
|
||||||
|
local_only_long :
|
||||||
|
{ id: 'advanced-options.local-only.long', defaultMessage: 'Do not post to other instances' },
|
||||||
|
advanced_options_icon_title :
|
||||||
|
{ id: 'advanced_options.icon_title', defaultMessage: 'Advanced options' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const iconStyle = {
|
||||||
|
height : null,
|
||||||
|
lineHeight : '27px',
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
---------------
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
@injectIntl
|
||||||
|
export default class ComposeAdvancedOptions extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
values : ImmutablePropTypes.contains({
|
||||||
|
do_not_federate : PropTypes.bool.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
onChange : PropTypes.func.isRequired,
|
||||||
|
intl : PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
open: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
### `onToggleDropdown()`
|
||||||
|
|
||||||
|
This function toggles the opening and closing of the advanced options
|
||||||
|
dropdown.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
onToggleDropdown = () => {
|
||||||
|
this.setState({ open: !this.state.open });
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
### `onGlobalClick(e)`
|
||||||
|
|
||||||
|
This function closes the advanced options dropdown if you click
|
||||||
|
anywhere else on the screen.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
onGlobalClick = (e) => {
|
||||||
|
if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) {
|
||||||
|
this.setState({ open: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
### `componentDidMount()`, `componentWillUnmount()`
|
||||||
|
|
||||||
|
This function closes the advanced options dropdown if you click
|
||||||
|
anywhere else on the screen.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
window.addEventListener('click', this.onGlobalClick);
|
||||||
|
window.addEventListener('touchstart', this.onGlobalClick);
|
||||||
|
}
|
||||||
|
componentWillUnmount () {
|
||||||
|
window.removeEventListener('click', this.onGlobalClick);
|
||||||
|
window.removeEventListener('touchstart', this.onGlobalClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
### `setRef(c)`
|
||||||
|
|
||||||
|
`setRef()` stores a reference to the dropdown's `<div> in `this.node`.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
setRef = (c) => {
|
||||||
|
this.node = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
### `render()`
|
||||||
|
|
||||||
|
`render()` actually puts our component on the screen.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { open } = this.state;
|
||||||
|
const { intl, values } = this.props;
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
The `options` array provides all of the available advanced options
|
||||||
|
alongside their icon, text, and name.
|
||||||
|
|
||||||
|
*/
|
||||||
|
const options = [
|
||||||
|
{ icon: 'wifi', shortText: messages.local_only_short, longText: messages.local_only_long, name: 'do_not_federate' },
|
||||||
|
];
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
`anyEnabled` tells us if any of our advanced options have been enabled.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
const anyEnabled = values.some((enabled) => enabled);
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
`optionElems` takes our `options` and creates
|
||||||
|
`<ComposeAdvancedOptionsToggle>`s out of them. We use the `name` of the
|
||||||
|
toggle as its `key` so that React can keep track of it.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
const optionElems = options.map((option) => {
|
||||||
|
return (
|
||||||
|
<ComposeAdvancedOptionsToggle
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
active={values.get(option.name)}
|
||||||
|
key={option.name}
|
||||||
|
name={option.name}
|
||||||
|
shortText={intl.formatMessage(option.shortText)}
|
||||||
|
longText={intl.formatMessage(option.longText)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Finally, we can render our component.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={this.setRef} className={`advanced-options-dropdown ${open ? 'open' : ''} ${anyEnabled ? 'active' : ''} `}>
|
||||||
|
<div className='advanced-options-dropdown__value'>
|
||||||
|
<IconButton
|
||||||
|
className='advanced-options-dropdown__value'
|
||||||
|
title={intl.formatMessage(messages.advanced_options_icon_title)}
|
||||||
|
icon='ellipsis-h' active={open || anyEnabled}
|
||||||
|
size={18}
|
||||||
|
style={iconStyle}
|
||||||
|
onClick={this.onToggleDropdown}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='advanced-options-dropdown__dropdown'>
|
||||||
|
{optionElems}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
`<ComposeAdvancedOptionsToggle>`
|
||||||
|
================================
|
||||||
|
|
||||||
|
> For more information on the contents of this file, please contact:
|
||||||
|
>
|
||||||
|
> - surinna [@srn@dev.glitch.social]
|
||||||
|
|
||||||
|
This creates the toggle used by `<ComposeAdvancedOptions>`.
|
||||||
|
|
||||||
|
__Props:__
|
||||||
|
|
||||||
|
- __`onChange` (`PropTypes.func`) :__
|
||||||
|
This provides the function to call when the toggle is
|
||||||
|
(de-?)activated.
|
||||||
|
|
||||||
|
- __`active` (`PropTypes.bool`) :__
|
||||||
|
This prop controls whether the toggle is currently active or not.
|
||||||
|
|
||||||
|
- __`name` (`PropTypes.string`) :__
|
||||||
|
This identifies the toggle, and is sent to `onChange()` when it is
|
||||||
|
called.
|
||||||
|
|
||||||
|
- __`shortText` (`PropTypes.string`) :__
|
||||||
|
This is a short string used as the title of the toggle.
|
||||||
|
|
||||||
|
- __`longText` (`PropTypes.string`) :__
|
||||||
|
This is a longer string used as a subtitle for the toggle.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Imports:
|
||||||
|
--------
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package imports //
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Toggle from 'react-toggle';
|
||||||
|
|
||||||
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Implementation:
|
||||||
|
---------------
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class ComposeAdvancedOptionsToggle extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
active: PropTypes.bool.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
shortText: PropTypes.string.isRequired,
|
||||||
|
longText: PropTypes.string.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
### `onToggle()`
|
||||||
|
|
||||||
|
The `onToggle()` function simply calls the `onChange()` prop with the
|
||||||
|
toggle's `name`.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
onToggle = () => {
|
||||||
|
this.props.onChange(this.props.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
### `render()`
|
||||||
|
|
||||||
|
The `render()` function is used to render our component. We just render
|
||||||
|
a `<Toggle>` and place next to it our text.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { active, shortText, longText } = this.props;
|
||||||
|
return (
|
||||||
|
<div role='button' tabIndex='0' className='advanced-options-dropdown__option' onClick={this.onToggle}>
|
||||||
|
<div className='advanced-options-dropdown__option__toggle'>
|
||||||
|
<Toggle checked={active} onChange={this.onToggle} />
|
||||||
|
</div>
|
||||||
|
<div className='advanced-options-dropdown__option__content'>
|
||||||
|
<strong>{shortText}</strong>
|
||||||
|
{longText}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
24
app/javascript/glitch/components/local_settings/container.js
Normal file
24
app/javascript/glitch/components/local_settings/container.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// Package imports //
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
// Mastodon imports //
|
||||||
|
import { closeModal } from 'mastodon/actions/modal';
|
||||||
|
|
||||||
|
// Our imports //
|
||||||
|
import { changeLocalSetting } from 'glitch/actions/local_settings';
|
||||||
|
import LocalSettings from '.';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
settings: state.get('local_settings'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
onChange (setting, value) {
|
||||||
|
dispatch(changeLocalSetting(setting, value));
|
||||||
|
},
|
||||||
|
onClose () {
|
||||||
|
dispatch(closeModal());
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(LocalSettings);
|
||||||
50
app/javascript/glitch/components/local_settings/index.js
Normal file
50
app/javascript/glitch/components/local_settings/index.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// Package imports
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
|
||||||
|
// Our imports
|
||||||
|
import LocalSettingsPage from './page';
|
||||||
|
import LocalSettingsNavigation from './navigation';
|
||||||
|
|
||||||
|
// Stylesheet imports
|
||||||
|
import './style';
|
||||||
|
|
||||||
|
export default class LocalSettings extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
currentIndex: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
navigateTo = (index) =>
|
||||||
|
this.setState({ currentIndex: +index });
|
||||||
|
|
||||||
|
render () {
|
||||||
|
|
||||||
|
const { navigateTo } = this;
|
||||||
|
const { onChange, onClose, settings } = this.props;
|
||||||
|
const { currentIndex } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='glitch modal-root__modal local-settings'>
|
||||||
|
<LocalSettingsNavigation
|
||||||
|
index={currentIndex}
|
||||||
|
onClose={onClose}
|
||||||
|
onNavigate={navigateTo}
|
||||||
|
/>
|
||||||
|
<LocalSettingsPage
|
||||||
|
index={currentIndex}
|
||||||
|
onChange={onChange}
|
||||||
|
settings={settings}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
// Package imports
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
// Our imports
|
||||||
|
import LocalSettingsNavigationItem from './item';
|
||||||
|
|
||||||
|
// Stylesheet imports
|
||||||
|
import './style';
|
||||||
|
|
||||||
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
general: { id: 'settings.general', defaultMessage: 'General' },
|
||||||
|
collapsed: { id: 'settings.collapsed_statuses', defaultMessage: 'Collapsed toots' },
|
||||||
|
media: { id: 'settings.media', defaultMessage: 'Media' },
|
||||||
|
preferences: { id: 'settings.preferences', defaultMessage: 'Preferences' },
|
||||||
|
close: { id: 'settings.close', defaultMessage: 'Close' },
|
||||||
|
});
|
||||||
|
|
||||||
|
@injectIntl
|
||||||
|
export default class LocalSettingsNavigation extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
index : PropTypes.number,
|
||||||
|
intl : PropTypes.object.isRequired,
|
||||||
|
onClose : PropTypes.func.isRequired,
|
||||||
|
onNavigate : PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
|
||||||
|
const { index, intl, onClose, onNavigate } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className='glitch local-settings__navigation'>
|
||||||
|
<LocalSettingsNavigationItem
|
||||||
|
active={index === 0}
|
||||||
|
index={0}
|
||||||
|
onNavigate={onNavigate}
|
||||||
|
title={intl.formatMessage(messages.general)}
|
||||||
|
/>
|
||||||
|
<LocalSettingsNavigationItem
|
||||||
|
active={index === 1}
|
||||||
|
index={1}
|
||||||
|
onNavigate={onNavigate}
|
||||||
|
title={intl.formatMessage(messages.collapsed)}
|
||||||
|
/>
|
||||||
|
<LocalSettingsNavigationItem
|
||||||
|
active={index === 2}
|
||||||
|
index={2}
|
||||||
|
onNavigate={onNavigate}
|
||||||
|
title={intl.formatMessage(messages.media)}
|
||||||
|
/>
|
||||||
|
<LocalSettingsNavigationItem
|
||||||
|
active={index === 3}
|
||||||
|
href='/settings/preferences'
|
||||||
|
index={3}
|
||||||
|
icon='cog'
|
||||||
|
title={intl.formatMessage(messages.preferences)}
|
||||||
|
/>
|
||||||
|
<LocalSettingsNavigationItem
|
||||||
|
active={index === 4}
|
||||||
|
className='close'
|
||||||
|
index={4}
|
||||||
|
onNavigate={onClose}
|
||||||
|
title={intl.formatMessage(messages.close)}
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
// Package imports
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
// Stylesheet imports
|
||||||
|
import './style';
|
||||||
|
|
||||||
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
|
export default class LocalSettingsPage extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
active: PropTypes.bool,
|
||||||
|
className: PropTypes.string,
|
||||||
|
href: PropTypes.string,
|
||||||
|
icon: PropTypes.string,
|
||||||
|
index: PropTypes.number.isRequired,
|
||||||
|
onNavigate: PropTypes.func,
|
||||||
|
title: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = (e) => {
|
||||||
|
const { index, onNavigate } = this.props;
|
||||||
|
if (onNavigate) {
|
||||||
|
onNavigate(index);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { handleClick } = this;
|
||||||
|
const {
|
||||||
|
active,
|
||||||
|
className,
|
||||||
|
href,
|
||||||
|
icon,
|
||||||
|
onNavigate,
|
||||||
|
title,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const finalClassName = classNames('glitch', 'local-settings__navigation__item', {
|
||||||
|
active,
|
||||||
|
}, className);
|
||||||
|
|
||||||
|
const iconElem = icon ? <i className={`fa fa-fw fa-${icon}`} /> : null;
|
||||||
|
|
||||||
|
if (href) return (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
className={finalClassName}
|
||||||
|
>
|
||||||
|
{iconElem} {title}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
else if (onNavigate) return (
|
||||||
|
<a
|
||||||
|
onClick={handleClick}
|
||||||
|
role='button'
|
||||||
|
tabIndex='0'
|
||||||
|
className={finalClassName}
|
||||||
|
>
|
||||||
|
{iconElem} {title}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
else return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
@import 'variables';
|
||||||
|
|
||||||
|
.glitch.local-settings__navigation__item {
|
||||||
|
display: block;
|
||||||
|
padding: 15px 20px;
|
||||||
|
color: inherit;
|
||||||
|
background: $primary-text-color;
|
||||||
|
border-bottom: 1px $ui-primary-color solid;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
outline: none;
|
||||||
|
transition: background .3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $ui-secondary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: $ui-highlight-color;
|
||||||
|
color: $primary-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.close, &.close:hover {
|
||||||
|
background: $error-value-color;
|
||||||
|
color: $primary-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
@import 'variables';
|
||||||
|
|
||||||
|
.glitch.local-settings__navigation {
|
||||||
|
background: $primary-text-color;
|
||||||
|
color: $ui-base-color;
|
||||||
|
width: 200px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
// Package imports //
|
// Package imports
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
// Our imports //
|
// Our imports
|
||||||
import SettingsItem from './item';
|
import LocalSettingsPageItem from './item';
|
||||||
|
|
||||||
|
// Stylesheet imports
|
||||||
|
import './style';
|
||||||
|
|
||||||
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
layout_auto: { id: 'layout.auto', defaultMessage: 'Auto' },
|
layout_auto: { id: 'layout.auto', defaultMessage: 'Auto' },
|
||||||
@@ -14,27 +19,21 @@ const messages = defineMessages({
|
|||||||
});
|
});
|
||||||
|
|
||||||
@injectIntl
|
@injectIntl
|
||||||
export default class Settings extends React.PureComponent {
|
export default class LocalSettingsPage extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
settings: ImmutablePropTypes.map.isRequired,
|
index : PropTypes.number,
|
||||||
toggleSetting: PropTypes.func.isRequired,
|
|
||||||
changeSetting: PropTypes.func.isRequired,
|
|
||||||
onClose: PropTypes.func.isRequired,
|
|
||||||
intl : PropTypes.object.isRequired,
|
intl : PropTypes.object.isRequired,
|
||||||
|
onChange : PropTypes.func.isRequired,
|
||||||
|
settings : ImmutablePropTypes.map.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
pages = [
|
||||||
currentIndex: 0,
|
({ intl, onChange, settings }) => (
|
||||||
};
|
<div className='glitch local-settings__page general'>
|
||||||
|
|
||||||
General = () => {
|
|
||||||
const { intl } = this.props;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1><FormattedMessage id='settings.general' defaultMessage='General' /></h1>
|
<h1><FormattedMessage id='settings.general' defaultMessage='General' /></h1>
|
||||||
<SettingsItem
|
<LocalSettingsPageItem
|
||||||
settings={this.props.settings}
|
settings={settings}
|
||||||
item={['layout']}
|
item={['layout']}
|
||||||
id='mastodon-settings--layout'
|
id='mastodon-settings--layout'
|
||||||
options={[
|
options={[
|
||||||
@@ -42,180 +41,143 @@ export default class Settings extends React.PureComponent {
|
|||||||
{ value: 'multiple', message: intl.formatMessage(messages.layout_desktop) },
|
{ value: 'multiple', message: intl.formatMessage(messages.layout_desktop) },
|
||||||
{ value: 'single', message: intl.formatMessage(messages.layout_mobile) },
|
{ value: 'single', message: intl.formatMessage(messages.layout_mobile) },
|
||||||
]}
|
]}
|
||||||
onChange={this.props.changeSetting}
|
onChange={onChange}
|
||||||
>
|
>
|
||||||
<FormattedMessage id='settings.layout' defaultMessage='Layout:' />
|
<FormattedMessage id='settings.layout' defaultMessage='Layout:' />
|
||||||
</SettingsItem>
|
</LocalSettingsPageItem>
|
||||||
|
<LocalSettingsPageItem
|
||||||
<SettingsItem
|
settings={settings}
|
||||||
settings={this.props.settings}
|
|
||||||
item={['stretch']}
|
item={['stretch']}
|
||||||
id='mastodon-settings--stretch'
|
id='mastodon-settings--stretch'
|
||||||
onChange={this.props.toggleSetting}
|
onChange={onChange}
|
||||||
>
|
>
|
||||||
<FormattedMessage id='settings.wide_view' defaultMessage='Wide view (Desktop mode only)' />
|
<FormattedMessage id='settings.wide_view' defaultMessage='Wide view (Desktop mode only)' />
|
||||||
</SettingsItem>
|
</LocalSettingsPageItem>
|
||||||
|
<LocalSettingsPageItem
|
||||||
|
settings={settings}
|
||||||
|
item={['navbar_under']}
|
||||||
|
id='mastodon-settings--navbar_under'
|
||||||
|
onChange={onChange}
|
||||||
|
>
|
||||||
|
<FormattedMessage id='settings.navbar_under' defaultMessage='Navbar at the bottom (Mobile only)' />
|
||||||
|
</LocalSettingsPageItem>
|
||||||
</div>
|
</div>
|
||||||
);
|
),
|
||||||
}
|
({ onChange, settings }) => (
|
||||||
|
<div className='glitch local-settings__page collapsed'>
|
||||||
CollapsedStatuses = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1><FormattedMessage id='settings.collapsed_statuses' defaultMessage='Collapsed toots' /></h1>
|
<h1><FormattedMessage id='settings.collapsed_statuses' defaultMessage='Collapsed toots' /></h1>
|
||||||
<SettingsItem
|
<LocalSettingsPageItem
|
||||||
settings={this.props.settings}
|
settings={settings}
|
||||||
item={['collapsed', 'enabled']}
|
item={['collapsed', 'enabled']}
|
||||||
id='mastodon-settings--collapsed-enabled'
|
id='mastodon-settings--collapsed-enabled'
|
||||||
onChange={this.props.toggleSetting}
|
onChange={onChange}
|
||||||
>
|
>
|
||||||
<FormattedMessage id='settings.enable_collapsed' defaultMessage='Enable collapsed toots' />
|
<FormattedMessage id='settings.enable_collapsed' defaultMessage='Enable collapsed toots' />
|
||||||
</SettingsItem>
|
</LocalSettingsPageItem>
|
||||||
<section>
|
<section>
|
||||||
<h2><FormattedMessage id='settings.auto_collapse' defaultMessage='Automatic collapsing' /></h2>
|
<h2><FormattedMessage id='settings.auto_collapse' defaultMessage='Automatic collapsing' /></h2>
|
||||||
<SettingsItem
|
<LocalSettingsPageItem
|
||||||
settings={this.props.settings}
|
settings={settings}
|
||||||
item={['collapsed', 'auto', 'all']}
|
item={['collapsed', 'auto', 'all']}
|
||||||
id='mastodon-settings--collapsed-auto-all'
|
id='mastodon-settings--collapsed-auto-all'
|
||||||
onChange={this.props.toggleSetting}
|
onChange={onChange}
|
||||||
dependsOn={[['collapsed', 'enabled']]}
|
dependsOn={[['collapsed', 'enabled']]}
|
||||||
>
|
>
|
||||||
<FormattedMessage id='settings.auto_collapse_all' defaultMessage='Everything' />
|
<FormattedMessage id='settings.auto_collapse_all' defaultMessage='Everything' />
|
||||||
</SettingsItem>
|
</LocalSettingsPageItem>
|
||||||
<SettingsItem
|
<LocalSettingsPageItem
|
||||||
settings={this.props.settings}
|
settings={settings}
|
||||||
item={['collapsed', 'auto', 'notifications']}
|
item={['collapsed', 'auto', 'notifications']}
|
||||||
id='mastodon-settings--collapsed-auto-notifications'
|
id='mastodon-settings--collapsed-auto-notifications'
|
||||||
onChange={this.props.toggleSetting}
|
onChange={onChange}
|
||||||
dependsOn={[['collapsed', 'enabled']]}
|
dependsOn={[['collapsed', 'enabled']]}
|
||||||
dependsOnNot={[['collapsed', 'auto', 'all']]}
|
dependsOnNot={[['collapsed', 'auto', 'all']]}
|
||||||
>
|
>
|
||||||
<FormattedMessage id='settings.auto_collapse_notifications' defaultMessage='Notifications' />
|
<FormattedMessage id='settings.auto_collapse_notifications' defaultMessage='Notifications' />
|
||||||
</SettingsItem>
|
</LocalSettingsPageItem>
|
||||||
<SettingsItem
|
<LocalSettingsPageItem
|
||||||
settings={this.props.settings}
|
settings={settings}
|
||||||
item={['collapsed', 'auto', 'lengthy']}
|
item={['collapsed', 'auto', 'lengthy']}
|
||||||
id='mastodon-settings--collapsed-auto-lengthy'
|
id='mastodon-settings--collapsed-auto-lengthy'
|
||||||
onChange={this.props.toggleSetting}
|
onChange={onChange}
|
||||||
dependsOn={[['collapsed', 'enabled']]}
|
dependsOn={[['collapsed', 'enabled']]}
|
||||||
dependsOnNot={[['collapsed', 'auto', 'all']]}
|
dependsOnNot={[['collapsed', 'auto', 'all']]}
|
||||||
>
|
>
|
||||||
<FormattedMessage id='settings.auto_collapse_lengthy' defaultMessage='Lengthy toots' />
|
<FormattedMessage id='settings.auto_collapse_lengthy' defaultMessage='Lengthy toots' />
|
||||||
</SettingsItem>
|
</LocalSettingsPageItem>
|
||||||
<SettingsItem
|
<LocalSettingsPageItem
|
||||||
settings={this.props.settings}
|
settings={settings}
|
||||||
item={['collapsed', 'auto', 'replies']}
|
item={['collapsed', 'auto', 'replies']}
|
||||||
id='mastodon-settings--collapsed-auto-replies'
|
id='mastodon-settings--collapsed-auto-replies'
|
||||||
onChange={this.props.toggleSetting}
|
onChange={onChange}
|
||||||
dependsOn={[['collapsed', 'enabled']]}
|
dependsOn={[['collapsed', 'enabled']]}
|
||||||
dependsOnNot={[['collapsed', 'auto', 'all']]}
|
dependsOnNot={[['collapsed', 'auto', 'all']]}
|
||||||
>
|
>
|
||||||
<FormattedMessage id='settings.auto_collapse_replies' defaultMessage='Replies' />
|
<FormattedMessage id='settings.auto_collapse_replies' defaultMessage='Replies' />
|
||||||
</SettingsItem>
|
</LocalSettingsPageItem>
|
||||||
<SettingsItem
|
<LocalSettingsPageItem
|
||||||
settings={this.props.settings}
|
settings={settings}
|
||||||
item={['collapsed', 'auto', 'media']}
|
item={['collapsed', 'auto', 'media']}
|
||||||
id='mastodon-settings--collapsed-auto-media'
|
id='mastodon-settings--collapsed-auto-media'
|
||||||
onChange={this.props.toggleSetting}
|
onChange={onChange}
|
||||||
dependsOn={[['collapsed', 'enabled']]}
|
dependsOn={[['collapsed', 'enabled']]}
|
||||||
dependsOnNot={[['collapsed', 'auto', 'all']]}
|
dependsOnNot={[['collapsed', 'auto', 'all']]}
|
||||||
>
|
>
|
||||||
<FormattedMessage id='settings.auto_collapse_media' defaultMessage='Toots with media' />
|
<FormattedMessage id='settings.auto_collapse_media' defaultMessage='Toots with media' />
|
||||||
</SettingsItem>
|
</LocalSettingsPageItem>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h2><FormattedMessage id='settings.image_backgrounds' defaultMessage='Image backgrounds' /></h2>
|
<h2><FormattedMessage id='settings.image_backgrounds' defaultMessage='Image backgrounds' /></h2>
|
||||||
<SettingsItem
|
<LocalSettingsPageItem
|
||||||
settings={this.props.settings}
|
settings={settings}
|
||||||
item={['collapsed', 'backgrounds', 'user_backgrounds']}
|
item={['collapsed', 'backgrounds', 'user_backgrounds']}
|
||||||
id='mastodon-settings--collapsed-user-backgrouns'
|
id='mastodon-settings--collapsed-user-backgrouns'
|
||||||
onChange={this.props.toggleSetting}
|
onChange={onChange}
|
||||||
dependsOn={[['collapsed', 'enabled']]}
|
dependsOn={[['collapsed', 'enabled']]}
|
||||||
>
|
>
|
||||||
<FormattedMessage id='settings.image_backgrounds_users' defaultMessage='Give collapsed toots an image background' />
|
<FormattedMessage id='settings.image_backgrounds_users' defaultMessage='Give collapsed toots an image background' />
|
||||||
</SettingsItem>
|
</LocalSettingsPageItem>
|
||||||
<SettingsItem
|
<LocalSettingsPageItem
|
||||||
settings={this.props.settings}
|
settings={settings}
|
||||||
item={['collapsed', 'backgrounds', 'preview_images']}
|
item={['collapsed', 'backgrounds', 'preview_images']}
|
||||||
id='mastodon-settings--collapsed-preview-images'
|
id='mastodon-settings--collapsed-preview-images'
|
||||||
onChange={this.props.toggleSetting}
|
onChange={onChange}
|
||||||
dependsOn={[['collapsed', 'enabled']]}
|
dependsOn={[['collapsed', 'enabled']]}
|
||||||
>
|
>
|
||||||
<FormattedMessage id='settings.image_backgrounds_media' defaultMessage='Preview collapsed toot media' />
|
<FormattedMessage id='settings.image_backgrounds_media' defaultMessage='Preview collapsed toot media' />
|
||||||
</SettingsItem>
|
</LocalSettingsPageItem>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
),
|
||||||
}
|
({ onChange, settings }) => (
|
||||||
|
<div className='glitch local-settings__page media'>
|
||||||
Media = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1><FormattedMessage id='settings.media' defaultMessage='Media' /></h1>
|
<h1><FormattedMessage id='settings.media' defaultMessage='Media' /></h1>
|
||||||
<SettingsItem
|
<LocalSettingsPageItem
|
||||||
settings={this.props.settings}
|
settings={settings}
|
||||||
item={['media', 'letterbox']}
|
item={['media', 'letterbox']}
|
||||||
id='mastodon-settings--media-letterbox'
|
id='mastodon-settings--media-letterbox'
|
||||||
onChange={this.props.toggleSetting}
|
onChange={onChange}
|
||||||
>
|
>
|
||||||
<FormattedMessage id='settings.media_letterbox' defaultMessage='Letterbox media' />
|
<FormattedMessage id='settings.media_letterbox' defaultMessage='Letterbox media' />
|
||||||
</SettingsItem>
|
</LocalSettingsPageItem>
|
||||||
<SettingsItem
|
<LocalSettingsPageItem
|
||||||
settings={this.props.settings}
|
settings={settings}
|
||||||
item={['media', 'fullwidth']}
|
item={['media', 'fullwidth']}
|
||||||
id='mastodon-settings--media-fullwidth'
|
id='mastodon-settings--media-fullwidth'
|
||||||
onChange={this.props.toggleSetting}
|
onChange={onChange}
|
||||||
>
|
>
|
||||||
<FormattedMessage id='settings.media_fullwidth' defaultMessage='Full-width media previews' />
|
<FormattedMessage id='settings.media_fullwidth' defaultMessage='Full-width media previews' />
|
||||||
</SettingsItem>
|
</LocalSettingsPageItem>
|
||||||
</div>
|
</div>
|
||||||
);
|
),
|
||||||
}
|
];
|
||||||
|
|
||||||
navigateTo = (e) =>
|
|
||||||
this.setState({ currentIndex: +e.currentTarget.getAttribute('data-mastodon-navigation_index') });
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
const { pages } = this;
|
||||||
|
const { index, intl, onChange, settings } = this.props;
|
||||||
|
const CurrentPage = pages[index] || pages[0];
|
||||||
|
|
||||||
const { General, CollapsedStatuses, Media, navigateTo } = this;
|
return <CurrentPage intl={intl} onChange={onChange} settings={settings} />;
|
||||||
const { onClose } = this.props;
|
|
||||||
const { currentIndex } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='modal-root__modal settings-modal'>
|
|
||||||
|
|
||||||
<nav className='settings-modal__navigation'>
|
|
||||||
<a onClick={navigateTo} role='button' data-mastodon-navigation_index='0' tabIndex='0' className={`settings-modal__navigation-item${currentIndex === 0 ? ' active' : ''}`}>
|
|
||||||
<FormattedMessage id='settings.general' defaultMessage='General' />
|
|
||||||
</a>
|
|
||||||
<a onClick={navigateTo} role='button' data-mastodon-navigation_index='1' tabIndex='0' className={`settings-modal__navigation-item${currentIndex === 1 ? ' active' : ''}`}>
|
|
||||||
<FormattedMessage id='settings.collapsed_statuses' defaultMessage='Collapsed toots' />
|
|
||||||
</a>
|
|
||||||
<a onClick={navigateTo} role='button' data-mastodon-navigation_index='2' tabIndex='0' className={`settings-modal__navigation-item${currentIndex === 2 ? ' active' : ''}`}>
|
|
||||||
<FormattedMessage id='settings.media' defaultMessage='Media' />
|
|
||||||
</a>
|
|
||||||
<a href='/settings/preferences' className='settings-modal__navigation-item'>
|
|
||||||
<i className='fa fa-fw fa-cog' /> <FormattedMessage id='settings.preferences' defaultMessage='User preferences' />
|
|
||||||
</a>
|
|
||||||
<a onClick={onClose} role='button' tabIndex='0' className='settings-modal__navigation-close'>
|
|
||||||
<FormattedMessage id='settings.close' defaultMessage='Close' />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div className='settings-modal__content'>
|
|
||||||
{
|
|
||||||
[
|
|
||||||
<General />,
|
|
||||||
<CollapsedStatuses />,
|
|
||||||
<Media />,
|
|
||||||
][currentIndex] || <General />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,30 +1,38 @@
|
|||||||
// Package imports //
|
// Package imports
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
|
||||||
export default class SettingsItem extends React.PureComponent {
|
// Stylesheet imports
|
||||||
|
import './style';
|
||||||
|
|
||||||
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
|
export default class LocalSettingsPageItem extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
settings: ImmutablePropTypes.map.isRequired,
|
children: PropTypes.element.isRequired,
|
||||||
item: PropTypes.array.isRequired,
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
options: PropTypes.arrayOf(PropTypes.shape({
|
|
||||||
value: PropTypes.string.isRequired,
|
|
||||||
message: PropTypes.object.isRequired,
|
|
||||||
})),
|
|
||||||
dependsOn: PropTypes.array,
|
dependsOn: PropTypes.array,
|
||||||
dependsOnNot: PropTypes.array,
|
dependsOnNot: PropTypes.array,
|
||||||
children: PropTypes.element.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
|
item: PropTypes.array.isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
options: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
value: PropTypes.string.isRequired,
|
||||||
|
message: PropTypes.string.isRequired,
|
||||||
|
})),
|
||||||
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleChange = (e) => {
|
handleChange = e => {
|
||||||
const { item, onChange } = this.props;
|
const { target } = e;
|
||||||
onChange(item, e);
|
const { item, onChange, options } = this.props;
|
||||||
|
if (options && options.length > 0) onChange(item, target.value);
|
||||||
|
else onChange(item, target.checked);
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
const { handleChange } = this;
|
||||||
const { settings, item, id, options, children, dependsOn, dependsOnNot } = this.props;
|
const { settings, item, id, options, children, dependsOn, dependsOnNot } = this.props;
|
||||||
let enabled = true;
|
let enabled = true;
|
||||||
|
|
||||||
@@ -42,38 +50,41 @@ export default class SettingsItem extends React.PureComponent {
|
|||||||
if (options && options.length > 0) {
|
if (options && options.length > 0) {
|
||||||
const currentValue = settings.getIn(item);
|
const currentValue = settings.getIn(item);
|
||||||
const optionElems = options && options.length > 0 && options.map((opt) => (
|
const optionElems = options && options.length > 0 && options.map((opt) => (
|
||||||
<option key={opt.value} selected={currentValue === opt.value} value={opt.value} >
|
<option
|
||||||
|
key={opt.value}
|
||||||
|
value={opt.value}
|
||||||
|
>
|
||||||
{opt.message}
|
{opt.message}
|
||||||
</option>
|
</option>
|
||||||
));
|
));
|
||||||
return (
|
return (
|
||||||
<label htmlFor={id}>
|
<label className='glitch local-settings__page__item' htmlFor={id}>
|
||||||
<p>{children}</p>
|
<p>{children}</p>
|
||||||
<p>
|
<p>
|
||||||
<select
|
<select
|
||||||
id={id}
|
id={id}
|
||||||
disabled={!enabled}
|
disabled={!enabled}
|
||||||
onBlur={this.handleChange}
|
onBlur={handleChange}
|
||||||
|
onChange={handleChange}
|
||||||
|
value={currentValue}
|
||||||
>
|
>
|
||||||
{optionElems}
|
{optionElems}
|
||||||
</select>
|
</select>
|
||||||
</p>
|
</p>
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
} else {
|
} else return (
|
||||||
return (
|
<label className='glitch local-settings__page__item' htmlFor={id}>
|
||||||
<label htmlFor={id}>
|
|
||||||
<input
|
<input
|
||||||
id={id}
|
id={id}
|
||||||
type='checkbox'
|
type='checkbox'
|
||||||
checked={settings.getIn(item)}
|
checked={settings.getIn(item)}
|
||||||
onChange={this.handleChange}
|
onChange={handleChange}
|
||||||
disabled={!enabled}
|
disabled={!enabled}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
@import 'variables';
|
||||||
|
|
||||||
|
.glitch.local-settings__page__item {
|
||||||
|
select {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
@import 'variables';
|
||||||
|
|
||||||
|
.glitch.local-settings__page {
|
||||||
|
display: block;
|
||||||
|
flex: auto;
|
||||||
|
padding: 15px 20px 15px 20px;
|
||||||
|
width: 360px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
34
app/javascript/glitch/components/local_settings/style.scss
Normal file
34
app/javascript/glitch/components/local_settings/style.scss
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
@import 'variables';
|
||||||
|
|
||||||
|
.glitch.local-settings {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
background: $ui-secondary-color;
|
||||||
|
color: $ui-base-color;
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 80vh;
|
||||||
|
width: 80vw;
|
||||||
|
max-width: 740px;
|
||||||
|
max-height: 450px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
56
app/javascript/glitch/components/notification/container.js
Normal file
56
app/javascript/glitch/components/notification/container.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
`<NotificationContainer>`
|
||||||
|
=========================
|
||||||
|
|
||||||
|
This container connects `<Notification>`s to the Redux store.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Imports:
|
||||||
|
--------
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package imports //
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
// Mastodon imports //
|
||||||
|
import { makeGetNotification } from '../../../mastodon/selectors';
|
||||||
|
|
||||||
|
// Our imports //
|
||||||
|
import Notification from '.';
|
||||||
|
|
||||||
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
State mapping:
|
||||||
|
--------------
|
||||||
|
|
||||||
|
The `mapStateToProps()` function maps various state properties to the
|
||||||
|
props of our component. We wrap this in `makeMapStateToProps()` so that
|
||||||
|
we only have to call `makeGetNotification()` once instead of every
|
||||||
|
time.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
const makeMapStateToProps = () => {
|
||||||
|
const getNotification = makeGetNotification();
|
||||||
|
|
||||||
|
const mapStateToProps = (state, props) => ({
|
||||||
|
notification: getNotification(state, props.notification, props.accountId),
|
||||||
|
settings: state.get('local_settings'),
|
||||||
|
notifCleaning: state.getIn(['notifications', 'cleaningMode']),
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapStateToProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
|
export default connect(makeMapStateToProps)(Notification);
|
||||||
75
app/javascript/glitch/components/notification/follow.js
Normal file
75
app/javascript/glitch/components/notification/follow.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// `<NotificationFollow>`
|
||||||
|
// ======================
|
||||||
|
|
||||||
|
// * * * * * * * //
|
||||||
|
|
||||||
|
// Imports
|
||||||
|
// -------
|
||||||
|
|
||||||
|
// Package imports.
|
||||||
|
import React from 'react';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
|
// Mastodon imports.
|
||||||
|
import emojify from '../../../mastodon/emoji';
|
||||||
|
import Permalink from '../../../mastodon/components/permalink';
|
||||||
|
import AccountContainer from '../../../mastodon/containers/account_container';
|
||||||
|
|
||||||
|
// Our imports.
|
||||||
|
import NotificationOverlayContainer from '../notification/overlay/container';
|
||||||
|
|
||||||
|
// * * * * * * * //
|
||||||
|
|
||||||
|
// Implementation
|
||||||
|
// --------------
|
||||||
|
|
||||||
|
export default class NotificationFollow extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
id : PropTypes.number.isRequired,
|
||||||
|
account : ImmutablePropTypes.map.isRequired,
|
||||||
|
notification : ImmutablePropTypes.map.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { account, notification } = this.props;
|
||||||
|
|
||||||
|
// Links to the display name.
|
||||||
|
const displayName = account.get('display_name') || account.get('username');
|
||||||
|
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
|
||||||
|
const link = (
|
||||||
|
<Permalink
|
||||||
|
className='notification__display-name'
|
||||||
|
href={account.get('url')}
|
||||||
|
title={account.get('acct')}
|
||||||
|
to={`/accounts/${account.get('id')}`}
|
||||||
|
dangerouslySetInnerHTML={displayNameHTML}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Renders.
|
||||||
|
return (
|
||||||
|
<div className='notification notification-follow'>
|
||||||
|
<div className='notification__message'>
|
||||||
|
<div className='notification__favourite-icon-wrapper'>
|
||||||
|
<i className='fa fa-fw fa-user-plus' />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.follow'
|
||||||
|
defaultMessage='{name} followed you'
|
||||||
|
values={{ name: link }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AccountContainer id={account.get('id')} withNote={false} />
|
||||||
|
<NotificationOverlayContainer notification={notification} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,17 +1,13 @@
|
|||||||
// Package imports //
|
// Package imports //
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
import escapeTextContentForBrowser from 'escape-html';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
// Mastodon imports //
|
// Mastodon imports //
|
||||||
import AccountContainer from '../../../mastodon/containers/account_container';
|
|
||||||
import Permalink from '../../../mastodon/components/permalink';
|
|
||||||
import emojify from '../../../mastodon/emoji';
|
|
||||||
|
|
||||||
// Our imports //
|
// Our imports //
|
||||||
import StatusContainer from '../../containers/status';
|
import StatusContainer from '../status/container';
|
||||||
|
import NotificationFollow from './follow';
|
||||||
|
|
||||||
export default class Notification extends ImmutablePureComponent {
|
export default class Notification extends ImmutablePureComponent {
|
||||||
|
|
||||||
@@ -21,22 +17,12 @@ export default class Notification extends ImmutablePureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderFollow (notification) {
|
renderFollow (notification) {
|
||||||
const account = notification.get('account');
|
|
||||||
const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');
|
|
||||||
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
|
|
||||||
const link = <Permalink className='notification__display-name' href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />;
|
|
||||||
return (
|
return (
|
||||||
<div className='notification notification-follow'>
|
<NotificationFollow
|
||||||
<div className='notification__message'>
|
id={notification.get('id')}
|
||||||
<div className='notification__favourite-icon-wrapper'>
|
account={notification.get('account')}
|
||||||
<i className='fa fa-fw fa-user-plus' />
|
notification={notification}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
<FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AccountContainer id={account.get('id')} withNote={false} />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +30,7 @@ export default class Notification extends ImmutablePureComponent {
|
|||||||
return (
|
return (
|
||||||
<StatusContainer
|
<StatusContainer
|
||||||
id={notification.get('status')}
|
id={notification.get('status')}
|
||||||
|
notification={notification}
|
||||||
withDismiss
|
withDismiss
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -56,6 +43,7 @@ export default class Notification extends ImmutablePureComponent {
|
|||||||
account={notification.get('account')}
|
account={notification.get('account')}
|
||||||
prepend='favourite'
|
prepend='favourite'
|
||||||
muted
|
muted
|
||||||
|
notification={notification}
|
||||||
withDismiss
|
withDismiss
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -68,6 +56,7 @@ export default class Notification extends ImmutablePureComponent {
|
|||||||
account={notification.get('account')}
|
account={notification.get('account')}
|
||||||
prepend='reblog'
|
prepend='reblog'
|
||||||
muted
|
muted
|
||||||
|
notification={notification}
|
||||||
withDismiss
|
withDismiss
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user