mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-15 16:59:41 +00:00
Compare commits
1076 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1955a3f444 | ||
|
|
8ebed7fc68 | ||
|
|
8f2ed79a0b | ||
|
|
2b6b89491d | ||
|
|
f902a335f9 | ||
|
|
e4af4898de | ||
|
|
6d70a80263 | ||
|
|
0cbcc5e297 | ||
|
|
f87b51fda8 | ||
|
|
7e2e0d6dcc | ||
|
|
181115422c | ||
|
|
86d1dcc97a | ||
|
|
86eaaf0761 | ||
|
|
19f63ff801 | ||
|
|
babbb2135e | ||
|
|
99226aba93 | ||
|
|
42c9d5111a | ||
|
|
e0b5a94a4b | ||
|
|
26ec042f38 | ||
|
|
73b0af5c93 | ||
|
|
7efde22c3a | ||
|
|
90760eae4c | ||
|
|
fc34e0e191 | ||
|
|
de72db99fa | ||
|
|
77d1447ac4 | ||
|
|
2c329f2b69 | ||
|
|
c2762fa498 | ||
|
|
0611209141 | ||
|
|
da302a43cd | ||
|
|
8f8319852c | ||
|
|
75f416a492 | ||
|
|
24baaa17e8 | ||
|
|
c99fc08a0d | ||
|
|
a8f45c0838 | ||
|
|
6df63465b9 | ||
|
|
13b11ddc8c | ||
|
|
f2997c9715 | ||
|
|
e17f9d5e1a | ||
|
|
865cb39e9b | ||
|
|
5d710b1139 | ||
|
|
8a58942c80 | ||
|
|
f97272549c | ||
|
|
363de2dffd | ||
|
|
6a1ac9b31f | ||
|
|
3834e1e69b | ||
|
|
95bcbaa434 | ||
|
|
911338bdcc | ||
|
|
0f8b7d0660 | ||
|
|
e332552816 | ||
|
|
0d83569899 | ||
|
|
515434ed87 | ||
|
|
3d3e32befb | ||
|
|
fa08b5079d | ||
|
|
00392d3c63 | ||
|
|
28606d730a | ||
|
|
1c8477eab2 | ||
|
|
6670e6d33f | ||
|
|
9d2f55ecc3 | ||
|
|
b7ec2fd492 | ||
|
|
3b8908c114 | ||
|
|
7b10794afb | ||
|
|
355965c17b | ||
|
|
0f889523e4 | ||
|
|
f4045ba3d9 | ||
|
|
df4ff9a8e1 | ||
|
|
67ad84b7eb | ||
|
|
f0f6a3279a | ||
|
|
9e620ca16c | ||
|
|
bddd36f260 | ||
|
|
b857551617 | ||
|
|
e28a5aab08 | ||
|
|
f9d7ec8971 | ||
|
|
40fd1de488 | ||
|
|
31f0bcf804 | ||
|
|
09540192c9 | ||
|
|
08059ddda9 | ||
|
|
a3aa9381c4 | ||
|
|
120a37a197 | ||
|
|
ec9999cdfe | ||
|
|
e806d3c3f0 | ||
|
|
4ba6acd518 | ||
|
|
061922b38c | ||
|
|
5d8d827436 | ||
|
|
cde1f37d93 | ||
|
|
89707ad0ac | ||
|
|
4bebeb27d3 | ||
|
|
ef879a8839 | ||
|
|
9240ca6cef | ||
|
|
619817d29e | ||
|
|
0655f16cc1 | ||
|
|
ade004b5ee | ||
|
|
7609593e48 | ||
|
|
a9529d3b4b | ||
|
|
0d2910478a | ||
|
|
5ab0ffc6c8 | ||
|
|
8482f67caf | ||
|
|
fe8dd58bc1 | ||
|
|
92cd207c50 | ||
|
|
f5cd138323 | ||
|
|
66ea015a01 | ||
|
|
1616cf98a1 | ||
|
|
5aae6384ff | ||
|
|
fa89deb4eb | ||
|
|
26a892dd90 | ||
|
|
8321884eef | ||
|
|
290e8ef854 | ||
|
|
067eb220c6 | ||
|
|
1467515d3d | ||
|
|
8b74aa4217 | ||
|
|
a6807201d2 | ||
|
|
4445ebdad2 | ||
|
|
c019b0acfd | ||
|
|
7447e7a2ea | ||
|
|
c8be05a4a7 | ||
|
|
41c697fd81 | ||
|
|
c5afe573da | ||
|
|
485310a43c | ||
|
|
9aae9ae40c | ||
|
|
1fb125b630 | ||
|
|
057567d548 | ||
|
|
169c68a739 | ||
|
|
9f182346d7 | ||
|
|
a58c935c3d | ||
|
|
c0c56db0fa | ||
|
|
d9dc0fe84e | ||
|
|
55b56e3f95 | ||
|
|
c4d39b1b3d | ||
|
|
ac54da9394 | ||
|
|
043862f411 | ||
|
|
9e5c1c487e | ||
|
|
5619099564 | ||
|
|
ce80d0b0a9 | ||
|
|
6327f69cab | ||
|
|
5f8155482a | ||
|
|
efcf9448da | ||
|
|
e70b84b1dc | ||
|
|
d7a4e8739a | ||
|
|
9362700137 | ||
|
|
1206627c59 | ||
|
|
edefcfcf42 | ||
|
|
b330d1f000 | ||
|
|
1a5a54eb4b | ||
|
|
447b8bc44e | ||
|
|
093879c177 | ||
|
|
d2c20936f3 | ||
|
|
0220f3a171 | ||
|
|
905a4faa1c | ||
|
|
5355b7d930 | ||
|
|
aec2458d81 | ||
|
|
4fe5e04ea4 | ||
|
|
3f42ad7d1a | ||
|
|
f7c466c8d8 | ||
|
|
245b9cb4ba | ||
|
|
9275f92972 | ||
|
|
4a6d3bac86 | ||
|
|
00cc3066a2 | ||
|
|
a57d30c680 | ||
|
|
467d32fce3 | ||
|
|
8aadb7b0b2 | ||
|
|
79546799af | ||
|
|
90d0018fd5 | ||
|
|
1a12fd14d4 | ||
|
|
282bb55c3c | ||
|
|
0e4479bb3a | ||
|
|
af7e880df5 | ||
|
|
aa7bf1515c | ||
|
|
4f781b17cc | ||
|
|
137100dcf3 | ||
|
|
3a9eb81a80 | ||
|
|
0e39cc6a35 | ||
|
|
faefd8ec8f | ||
|
|
a18fd491b9 | ||
|
|
96715d9af5 | ||
|
|
f24daa399b | ||
|
|
af96e71883 | ||
|
|
5dc73339ae | ||
|
|
ccaf3dbc5a | ||
|
|
1ea662963f | ||
|
|
bd834add56 | ||
|
|
9966bd27c2 | ||
|
|
b0ab632531 | ||
|
|
e1264bbd92 | ||
|
|
38e24a699b | ||
|
|
bf3e56b8ad | ||
|
|
9b698bf448 | ||
|
|
0254ee9795 | ||
|
|
e32edd247f | ||
|
|
dab9b5bd3a | ||
|
|
e17b5b228d | ||
|
|
c4baa9fb6b | ||
|
|
c2a31b8032 | ||
|
|
9e63bf446e | ||
|
|
c44a700252 | ||
|
|
aa90798386 | ||
|
|
0930ce5560 | ||
|
|
7f0a865b05 | ||
|
|
08fce08217 | ||
|
|
3064ef96a1 | ||
|
|
ee69ece7b5 | ||
|
|
d90d23699c | ||
|
|
1f5ff46fd9 | ||
|
|
13528f50c3 | ||
|
|
dd1ae3b109 | ||
|
|
b352a8e5d4 | ||
|
|
fd102059aa | ||
|
|
323671a653 | ||
|
|
b155e6ccf5 | ||
|
|
f16b9a4928 | ||
|
|
24eb45425e | ||
|
|
3442bc0ea3 | ||
|
|
40bdf43297 | ||
|
|
8ead070b94 | ||
|
|
b22b2cbfac | ||
|
|
2f2b84bfbb | ||
|
|
5cdd2c2414 | ||
|
|
3ddd936b03 | ||
|
|
1921c5416b | ||
|
|
fc47c1d00e | ||
|
|
327a6e166f | ||
|
|
6f5268b02d | ||
|
|
4964433190 | ||
|
|
9e3c4fd2d7 | ||
|
|
89e8e110c8 | ||
|
|
9f7ea77d0c | ||
|
|
5f74397ef0 | ||
|
|
960181fd99 | ||
|
|
2a7602cad4 | ||
|
|
47aacb773b | ||
|
|
82d9336114 | ||
|
|
e60286a344 | ||
|
|
53850bce93 | ||
|
|
1236529e39 | ||
|
|
06444bf050 | ||
|
|
b723ee73fc | ||
|
|
c35bda0551 | ||
|
|
f53fb6aa66 | ||
|
|
a85d4473aa | ||
|
|
c9b9225951 | ||
|
|
11898a6461 | ||
|
|
01e5447e35 | ||
|
|
4ada50985a | ||
|
|
a283786463 | ||
|
|
12f72e1740 | ||
|
|
b57eed4584 | ||
|
|
3672a799d4 | ||
|
|
3fd5385e7b | ||
|
|
d439855a6d | ||
|
|
2810013b93 | ||
|
|
0687ab8ae3 | ||
|
|
64dbde0dbf | ||
|
|
ae57b3a8c5 | ||
|
|
0dbbc16c69 | ||
|
|
f690320fb9 | ||
|
|
553170b77a | ||
|
|
8a6096a3de | ||
|
|
d2f6d9b9fb | ||
|
|
dbe9f33fdc | ||
|
|
1be6aa0c7f | ||
|
|
087ca3009b | ||
|
|
db7c7d1af1 | ||
|
|
42fb4faa0f | ||
|
|
9bb398ee91 | ||
|
|
9043b32183 | ||
|
|
e30bbb1cb0 | ||
|
|
8bdf02812c | ||
|
|
93db265be7 | ||
|
|
c172919745 | ||
|
|
15d442cf9d | ||
|
|
43f955e31f | ||
|
|
4ea4ef9d0f | ||
|
|
d19ed18388 | ||
|
|
f0bd439486 | ||
|
|
b16fbd52b2 | ||
|
|
3b34c28bee | ||
|
|
8bfdbf0aa6 | ||
|
|
d4fe6cd2bf | ||
|
|
ea6c930c04 | ||
|
|
12e29c9660 | ||
|
|
082bef3027 | ||
|
|
e6b48a7048 | ||
|
|
ba2aea3a80 | ||
|
|
e5282e4ec0 | ||
|
|
53eb31f124 | ||
|
|
388ec0d5b6 | ||
|
|
71706f21c2 | ||
|
|
b1881a3d48 | ||
|
|
d5a675099a | ||
|
|
c3e7bac1cc | ||
|
|
6e3925521d | ||
|
|
b89f007862 | ||
|
|
9acdb166e8 | ||
|
|
470eb0042e | ||
|
|
fc146a19cc | ||
|
|
941a593ea8 | ||
|
|
982fef811e | ||
|
|
41f8fde83e | ||
|
|
157f0a2aa7 | ||
|
|
49043f644d | ||
|
|
c803f5b440 | ||
|
|
f860eb7d71 | ||
|
|
8f9a11b642 | ||
|
|
c6b0707cf6 | ||
|
|
b79c80b620 | ||
|
|
211920b622 | ||
|
|
01d8003867 | ||
|
|
ed3dfd0bee | ||
|
|
35eff3f2d0 | ||
|
|
ca44c13455 | ||
|
|
a345eb44fc | ||
|
|
7b814d5bcb | ||
|
|
9f9f4b248e | ||
|
|
3660f01f60 | ||
|
|
a2ec54a20b | ||
|
|
e33dcb79c6 | ||
|
|
7d5ea5c170 | ||
|
|
33849acfa7 | ||
|
|
c141f0a886 | ||
|
|
55d03da303 | ||
|
|
2c3a730eae | ||
|
|
b04cbb9f5d | ||
|
|
75aade3de2 | ||
|
|
ac0b84534e | ||
|
|
3c48c9ac2e | ||
|
|
ecf0320a78 | ||
|
|
40703b96fa | ||
|
|
1e4453405b | ||
|
|
0ad694f96b | ||
|
|
541c538f9b | ||
|
|
50910d1543 | ||
|
|
4ff8653d6c | ||
|
|
17690f51a2 | ||
|
|
cf80cb5e8b | ||
|
|
315ff648c8 | ||
|
|
41923d1c6b | ||
|
|
63686fd36f | ||
|
|
ae9d2f4a32 | ||
|
|
741bbba6ff | ||
|
|
a25a384af3 | ||
|
|
971c4de18c | ||
|
|
394c8ef680 | ||
|
|
3c5b0c55cb | ||
|
|
0dcf3c6abe | ||
|
|
b67b60fec3 | ||
|
|
2c0ef75f58 | ||
|
|
cc16fa7513 | ||
|
|
d6827e38a6 | ||
|
|
29ed448445 | ||
|
|
e7d7a99fbc | ||
|
|
8d27de32b3 | ||
|
|
abab82ec1e | ||
|
|
f0797bf8ce | ||
|
|
38c5130930 | ||
|
|
ee8af9083a | ||
|
|
c9f15f7991 | ||
|
|
91afe1f8fd | ||
|
|
9edee2e64f | ||
|
|
d7e1a282fe | ||
|
|
c890b86ef6 | ||
|
|
5b571fc434 | ||
|
|
3e4eb9c95f | ||
|
|
ceba26d527 | ||
|
|
7826b5e93d | ||
|
|
fc7b830719 | ||
|
|
c945f29e96 | ||
|
|
0d4d42dce6 | ||
|
|
cf03634e74 | ||
|
|
698d74a15f | ||
|
|
d7d165db5b | ||
|
|
d7f4300ee3 | ||
|
|
7632178300 | ||
|
|
e4e948a21b | ||
|
|
fef478781d | ||
|
|
c1a553d2c2 | ||
|
|
3e8c1a1c36 | ||
|
|
9fd8bbe15c | ||
|
|
1b42f717f2 | ||
|
|
7004c69204 | ||
|
|
9981972844 | ||
|
|
5f61ef2417 | ||
|
|
cf13c97cb2 | ||
|
|
90a408f592 | ||
|
|
451b7431c9 | ||
|
|
530725ba3c | ||
|
|
6efaee30b1 | ||
|
|
698fe3686a | ||
|
|
05be34a94b | ||
|
|
cb58694a81 | ||
|
|
e51b6bba94 | ||
|
|
7f393a0b68 | ||
|
|
9f43e3b428 | ||
|
|
4a40b40324 | ||
|
|
4c05f0e630 | ||
|
|
d438eab673 | ||
|
|
7b7bff04df | ||
|
|
e659608797 | ||
|
|
ed332693fe | ||
|
|
881e4277fd | ||
|
|
5dfc9854f1 | ||
|
|
b300bb3b4e | ||
|
|
6d519e6fd1 | ||
|
|
d2c9cc31de | ||
|
|
065defefac | ||
|
|
6bfe068904 | ||
|
|
bc237d17a7 | ||
|
|
485d75a805 | ||
|
|
4b621188ad | ||
|
|
4fb24a70d3 | ||
|
|
b2a7218ab7 | ||
|
|
a872f2f4c6 | ||
|
|
32a6f0884c | ||
|
|
a54af44975 | ||
|
|
f113af5350 | ||
|
|
f578cf8331 | ||
|
|
3cb13bdd84 | ||
|
|
0bf31f5436 | ||
|
|
a7ab2204d4 | ||
|
|
36a83cc4f9 | ||
|
|
30903d5f02 | ||
|
|
94536af96d | ||
|
|
0b32b5108e | ||
|
|
12f1cdeed1 | ||
|
|
e2f024147c | ||
|
|
1961825ff9 | ||
|
|
32748c0f71 | ||
|
|
37a36b0bec | ||
|
|
c7d9b81d41 | ||
|
|
4fdeac21f4 | ||
|
|
131f505fd0 | ||
|
|
27012aaeb6 | ||
|
|
4c751d25e5 | ||
|
|
ad5ddd5e95 | ||
|
|
13c0077003 | ||
|
|
d4c94fa004 | ||
|
|
38bec79811 | ||
|
|
0f4fa59812 | ||
|
|
41396de7a9 | ||
|
|
2ac8a590cd | ||
|
|
4e41cd9ab8 | ||
|
|
10459241a8 | ||
|
|
c9b23a93c7 | ||
|
|
0adee18d73 | ||
|
|
8a6d8de60a | ||
|
|
786e6f94b9 | ||
|
|
0c4e9fdda0 | ||
|
|
e3a3422a65 | ||
|
|
624a9a7136 | ||
|
|
1c351709bc | ||
|
|
8e7d0bda40 | ||
|
|
6d6a429af8 | ||
|
|
5d43a9cae2 | ||
|
|
8a4ff30ceb | ||
|
|
56f4a94e7b | ||
|
|
31597fd377 | ||
|
|
4b08b7c502 | ||
|
|
8cb7d157bd | ||
|
|
acf10a2c87 | ||
|
|
7596de1aec | ||
|
|
97ae53daa8 | ||
|
|
cd77c75d6c | ||
|
|
cfbd90cf44 | ||
|
|
ed2bfdee67 | ||
|
|
a3318814e1 | ||
|
|
acf6436a99 | ||
|
|
a0f1f9c664 | ||
|
|
f0d1107c53 | ||
|
|
02c1ad5347 | ||
|
|
fa494dbb1d | ||
|
|
a736b28646 | ||
|
|
c392c54271 | ||
|
|
0d5d3c7abe | ||
|
|
c441208e29 | ||
|
|
5e63828917 | ||
|
|
ea86f4db15 | ||
|
|
a3d204e982 | ||
|
|
07495cc13f | ||
|
|
6a88151eda | ||
|
|
30619a6716 | ||
|
|
a2adf84858 | ||
|
|
eadac4e7f4 | ||
|
|
0209b7d1b5 | ||
|
|
a2637c1720 | ||
|
|
c62696bc46 | ||
|
|
83530f0eef | ||
|
|
73b8e67f4b | ||
|
|
51d7caaf19 | ||
|
|
b5d87500d2 | ||
|
|
a9c0062e80 | ||
|
|
dbd529109e | ||
|
|
540d6efe88 | ||
|
|
d025c5e593 | ||
|
|
7aede8e720 | ||
|
|
dea6e47de0 | ||
|
|
5442083b3c | ||
|
|
c19e0f1212 | ||
|
|
bafbf63fcc | ||
|
|
eb98c99924 | ||
|
|
6b41fb2e6f | ||
|
|
bf7cefa516 | ||
|
|
65b3a2a5a6 | ||
|
|
d13d169922 | ||
|
|
5b95be1c42 | ||
|
|
d755ce96da | ||
|
|
29ffe1cad3 | ||
|
|
8fa8004a2b | ||
|
|
e5566ac6a6 | ||
|
|
c9ebd5d19f | ||
|
|
ac1989d2c0 | ||
|
|
1b8c244dff | ||
|
|
32d4b51939 | ||
|
|
3d8b80e1cc | ||
|
|
b38bd58921 | ||
|
|
8989569dd4 | ||
|
|
96812a6c79 | ||
|
|
a31f5765af | ||
|
|
41a78be25e | ||
|
|
9cf0b5b255 | ||
|
|
b8218ca482 | ||
|
|
4335dffe35 | ||
|
|
4de3182dc8 | ||
|
|
ee758551d1 | ||
|
|
b142a2ebf5 | ||
|
|
9a534d1df6 | ||
|
|
28fb01c71a | ||
|
|
d6bab0c71c | ||
|
|
5e7ec0fe57 | ||
|
|
152a1e578c | ||
|
|
a371a9e002 | ||
|
|
be807ff7bd | ||
|
|
68e0421577 | ||
|
|
cbcfd92a14 | ||
|
|
b45b4865a7 | ||
|
|
188172c401 | ||
|
|
7c4dde56a3 | ||
|
|
837030db98 | ||
|
|
deb001bba8 | ||
|
|
d3bf0307db | ||
|
|
bf523fcd16 | ||
|
|
220bc48e8e | ||
|
|
259e626165 | ||
|
|
5ed2de6be2 | ||
|
|
79765d61f5 | ||
|
|
6201fba2d3 | ||
|
|
0a984e90d3 | ||
|
|
cfe91ac984 | ||
|
|
8530f9413b | ||
|
|
85c768bf16 | ||
|
|
9572282a55 | ||
|
|
f8a5ff95ec | ||
|
|
af0decf597 | ||
|
|
ba5e23ecce | ||
|
|
a107de07c9 | ||
|
|
93e53a3311 | ||
|
|
bdf3ac95b8 | ||
|
|
fa6f7c8898 | ||
|
|
5963fce131 | ||
|
|
5dbcd92193 | ||
|
|
792389da38 | ||
|
|
8b8839978a | ||
|
|
78cf0fe1c7 | ||
|
|
18d0e817dd | ||
|
|
1904a1aa14 | ||
|
|
b5f8273312 | ||
|
|
3504da5cac | ||
|
|
8814f90eb5 | ||
|
|
6b566c6b88 | ||
|
|
6b02591fa3 | ||
|
|
aab818717e | ||
|
|
20b53e6add | ||
|
|
3ec221d3b7 | ||
|
|
b8a867adcc | ||
|
|
473e4f7813 | ||
|
|
b845ef395d | ||
|
|
6a1da87cd3 | ||
|
|
8040d1d8ce | ||
|
|
03adb5d727 | ||
|
|
29efeecb9e | ||
|
|
22dcadedb4 | ||
|
|
7bed4e51db | ||
|
|
9638894233 | ||
|
|
220051b8b2 | ||
|
|
0069c01285 | ||
|
|
dc5704b0b0 | ||
|
|
96ef933820 | ||
|
|
1e96ce378e | ||
|
|
b73cee9774 | ||
|
|
7d354cc8c5 | ||
|
|
fa7b74cf51 | ||
|
|
128dcb2825 | ||
|
|
ccb6a658fd | ||
|
|
667ffafef8 | ||
|
|
4c92f15664 | ||
|
|
94d00f2788 | ||
|
|
afdcdce551 | ||
|
|
731f397b08 | ||
|
|
aa464aa0d8 | ||
|
|
a23123e49c | ||
|
|
78c1e2e958 | ||
|
|
ab98591af8 | ||
|
|
bdaf31bcc9 | ||
|
|
c106b6d3e0 | ||
|
|
086a88f3bb | ||
|
|
e76dd52b08 | ||
|
|
f735efdbc1 | ||
|
|
1709dee0f1 | ||
|
|
1c634ad21a | ||
|
|
682c8a6fc8 | ||
|
|
66ec005bf8 | ||
|
|
c12214963e | ||
|
|
aa8dacbc8a | ||
|
|
ce524cbb49 | ||
|
|
117b22e905 | ||
|
|
d3dab68978 | ||
|
|
57466d542b | ||
|
|
417273326a | ||
|
|
909d81923e | ||
|
|
2edeb3fe1c | ||
|
|
dd441606aa | ||
|
|
feea517046 | ||
|
|
f7e35d90db | ||
|
|
e55fbdede3 | ||
|
|
bda37489ac | ||
|
|
50a88d6a6e | ||
|
|
5af0ecbcd9 | ||
|
|
79ef756f64 | ||
|
|
04225ed72e | ||
|
|
073f92fc76 | ||
|
|
46c0e8b0e7 | ||
|
|
7762467b47 | ||
|
|
81c76fe375 | ||
|
|
4512fde181 | ||
|
|
1e5a1b9abd | ||
|
|
ed22f65b3c | ||
|
|
9ae9ecdebe | ||
|
|
8736ef50ad | ||
|
|
dcda852b5f | ||
|
|
6091b9b1a9 | ||
|
|
6fd865c000 | ||
|
|
350958babf | ||
|
|
9a5d6e9715 | ||
|
|
41ba74b511 | ||
|
|
22000ef7a9 | ||
|
|
7015578655 | ||
|
|
731e650681 | ||
|
|
2fcf8d79ad | ||
|
|
e9a6da6bc7 | ||
|
|
d8855150a0 | ||
|
|
192f079776 | ||
|
|
58bdb9b42e | ||
|
|
665ec615e3 | ||
|
|
5f54981846 | ||
|
|
10a8666e04 | ||
|
|
405c495c23 | ||
|
|
fee3312193 | ||
|
|
48dfdad492 | ||
|
|
ed4a723a82 | ||
|
|
c359e60034 | ||
|
|
4bb623a595 | ||
|
|
7c075b5551 | ||
|
|
904f9266ef | ||
|
|
1e81cad3f3 | ||
|
|
8e693b8e41 | ||
|
|
60d07c06c3 | ||
|
|
be2e7e1802 | ||
|
|
38b504b7a7 | ||
|
|
82aaedec46 | ||
|
|
b1f3499c38 | ||
|
|
b21f7c28f6 | ||
|
|
ce9df2fa82 | ||
|
|
3abb0f7bc7 | ||
|
|
db4a41cf58 | ||
|
|
dc89fc17cc | ||
|
|
b8243c1b49 | ||
|
|
e81ba26be9 | ||
|
|
c22388fc79 | ||
|
|
eb023beb49 | ||
|
|
b510a56c0c | ||
|
|
4c53af64f0 | ||
|
|
f722bd2387 | ||
|
|
ce1ca28594 | ||
|
|
5b6f4fdeb4 | ||
|
|
8232f76c48 | ||
|
|
3f30ae1f97 | ||
|
|
1ed37fae9b | ||
|
|
cc451e1fcb | ||
|
|
0700521ef3 | ||
|
|
98a93aa07e | ||
|
|
68f829e11c | ||
|
|
71458dc6df | ||
|
|
ec8029a955 | ||
|
|
7dd5ba42a3 | ||
|
|
b7c1b12367 | ||
|
|
8a45a97e2e | ||
|
|
f6e9251054 | ||
|
|
9738aaf5fb | ||
|
|
ce433e22f6 | ||
|
|
5652f00d81 | ||
|
|
d06c810b16 | ||
|
|
5cb011b66b | ||
|
|
663e5378c0 | ||
|
|
65d667dc6c | ||
|
|
d3fde60297 | ||
|
|
85a8b62ca2 | ||
|
|
97803600ed | ||
|
|
a229840ffe | ||
|
|
bfa99981e5 | ||
|
|
6501ffdadc | ||
|
|
ae95f35fe6 | ||
|
|
22f88b845a | ||
|
|
eabb86b124 | ||
|
|
b0f4c9b91f | ||
|
|
f9b4f30de6 | ||
|
|
01b79beca4 | ||
|
|
5d854f37b4 | ||
|
|
92a0b3d73a | ||
|
|
2f7139c6b5 | ||
|
|
87854745e9 | ||
|
|
69fc95a2f5 | ||
|
|
c97fc5c1f4 | ||
|
|
1236a12cae | ||
|
|
b455cbc6d3 | ||
|
|
a9db64cf26 | ||
|
|
5d2f1d600d | ||
|
|
5d65aa3bf9 | ||
|
|
565568dd9c | ||
|
|
6e41ef55ed | ||
|
|
2d384850cb | ||
|
|
1a5ab538de | ||
|
|
3f650c06b6 | ||
|
|
71b9205679 | ||
|
|
6b9c50117f | ||
|
|
fbcba65f81 | ||
|
|
b7beb4368c | ||
|
|
e2526a62e7 | ||
|
|
61f90f15b0 | ||
|
|
d2358aefec | ||
|
|
30df1c8476 | ||
|
|
b8d2373e0b | ||
|
|
92d35c52d9 | ||
|
|
d7d60073f9 | ||
|
|
61894582b8 | ||
|
|
ce495138ac | ||
|
|
9ca548c17e | ||
|
|
82ae07e48b | ||
|
|
b69e97c40b | ||
|
|
c76d20c2a0 | ||
|
|
590558d52c | ||
|
|
4f7cce25ac | ||
|
|
633e5ec6f6 | ||
|
|
e92796b49c | ||
|
|
a729d0c2b1 | ||
|
|
71cf9d0608 | ||
|
|
5e45982ed9 | ||
|
|
b829460b4a | ||
|
|
65e6a00817 | ||
|
|
60a82c9022 | ||
|
|
b277e88aae | ||
|
|
03b2f37b46 | ||
|
|
0f274589dd | ||
|
|
2cd792fc3d | ||
|
|
09b4b65fde | ||
|
|
48cb2dccd2 | ||
|
|
a23e4380b2 | ||
|
|
aaa4d1b0fb | ||
|
|
3618cc04ff | ||
|
|
2d07cb5771 | ||
|
|
f25fc04ea1 | ||
|
|
ca21be3e16 | ||
|
|
5b12624847 | ||
|
|
54c796c8df | ||
|
|
34ff11c496 | ||
|
|
25ed563cdb | ||
|
|
9f219d8968 | ||
|
|
30da6440d0 | ||
|
|
f4b5fe9caf | ||
|
|
4b7dca4713 | ||
|
|
3a62721e54 | ||
|
|
d6b965cf08 | ||
|
|
e809caa0e1 | ||
|
|
aaea7e1e7c | ||
|
|
750662d9e2 | ||
|
|
34aff3e269 | ||
|
|
e70661b495 | ||
|
|
a2fa09518f | ||
|
|
3e84a86eb8 | ||
|
|
f5d434e433 | ||
|
|
3ed75efc31 | ||
|
|
cb1989cbd8 | ||
|
|
03d676faa8 | ||
|
|
9e26af264e | ||
|
|
f9f6098e22 | ||
|
|
2c881e6717 | ||
|
|
1389d5d7fd | ||
|
|
433cb198fa | ||
|
|
3ffa27e812 | ||
|
|
ae43978433 | ||
|
|
0f05643f5d | ||
|
|
7ccc9b8b02 | ||
|
|
55d275d30f | ||
|
|
a8c2e44fee | ||
|
|
808017ff18 | ||
|
|
60ebfa182f | ||
|
|
f693ab69f3 | ||
|
|
355168b939 | ||
|
|
13dfd8d109 | ||
|
|
d93d6f5124 | ||
|
|
7ddda65269 | ||
|
|
b4046c5957 | ||
|
|
d835647ec0 | ||
|
|
553e6dd07c | ||
|
|
1ee292b4cc | ||
|
|
e4c10b32b3 | ||
|
|
f20f6b25b9 | ||
|
|
680f9efe9c | ||
|
|
bde5c0eaf9 | ||
|
|
5e26295e06 | ||
|
|
188cddefe9 | ||
|
|
3ac4455160 | ||
|
|
bf61bc1b96 | ||
|
|
e8875c6046 | ||
|
|
03fb6c16ec | ||
|
|
87513b31e0 | ||
|
|
30964350b2 | ||
|
|
de22c202f5 | ||
|
|
35933167c0 | ||
|
|
333e44c3fc | ||
|
|
bb7006bda1 | ||
|
|
139fc994e2 | ||
|
|
1c6b02f936 | ||
|
|
448a07cc3f | ||
|
|
5181702a60 | ||
|
|
76188d61f2 | ||
|
|
c334541011 | ||
|
|
f864fee116 | ||
|
|
ebe53cb38c | ||
|
|
efbbd42216 | ||
|
|
dddb2eb84f | ||
|
|
6ec6fe259a | ||
|
|
874d91126c | ||
|
|
d5d5afff9c | ||
|
|
5f7c997654 | ||
|
|
2b7e3d56c8 | ||
|
|
c66dc0d114 | ||
|
|
1fd88e3bad | ||
|
|
b8f9de8636 | ||
|
|
a9303e7062 | ||
|
|
ad1af951fb | ||
|
|
bd1ceb1daa | ||
|
|
99ca63a543 | ||
|
|
d8c5a83827 | ||
|
|
fc9bbdfd34 | ||
|
|
349c6cfa2b | ||
|
|
9bf4c34919 | ||
|
|
d7c6c6dbe1 | ||
|
|
3e2d6ea408 | ||
|
|
c46843c65c | ||
|
|
08faeedff7 | ||
|
|
d6ed2eb512 | ||
|
|
33fac87e81 | ||
|
|
5aa3df017b | ||
|
|
c89ccbab09 | ||
|
|
22e06a4077 | ||
|
|
7637386228 | ||
|
|
88f32708c3 | ||
|
|
acfee0945c | ||
|
|
8aae42f3d8 | ||
|
|
1b09c3cb17 | ||
|
|
05cf086766 | ||
|
|
98571b0ce4 | ||
|
|
8803ca9efe | ||
|
|
6b1db5c2b2 | ||
|
|
56d998cbdb | ||
|
|
08b96f1b9f | ||
|
|
8c7277acd4 | ||
|
|
50f3a81f63 | ||
|
|
2816b1bf8e | ||
|
|
5cfc9c7487 | ||
|
|
ac406a31b0 | ||
|
|
ad0d82d3ce | ||
|
|
22f9399cc3 | ||
|
|
dd64baeba2 | ||
|
|
38dceb3bf7 | ||
|
|
017350e0ea | ||
|
|
a2696cf542 | ||
|
|
6be7bde243 | ||
|
|
7b58c1a694 | ||
|
|
74ae158c2f | ||
|
|
e245115f47 | ||
|
|
c1124228e8 | ||
|
|
02349b3269 | ||
|
|
952bce3023 | ||
|
|
a5daa806f2 | ||
|
|
47bf592db7 | ||
|
|
1fb3e8988b | ||
|
|
d6cb4bbe99 | ||
|
|
03a857f59a | ||
|
|
8d93f0ca56 | ||
|
|
6382ef2bc6 | ||
|
|
79b08c5f0a | ||
|
|
ebc01bf0f6 | ||
|
|
85fce04d1b | ||
|
|
4fb95c91fb | ||
|
|
5f4e402204 | ||
|
|
07b166af64 | ||
|
|
caf5b8e975 | ||
|
|
4cbeb9a7eb | ||
|
|
851b730373 | ||
|
|
5d100293fb | ||
|
|
6b81d10030 | ||
|
|
f5457cc3d2 | ||
|
|
fac8bfc03f | ||
|
|
4690236724 | ||
|
|
b9345b3fc6 | ||
|
|
4d23a85c29 | ||
|
|
4c0e9f85c5 | ||
|
|
c64a1c25c4 | ||
|
|
442fdbfc53 | ||
|
|
89fc2d7f48 | ||
|
|
6a1b738e0b | ||
|
|
95ebfa5610 | ||
|
|
1fa2e7cc86 | ||
|
|
91c79f2445 | ||
|
|
0924eee81b | ||
|
|
5f6f7aaa27 | ||
|
|
e84c1dc95f | ||
|
|
5960bac11e | ||
|
|
3208979655 | ||
|
|
fbdb3bcf1e | ||
|
|
e1b00757a6 | ||
|
|
7f0d1b8cc0 | ||
|
|
92569b1f0d | ||
|
|
955e9088d7 | ||
|
|
70a56b92a6 | ||
|
|
b4a12c88e7 | ||
|
|
0327ab9616 | ||
|
|
d180aaa2a7 | ||
|
|
809455aaae | ||
|
|
9214b5d02e | ||
|
|
5a7590d94c | ||
|
|
4d2af0d664 | ||
|
|
620f70e42c | ||
|
|
af5cb0f853 | ||
|
|
175a9b9caa | ||
|
|
2826e6dada | ||
|
|
a0df694c24 | ||
|
|
5f511324b6 | ||
|
|
063432d7e3 | ||
|
|
1b8d3375c8 | ||
|
|
4fbdf100c4 | ||
|
|
4bb8ff7c8e | ||
|
|
03000fee5f | ||
|
|
b44dd38360 | ||
|
|
e2209e1104 | ||
|
|
2c50687279 | ||
|
|
3e9d794ea5 | ||
|
|
9d4cad6307 | ||
|
|
53ae431867 | ||
|
|
3202bdd744 | ||
|
|
c96fd24f48 | ||
|
|
c77a54fe0a | ||
|
|
974d712fbe | ||
|
|
5997bb47a8 | ||
|
|
f338cc6c94 | ||
|
|
e8ea9669c9 | ||
|
|
6b4ef92c6f | ||
|
|
c50256d25c | ||
|
|
4aa5ebe591 | ||
|
|
dfd4a42b35 | ||
|
|
9433d03705 | ||
|
|
87a6bed9e9 | ||
|
|
6f9ecd899e | ||
|
|
6d2301988f | ||
|
|
910df0f795 | ||
|
|
8e760d5f62 | ||
|
|
9e0dbb7337 | ||
|
|
9e99b8c068 | ||
|
|
1d5dfda3d4 | ||
|
|
6a6d8f60c4 | ||
|
|
dad204a54d | ||
|
|
2940deb51d | ||
|
|
ca57f17d3d | ||
|
|
8a93ae4f18 | ||
|
|
a1e96ae94f | ||
|
|
5ddad41245 | ||
|
|
dbd80465c8 | ||
|
|
f2931af61e | ||
|
|
8b16f81882 | ||
|
|
0cbf3a146f | ||
|
|
8132cf8153 | ||
|
|
d0f087db2d | ||
|
|
9c88d1b99e | ||
|
|
24ba7c9762 | ||
|
|
f722aa8c75 | ||
|
|
ff49649130 | ||
|
|
5426f06ac2 | ||
|
|
0a2427f79b | ||
|
|
e571a01853 | ||
|
|
3b81baaaaf | ||
|
|
40a4053732 | ||
|
|
94b61bdcf6 | ||
|
|
68dba5f2eb | ||
|
|
f3ce9322b0 | ||
|
|
63886bdc59 | ||
|
|
bae7cf8cce | ||
|
|
fae1799646 | ||
|
|
138d21aea8 | ||
|
|
7a99699e2d | ||
|
|
ed9ae70487 | ||
|
|
77df3785f1 | ||
|
|
ec521e6bfc | ||
|
|
73210a93df | ||
|
|
2ab7bd13e2 | ||
|
|
677e95031e | ||
|
|
2d8a4c4390 | ||
|
|
63b3cb37db | ||
|
|
e4a7e8222e | ||
|
|
8e08ae5bb9 | ||
|
|
df63461ff0 | ||
|
|
720ff55262 | ||
|
|
446267d1bf | ||
|
|
78b3b52663 | ||
|
|
0eee63f074 | ||
|
|
db814543c0 | ||
|
|
0518492158 | ||
|
|
968fae2603 | ||
|
|
3893ff1604 | ||
|
|
94d2182717 | ||
|
|
31c633e528 | ||
|
|
50660d54e8 | ||
|
|
0b95eb3612 | ||
|
|
e6408b2e7a | ||
|
|
446aad4ce2 | ||
|
|
dc851c922e | ||
|
|
00b5731ecb | ||
|
|
e610555e10 | ||
|
|
514fdfa268 | ||
|
|
149887a0ff | ||
|
|
d551e43a9b | ||
|
|
8cca6bc58c | ||
|
|
5c4c046132 | ||
|
|
d2619e0b53 | ||
|
|
02cbfcfe2c | ||
|
|
894a0515d9 | ||
|
|
b7a92867de | ||
|
|
874fffb7dc | ||
|
|
73e388e0d8 | ||
|
|
2d6f603c2b | ||
|
|
3f1f3d0827 | ||
|
|
1f1acd98f1 | ||
|
|
a0e6c80b60 | ||
|
|
63a18efb27 | ||
|
|
cbb962fd77 | ||
|
|
c9f42a7b85 | ||
|
|
e9fa557ead | ||
|
|
0afed995ce | ||
|
|
6331ed16e5 | ||
|
|
c424df5192 | ||
|
|
942a2e7d68 | ||
|
|
d96e031dfc | ||
|
|
aa1213e089 | ||
|
|
1d273e4430 | ||
|
|
02e91a96dd | ||
|
|
9d5fb49cd8 | ||
|
|
28cbb6dc21 | ||
|
|
8a081ce588 | ||
|
|
714e41d472 | ||
|
|
ac035108aa | ||
|
|
c8252759df | ||
|
|
347a153b3d | ||
|
|
53234e5947 | ||
|
|
da7f24c238 | ||
|
|
3fa5d05997 | ||
|
|
94e213c6c1 | ||
|
|
4d2be9f432 | ||
|
|
0af3401553 | ||
|
|
8bd8ea7c04 | ||
|
|
e2fbf8bc74 |
2
.buildpacks
Normal file
2
.buildpacks
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
https://github.com/Scalingo/nodejs-buildpack
|
||||||
|
https://github.com/Scalingo/ruby-buildpack
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
engines:
|
engines:
|
||||||
duplication:
|
duplication:
|
||||||
enabled: true
|
enabled: false
|
||||||
config:
|
|
||||||
languages:
|
|
||||||
- ruby
|
|
||||||
- javascript
|
|
||||||
rubocop:
|
rubocop:
|
||||||
enabled: true
|
enabled: true
|
||||||
eslint:
|
eslint:
|
||||||
|
|||||||
@@ -5,3 +5,7 @@ public/assets
|
|||||||
node_modules
|
node_modules
|
||||||
storybook
|
storybook
|
||||||
neo4j
|
neo4j
|
||||||
|
vendor/bundle
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
|||||||
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# EditorConfig is awesome: http://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# Unix-style newlines with a newline ending every file
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
# Service dependencies
|
# Service dependencies
|
||||||
REDIS_HOST=redis
|
REDIS_HOST=redis
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
# REDIS_DB=0
|
||||||
DB_HOST=db
|
DB_HOST=db
|
||||||
DB_USER=postgres
|
DB_USER=postgres
|
||||||
DB_NAME=postgres
|
DB_NAME=postgres
|
||||||
@@ -11,6 +12,10 @@ DB_PORT=5432
|
|||||||
LOCAL_DOMAIN=example.com
|
LOCAL_DOMAIN=example.com
|
||||||
LOCAL_HTTPS=true
|
LOCAL_HTTPS=true
|
||||||
|
|
||||||
|
# Use this only if you need to run mastodon on a different domain than the one used for federation.
|
||||||
|
# Do not use this unless you know exactly what you are doing.
|
||||||
|
# WEB_DOMAIN=mastodon.example.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 `rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)
|
||||||
PAPERCLIP_SECRET=
|
PAPERCLIP_SECRET=
|
||||||
@@ -22,13 +27,28 @@ OTP_SECRET=
|
|||||||
# SINGLE_USER_MODE=true
|
# SINGLE_USER_MODE=true
|
||||||
# Prevent registrations with following e-mail domains
|
# Prevent registrations with following e-mail domains
|
||||||
# EMAIL_DOMAIN_BLACKLIST=example1.com|example2.de|etc
|
# EMAIL_DOMAIN_BLACKLIST=example1.com|example2.de|etc
|
||||||
|
# Only allow registrations with the following e-mail domains
|
||||||
|
# EMAIL_DOMAIN_WHITELIST=example1.com|example2.de|etc
|
||||||
|
|
||||||
|
# Optionally change default language
|
||||||
|
# DEFAULT_LOCALE=de
|
||||||
|
|
||||||
# E-mail configuration
|
# E-mail configuration
|
||||||
|
# Note: Mailgun and SparkPost (https://sparkpo.st/smtp) each have good free tiers
|
||||||
SMTP_SERVER=smtp.mailgun.org
|
SMTP_SERVER=smtp.mailgun.org
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_LOGIN=
|
SMTP_LOGIN=
|
||||||
SMTP_PASSWORD=
|
SMTP_PASSWORD=
|
||||||
SMTP_FROM_ADDRESS=notifications@example.com
|
SMTP_FROM_ADDRESS=notifications@example.com
|
||||||
|
#SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail
|
||||||
|
#SMTP_AUTH_METHOD=plain
|
||||||
|
#SMTP_OPENSSL_VERIFY_MODE=peer
|
||||||
|
#SMTP_ENABLE_STARTTLS_AUTO=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.
|
||||||
|
# PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system
|
||||||
|
# PAPERCLIP_ROOT_URL=/system
|
||||||
|
|
||||||
# Optional asset host for multi-server setups
|
# Optional asset host for multi-server setups
|
||||||
# CDN_HOST=assets.example.com
|
# CDN_HOST=assets.example.com
|
||||||
@@ -39,9 +59,25 @@ SMTP_FROM_ADDRESS=notifications@example.com
|
|||||||
# AWS_ACCESS_KEY_ID=
|
# AWS_ACCESS_KEY_ID=
|
||||||
# AWS_SECRET_ACCESS_KEY=
|
# AWS_SECRET_ACCESS_KEY=
|
||||||
# S3_REGION=
|
# S3_REGION=
|
||||||
|
# S3_PROTOCOL=http
|
||||||
|
# S3_HOSTNAME=192.168.1.123:9000
|
||||||
|
|
||||||
|
# S3 (Minio Config (optional) Please check Minio instance for details)
|
||||||
|
# S3_ENABLED=true
|
||||||
|
# S3_BUCKET=
|
||||||
|
# AWS_ACCESS_KEY_ID=
|
||||||
|
# AWS_SECRET_ACCESS_KEY=
|
||||||
|
# S3_REGION=
|
||||||
|
# S3_PROTOCOL=https
|
||||||
|
# S3_HOSTNAME=
|
||||||
|
# S3_ENDPOINT=
|
||||||
|
|
||||||
# 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=
|
||||||
|
|
||||||
# Streaming API integration
|
# Streaming API integration
|
||||||
# STREAMING_API_BASE_URL=
|
# STREAMING_API_BASE_URL=
|
||||||
|
|
||||||
|
# Advanced settings
|
||||||
|
# If you need to use pgBouncer, you need to disable prepared statements:
|
||||||
|
# PREPARED_STATEMENTS=false
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
# Federation
|
# Federation
|
||||||
LOCAL_DOMAIN=cb6e6126.ngrok.io
|
LOCAL_DOMAIN=cb6e6126.ngrok.io
|
||||||
LOCAL_HTTPS=true
|
LOCAL_HTTPS=true
|
||||||
|
OTP_SECRET=100c7faeef00caa29242f6b04156742bf76065771fd4117990c4282b8748ff3d99f8fdae97c982ab5bd2e6756a159121377cce4421f4a8ecd2d67bd7749a3fb4
|
||||||
|
|||||||
30
.eslintignore
Normal file
30
.eslintignore
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
|
||||||
|
#
|
||||||
|
# If you find yourself ignoring temporary files generated by your text editor
|
||||||
|
# or operating system, you probably want to add a global ignore instead:
|
||||||
|
# git config --global core.excludesfile '~/.gitignore_global'
|
||||||
|
|
||||||
|
# Ignore bundler config.
|
||||||
|
/.bundle
|
||||||
|
|
||||||
|
# Ignore the default SQLite database.
|
||||||
|
/db/*.sqlite3
|
||||||
|
/db/*.sqlite3-journal
|
||||||
|
|
||||||
|
# Ignore all logfiles and tempfiles.
|
||||||
|
/log/*
|
||||||
|
!/log/.keep
|
||||||
|
/tmp
|
||||||
|
coverage
|
||||||
|
public/system
|
||||||
|
public/assets
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
node_modules/
|
||||||
|
neo4j/
|
||||||
|
|
||||||
|
# Ignore Vagrant files
|
||||||
|
.vagrant/
|
||||||
|
|
||||||
|
# Ignore Capistrano customizations
|
||||||
|
config/deploy/*
|
||||||
34
.eslintrc
34
.eslintrc
@@ -8,7 +8,8 @@
|
|||||||
"parser": "babel-eslint",
|
"parser": "babel-eslint",
|
||||||
|
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"react"
|
"react",
|
||||||
|
"jsx-a11y"
|
||||||
],
|
],
|
||||||
|
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
@@ -43,9 +44,36 @@
|
|||||||
"no-mixed-spaces-and-tabs": 1,
|
"no-mixed-spaces-and-tabs": 1,
|
||||||
"no-nested-ternary": 1,
|
"no-nested-ternary": 1,
|
||||||
"no-trailing-spaces": 1,
|
"no-trailing-spaces": 1,
|
||||||
"react/wrap-multilines": 2,
|
|
||||||
|
"react/jsx-wrap-multilines": 2,
|
||||||
"react/self-closing-comp": 2,
|
"react/self-closing-comp": 2,
|
||||||
"react/prop-types": 2,
|
"react/prop-types": 2,
|
||||||
"react/no-multi-comp": 0
|
"react/no-multi-comp": 0,
|
||||||
|
|
||||||
|
"jsx-a11y/accessible-emoji": 1,
|
||||||
|
"jsx-a11y/anchor-has-content": 1,
|
||||||
|
"jsx-a11y/aria-activedescendant-has-tabindex": 1,
|
||||||
|
"jsx-a11y/aria-props": 1,
|
||||||
|
"jsx-a11y/aria-proptypes": 1,
|
||||||
|
"jsx-a11y/aria-role": 1,
|
||||||
|
"jsx-a11y/aria-unsupported-elements": 1,
|
||||||
|
"jsx-a11y/heading-has-content": 1,
|
||||||
|
"jsx-a11y/href-no-hash": 1,
|
||||||
|
"jsx-a11y/html-has-lang": 1,
|
||||||
|
"jsx-a11y/iframe-has-title": 1,
|
||||||
|
"jsx-a11y/img-has-alt": 1,
|
||||||
|
"jsx-a11y/img-redundant-alt": 1,
|
||||||
|
"jsx-a11y/label-has-for": 1,
|
||||||
|
"jsx-a11y/mouse-events-have-key-events": 1,
|
||||||
|
"jsx-a11y/no-access-key": 1,
|
||||||
|
"jsx-a11y/no-distracting-elements": 1,
|
||||||
|
"jsx-a11y/no-onchange": 1,
|
||||||
|
"jsx-a11y/no-redundant-roles": 1,
|
||||||
|
"jsx-a11y/onclick-has-focus": 1,
|
||||||
|
"jsx-a11y/onclick-has-role": 1,
|
||||||
|
"jsx-a11y/role-has-required-aria-props": 1,
|
||||||
|
"jsx-a11y/role-supports-aria-props": 1,
|
||||||
|
"jsx-a11y/scope": 1,
|
||||||
|
"jsx-a11y/tabindex-no-positive": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -28,3 +28,17 @@ neo4j/
|
|||||||
|
|
||||||
# Ignore Capistrano customizations
|
# Ignore Capistrano customizations
|
||||||
config/deploy/*
|
config/deploy/*
|
||||||
|
|
||||||
|
# Ignore IDE files
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Ignore postgres + redis volume optionally created by docker-compose
|
||||||
|
postgres
|
||||||
|
redis
|
||||||
|
|
||||||
|
# Ignore Apple files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Ignore vim files
|
||||||
|
*~
|
||||||
|
*.swp
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
2.3.1
|
2.4.1
|
||||||
|
|||||||
5
.slugignore
Normal file
5
.slugignore
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
.cache/
|
||||||
|
docs/
|
||||||
|
spec/
|
||||||
|
storybook/
|
||||||
@@ -5,8 +5,6 @@ notifications:
|
|||||||
email: false
|
email: false
|
||||||
|
|
||||||
env:
|
env:
|
||||||
matrix:
|
|
||||||
- TRAVIS_NODE_VERSION="4"
|
|
||||||
global:
|
global:
|
||||||
- LOCAL_DOMAIN=cb6e6126.ngrok.io
|
- LOCAL_DOMAIN=cb6e6126.ngrok.io
|
||||||
- LOCAL_HTTPS=true
|
- LOCAL_HTTPS=true
|
||||||
@@ -16,7 +14,8 @@ addons:
|
|||||||
postgresql: 9.4
|
postgresql: 9.4
|
||||||
|
|
||||||
rvm:
|
rvm:
|
||||||
- 2.3.1
|
- 2.3.4
|
||||||
|
- 2.4.1
|
||||||
|
|
||||||
services:
|
services:
|
||||||
- redis-server
|
- redis-server
|
||||||
@@ -28,8 +27,7 @@ before_install:
|
|||||||
- sudo apt-get -qq update
|
- sudo apt-get -qq update
|
||||||
- sudo apt-get -qq install g++-4.8
|
- sudo apt-get -qq install g++-4.8
|
||||||
install:
|
install:
|
||||||
- nvm install $TRAVIS_NODE_VERSION
|
- nvm install
|
||||||
- npm install -g npm@3
|
|
||||||
- npm install -g yarn
|
- npm install -g yarn
|
||||||
- bundle install
|
- bundle install
|
||||||
- yarn install
|
- yarn install
|
||||||
@@ -40,3 +38,4 @@ before_script:
|
|||||||
script:
|
script:
|
||||||
- bundle exec rspec
|
- bundle exec rspec
|
||||||
- npm test
|
- npm test
|
||||||
|
- i18n-tasks unused
|
||||||
|
|||||||
44
CONTRIBUTING.md
Normal file
44
CONTRIBUTING.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
CONTRIBUTING
|
||||||
|
============
|
||||||
|
|
||||||
|
There are three ways in which you can contribute to this repository:
|
||||||
|
|
||||||
|
1. By improving the documentation
|
||||||
|
2. By working on the back-end application
|
||||||
|
3. By working on the front-end application
|
||||||
|
|
||||||
|
Choosing what to work on in a large open source project is not easy. The list of GitHub issues may provide some ideas, but not every feature request has been greenlit. Likewise, not every change or feature that resolves a personal itch will be merged into the main repository. Some communication ahead of time may be wise. If your addition creates a new feature or setting, or otherwise changes how things work in some substantial way, please remember to submit a correlating pull request to document your changes in the [documentation](http://github.com/tootsuite/documentation).
|
||||||
|
|
||||||
|
Below are the guidelines for working on pull requests:
|
||||||
|
|
||||||
|
## General
|
||||||
|
|
||||||
|
- 2 spaces indentation
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- No spelling mistakes
|
||||||
|
- No orthographic mistakes
|
||||||
|
- No Markdown syntax errors
|
||||||
|
|
||||||
|
## Back-end application
|
||||||
|
|
||||||
|
It is expected that you have a working development environment set up. The development environment includes rubocop, which checks your Ruby code for compliance with our style guide and best practices. Sublime Text, likely like other editors, has a Rubocop plugin that runs checks on files as you edit them. The codebase also has a test suite.
|
||||||
|
|
||||||
|
* The codebase is not perfect, at the time of writing, but it is expected that you do not introduce new code style violations
|
||||||
|
* The rspec test suite must pass
|
||||||
|
* To the extent that it is possible, verify your changes. In the best case, by adding new tests to the test suite. At the very least, by running the server or console and checking it manually
|
||||||
|
* If you are introducing new strings to the user interface, they must be using localization methods
|
||||||
|
|
||||||
|
If your code has syntax errors that won't let it run, it's a good sign that the pull request isn't ready for submission yet.
|
||||||
|
|
||||||
|
## Front-end application
|
||||||
|
|
||||||
|
It is expected that you have a working development environment set up (see back-end application section). This project includes an ESLint configuration file, with which you can lint your changes.
|
||||||
|
|
||||||
|
* Avoid grave ESLint violations
|
||||||
|
* Verify that your changes work
|
||||||
|
* 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.
|
||||||
|
|
||||||
1
Capfile
1
Capfile
@@ -8,6 +8,7 @@ require 'capistrano/rbenv'
|
|||||||
require 'capistrano/bundler'
|
require 'capistrano/bundler'
|
||||||
require 'capistrano/yarn'
|
require 'capistrano/yarn'
|
||||||
require 'capistrano/rails/assets'
|
require 'capistrano/rails/assets'
|
||||||
|
require 'capistrano/faster_assets'
|
||||||
require 'capistrano/rails/migrations'
|
require 'capistrano/rails/migrations'
|
||||||
|
|
||||||
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }
|
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }
|
||||||
|
|||||||
48
Dockerfile
48
Dockerfile
@@ -1,23 +1,41 @@
|
|||||||
FROM ruby:2.3.1
|
FROM ruby:2.4.1-alpine
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
LABEL maintainer="https://github.com/tootsuite/mastodon" \
|
||||||
|
description="A GNU Social-compatible microblogging server"
|
||||||
|
|
||||||
RUN echo 'deb http://httpredir.debian.org/debian jessie-backports main contrib non-free' >> /etc/apt/sources.list
|
ENV RAILS_ENV=production \
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_4.x | bash -
|
NODE_ENV=production
|
||||||
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev libxml2-dev libxslt1-dev nodejs ffmpeg && rm -rf /var/lib/apt/lists/*
|
|
||||||
RUN npm install -g npm@3 && npm install -g yarn
|
EXPOSE 3000 4000
|
||||||
RUN mkdir /mastodon
|
|
||||||
|
|
||||||
WORKDIR /mastodon
|
WORKDIR /mastodon
|
||||||
|
|
||||||
ADD Gemfile /mastodon/Gemfile
|
COPY Gemfile Gemfile.lock package.json yarn.lock /mastodon/
|
||||||
ADD Gemfile.lock /mastodon/Gemfile.lock
|
|
||||||
RUN bundle install --deployment --without test development
|
|
||||||
|
|
||||||
ADD package.json /mastodon/package.json
|
RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories \
|
||||||
ADD yarn.lock /mastodon/yarn.lock
|
&& BUILD_DEPS=" \
|
||||||
RUN yarn
|
postgresql-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
libxslt-dev \
|
||||||
|
build-base" \
|
||||||
|
&& apk -U upgrade && apk add \
|
||||||
|
$BUILD_DEPS \
|
||||||
|
nodejs@edge \
|
||||||
|
nodejs-npm@edge \
|
||||||
|
libpq \
|
||||||
|
libxml2 \
|
||||||
|
libxslt \
|
||||||
|
ffmpeg \
|
||||||
|
file \
|
||||||
|
imagemagick@edge \
|
||||||
|
&& npm install -g npm@3 && npm install -g yarn \
|
||||||
|
&& bundle install --deployment --without test development \
|
||||||
|
&& yarn --ignore-optional \
|
||||||
|
&& yarn cache clean \
|
||||||
|
&& npm -g cache clean \
|
||||||
|
&& apk del $BUILD_DEPS \
|
||||||
|
&& rm -rf /tmp/* /var/cache/apk/*
|
||||||
|
|
||||||
ADD . /mastodon
|
COPY . /mastodon
|
||||||
|
|
||||||
VOLUME ["/mastodon/public/system", "/mastodon/public/assets"]
|
VOLUME /mastodon/public/system /mastodon/public/assets
|
||||||
|
|||||||
61
Gemfile
61
Gemfile
@@ -1,15 +1,14 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
ruby '2.3.1'
|
ruby '>= 2.3.0', '< 2.5.0'
|
||||||
|
|
||||||
gem 'rails', '~> 5.0.1.0'
|
gem 'pkg-config'
|
||||||
|
|
||||||
|
gem 'rails', '~> 5.0.2'
|
||||||
gem 'sass-rails', '~> 5.0'
|
gem 'sass-rails', '~> 5.0'
|
||||||
gem 'uglifier', '>= 1.3.0'
|
gem 'uglifier', '>= 1.3.0'
|
||||||
gem 'coffee-rails', '~> 4.1.0'
|
|
||||||
gem 'jquery-rails'
|
gem 'jquery-rails'
|
||||||
gem 'jbuilder', '~> 2.0'
|
|
||||||
gem 'sdoc', '~> 0.4.0', group: :doc
|
|
||||||
gem 'puma'
|
gem 'puma'
|
||||||
|
|
||||||
gem 'hamlit-rails'
|
gem 'hamlit-rails'
|
||||||
@@ -23,33 +22,41 @@ gem 'paperclip', '~> 5.1'
|
|||||||
gem 'paperclip-av-transcoder'
|
gem 'paperclip-av-transcoder'
|
||||||
gem 'aws-sdk', '>= 2.0'
|
gem 'aws-sdk', '>= 2.0'
|
||||||
|
|
||||||
gem 'http'
|
|
||||||
gem 'httplog'
|
|
||||||
gem 'addressable'
|
gem 'addressable'
|
||||||
gem 'nokogiri'
|
|
||||||
gem 'link_header'
|
|
||||||
gem 'ostatus2'
|
|
||||||
gem 'goldfinger'
|
|
||||||
gem 'devise'
|
gem 'devise'
|
||||||
gem 'devise-two-factor'
|
gem 'devise-two-factor'
|
||||||
gem 'doorkeeper'
|
gem 'doorkeeper'
|
||||||
gem 'rabl'
|
|
||||||
gem 'rqrcode'
|
|
||||||
gem 'oj'
|
|
||||||
gem 'hiredis'
|
|
||||||
gem 'redis', '~>3.2'
|
|
||||||
gem 'fast_blank'
|
gem 'fast_blank'
|
||||||
|
gem 'goldfinger'
|
||||||
|
gem 'hiredis'
|
||||||
gem 'htmlentities'
|
gem 'htmlentities'
|
||||||
gem 'simple_form'
|
gem 'http'
|
||||||
gem 'will_paginate'
|
gem 'http_accept_language'
|
||||||
|
gem 'httplog'
|
||||||
|
gem 'kaminari'
|
||||||
|
gem 'link_header'
|
||||||
|
gem 'nokogiri'
|
||||||
|
gem 'oj'
|
||||||
|
gem 'ostatus2', '~> 1.1'
|
||||||
|
gem 'ox'
|
||||||
|
gem 'rabl'
|
||||||
gem 'rack-attack'
|
gem 'rack-attack'
|
||||||
gem 'rack-cors', require: 'rack/cors'
|
gem 'rack-cors', require: 'rack/cors'
|
||||||
gem 'sidekiq'
|
gem 'rack-timeout'
|
||||||
|
gem 'rails-i18n'
|
||||||
gem 'rails-settings-cached'
|
gem 'rails-settings-cached'
|
||||||
gem 'pg_search'
|
gem 'redis', '~>3.2', require: ['redis', 'redis/connection/hiredis']
|
||||||
gem 'simple-navigation'
|
gem 'rqrcode'
|
||||||
gem 'statsd-instrument'
|
|
||||||
gem 'ruby-oembed', require: 'oembed'
|
gem 'ruby-oembed', require: 'oembed'
|
||||||
|
gem 'sidekiq'
|
||||||
|
gem 'sidekiq-unique-jobs'
|
||||||
|
gem 'simple-navigation'
|
||||||
|
gem 'simple_form'
|
||||||
|
gem 'sprockets-rails', :require => 'sprockets/railtie'
|
||||||
|
gem 'statsd-instrument'
|
||||||
|
gem 'twitter-text'
|
||||||
|
gem 'tzinfo-data'
|
||||||
|
gem 'whatlanguage'
|
||||||
|
|
||||||
gem 'react-rails'
|
gem 'react-rails'
|
||||||
gem 'browserify-rails'
|
gem 'browserify-rails'
|
||||||
@@ -64,9 +71,13 @@ group :development, :test do
|
|||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
|
gem 'capybara'
|
||||||
|
gem 'faker'
|
||||||
|
gem 'microformats2'
|
||||||
|
gem 'rails-controller-testing'
|
||||||
|
gem 'rspec-sidekiq'
|
||||||
gem 'simplecov', require: false
|
gem 'simplecov', require: false
|
||||||
gem 'webmock'
|
gem 'webmock'
|
||||||
gem 'rspec-sidekiq'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
@@ -78,15 +89,15 @@ group :development do
|
|||||||
gem 'bullet'
|
gem 'bullet'
|
||||||
gem 'active_record_query_trace'
|
gem 'active_record_query_trace'
|
||||||
|
|
||||||
gem 'capistrano'
|
gem 'capistrano', '3.8.0'
|
||||||
gem 'capistrano-rails'
|
gem 'capistrano-rails'
|
||||||
gem 'capistrano-rbenv'
|
gem 'capistrano-rbenv'
|
||||||
gem 'capistrano-yarn'
|
gem 'capistrano-yarn'
|
||||||
|
gem 'capistrano-faster-assets', '~> 1.0'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :production do
|
group :production do
|
||||||
gem 'rails_12factor'
|
gem 'rails_12factor'
|
||||||
gem 'redis-rails'
|
gem 'redis-rails'
|
||||||
gem 'lograge'
|
gem 'lograge'
|
||||||
gem 'rack-timeout'
|
|
||||||
end
|
end
|
||||||
|
|||||||
367
Gemfile.lock
367
Gemfile.lock
@@ -1,63 +1,63 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (5.0.1)
|
actioncable (5.0.2)
|
||||||
actionpack (= 5.0.1)
|
actionpack (= 5.0.2)
|
||||||
nio4r (~> 1.2)
|
nio4r (>= 1.2, < 3.0)
|
||||||
websocket-driver (~> 0.6.1)
|
websocket-driver (~> 0.6.1)
|
||||||
actionmailer (5.0.1)
|
actionmailer (5.0.2)
|
||||||
actionpack (= 5.0.1)
|
actionpack (= 5.0.2)
|
||||||
actionview (= 5.0.1)
|
actionview (= 5.0.2)
|
||||||
activejob (= 5.0.1)
|
activejob (= 5.0.2)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (5.0.1)
|
actionpack (5.0.2)
|
||||||
actionview (= 5.0.1)
|
actionview (= 5.0.2)
|
||||||
activesupport (= 5.0.1)
|
activesupport (= 5.0.2)
|
||||||
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.0.1)
|
actionview (5.0.2)
|
||||||
activesupport (= 5.0.1)
|
activesupport (= 5.0.2)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubis (~> 2.7.0)
|
erubis (~> 2.7.0)
|
||||||
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.3)
|
||||||
active_record_query_trace (1.5.3)
|
active_record_query_trace (1.5.4)
|
||||||
activejob (5.0.1)
|
activejob (5.0.2)
|
||||||
activesupport (= 5.0.1)
|
activesupport (= 5.0.2)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (5.0.1)
|
activemodel (5.0.2)
|
||||||
activesupport (= 5.0.1)
|
activesupport (= 5.0.2)
|
||||||
activerecord (5.0.1)
|
activerecord (5.0.2)
|
||||||
activemodel (= 5.0.1)
|
activemodel (= 5.0.2)
|
||||||
activesupport (= 5.0.1)
|
activesupport (= 5.0.2)
|
||||||
arel (~> 7.0)
|
arel (~> 7.0)
|
||||||
activesupport (5.0.1)
|
activesupport (5.0.2)
|
||||||
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.0)
|
addressable (2.5.1)
|
||||||
public_suffix (~> 2.0, >= 2.0.2)
|
public_suffix (~> 2.0, >= 2.0.2)
|
||||||
airbrussh (1.1.2)
|
airbrussh (1.2.0)
|
||||||
sshkit (>= 1.6.1, != 1.7.0)
|
sshkit (>= 1.6.1, != 1.7.0)
|
||||||
arel (7.1.4)
|
arel (7.1.4)
|
||||||
ast (2.3.0)
|
ast (2.3.0)
|
||||||
attr_encrypted (3.0.3)
|
attr_encrypted (3.0.3)
|
||||||
encryptor (~> 3.0.0)
|
encryptor (~> 3.0.0)
|
||||||
autoprefixer-rails (6.5.0.2)
|
autoprefixer-rails (6.7.7.1)
|
||||||
execjs
|
execjs
|
||||||
av (0.9.0)
|
av (0.9.0)
|
||||||
cocaine (~> 0.5.3)
|
cocaine (~> 0.5.3)
|
||||||
aws-sdk (2.6.28)
|
aws-sdk (2.9.6)
|
||||||
aws-sdk-resources (= 2.6.28)
|
aws-sdk-resources (= 2.9.6)
|
||||||
aws-sdk-core (2.6.28)
|
aws-sdk-core (2.9.6)
|
||||||
aws-sigv4 (~> 1.0)
|
aws-sigv4 (~> 1.0)
|
||||||
jmespath (~> 1.0)
|
jmespath (~> 1.0)
|
||||||
aws-sdk-resources (2.6.28)
|
aws-sdk-resources (2.9.6)
|
||||||
aws-sdk-core (= 2.6.28)
|
aws-sdk-core (= 2.9.6)
|
||||||
aws-sigv4 (1.0.0)
|
aws-sigv4 (1.0.0)
|
||||||
babel-source (5.8.35)
|
babel-source (5.8.35)
|
||||||
babel-transpiler (0.7.0)
|
babel-transpiler (0.7.0)
|
||||||
@@ -73,24 +73,25 @@ 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)
|
||||||
browserify-rails (3.1.0)
|
browserify-rails (4.1.0)
|
||||||
|
addressable (>= 2.4.0)
|
||||||
railties (>= 4.0.0, < 5.1)
|
railties (>= 4.0.0, < 5.1)
|
||||||
sprockets (>= 3.5.2)
|
sprockets (>= 3.6.0)
|
||||||
builder (3.2.3)
|
builder (3.2.3)
|
||||||
bullet (5.3.0)
|
bullet (5.5.1)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
uniform_notifier (~> 1.10.0)
|
uniform_notifier (~> 1.10.0)
|
||||||
capistrano (3.7.2)
|
capistrano (3.8.0)
|
||||||
airbrussh (>= 1.0.0)
|
airbrussh (>= 1.0.0)
|
||||||
capistrano-harrow
|
|
||||||
i18n
|
i18n
|
||||||
rake (>= 10.0.0)
|
rake (>= 10.0.0)
|
||||||
sshkit (>= 1.9.0)
|
sshkit (>= 1.9.0)
|
||||||
capistrano-bundler (1.2.0)
|
capistrano-bundler (1.2.0)
|
||||||
capistrano (~> 3.1)
|
capistrano (~> 3.1)
|
||||||
sshkit (~> 1.2)
|
sshkit (~> 1.2)
|
||||||
capistrano-harrow (0.5.3)
|
capistrano-faster-assets (1.0.2)
|
||||||
capistrano-rails (1.2.2)
|
capistrano (>= 3.1)
|
||||||
|
capistrano-rails (1.2.3)
|
||||||
capistrano (~> 3.1)
|
capistrano (~> 3.1)
|
||||||
capistrano-bundler (~> 1.1)
|
capistrano-bundler (~> 1.1)
|
||||||
capistrano-rbenv (2.1.0)
|
capistrano-rbenv (2.1.0)
|
||||||
@@ -98,25 +99,25 @@ GEM
|
|||||||
sshkit (~> 1.3)
|
sshkit (~> 1.3)
|
||||||
capistrano-yarn (2.0.2)
|
capistrano-yarn (2.0.2)
|
||||||
capistrano (~> 3.0)
|
capistrano (~> 3.0)
|
||||||
|
capybara (2.13.0)
|
||||||
|
addressable
|
||||||
|
mime-types (>= 1.16)
|
||||||
|
nokogiri (>= 1.3.3)
|
||||||
|
rack (>= 1.0.0)
|
||||||
|
rack-test (>= 0.5.4)
|
||||||
|
xpath (~> 2.0)
|
||||||
chunky_png (1.3.8)
|
chunky_png (1.3.8)
|
||||||
climate_control (0.1.0)
|
climate_control (0.1.0)
|
||||||
cocaine (0.5.8)
|
cocaine (0.5.8)
|
||||||
climate_control (>= 0.0.3, < 1.0)
|
climate_control (>= 0.0.3, < 1.0)
|
||||||
coderay (1.1.1)
|
coderay (1.1.1)
|
||||||
coffee-rails (4.1.1)
|
|
||||||
coffee-script (>= 2.2.0)
|
|
||||||
railties (>= 4.0.0, < 5.1.x)
|
|
||||||
coffee-script (2.4.1)
|
|
||||||
coffee-script-source
|
|
||||||
execjs
|
|
||||||
coffee-script-source (1.10.0)
|
|
||||||
colorize (0.8.1)
|
colorize (0.8.1)
|
||||||
concurrent-ruby (1.0.4)
|
concurrent-ruby (1.0.5)
|
||||||
connection_pool (2.2.1)
|
connection_pool (2.2.1)
|
||||||
crack (0.4.3)
|
crack (0.4.3)
|
||||||
safe_yaml (~> 1.0.0)
|
safe_yaml (~> 1.0.0)
|
||||||
debug_inspector (0.0.2)
|
debug_inspector (0.0.2)
|
||||||
devise (4.2.0)
|
devise (4.2.1)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0, < 5.1)
|
railties (>= 4.1.0, < 5.1)
|
||||||
@@ -128,16 +129,16 @@ GEM
|
|||||||
devise (~> 4.0)
|
devise (~> 4.0)
|
||||||
railties
|
railties
|
||||||
rotp (~> 2.0)
|
rotp (~> 2.0)
|
||||||
diff-lcs (1.2.5)
|
diff-lcs (1.3)
|
||||||
docile (1.1.5)
|
docile (1.1.5)
|
||||||
domain_name (0.5.20161129)
|
domain_name (0.5.20170404)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
doorkeeper (4.2.0)
|
doorkeeper (4.2.5)
|
||||||
railties (>= 4.2)
|
railties (>= 4.2)
|
||||||
dotenv (2.1.1)
|
dotenv (2.2.0)
|
||||||
dotenv-rails (2.1.1)
|
dotenv-rails (2.2.0)
|
||||||
dotenv (= 2.1.1)
|
dotenv (= 2.2.0)
|
||||||
railties (>= 4.0, < 5.1)
|
railties (>= 3.2, < 5.1)
|
||||||
easy_translate (0.5.0)
|
easy_translate (0.5.0)
|
||||||
json
|
json
|
||||||
thread
|
thread
|
||||||
@@ -145,12 +146,14 @@ GEM
|
|||||||
encryptor (3.0.0)
|
encryptor (3.0.0)
|
||||||
erubis (2.7.0)
|
erubis (2.7.0)
|
||||||
execjs (2.7.0)
|
execjs (2.7.0)
|
||||||
fabrication (2.15.2)
|
fabrication (2.16.1)
|
||||||
|
faker (1.7.3)
|
||||||
|
i18n (~> 0.5)
|
||||||
fast_blank (1.0.0)
|
fast_blank (1.0.0)
|
||||||
font-awesome-rails (4.6.3.1)
|
font-awesome-rails (4.7.0.1)
|
||||||
railties (>= 3.2, < 5.1)
|
railties (>= 3.2, < 5.1)
|
||||||
fuubar (2.1.1)
|
fuubar (2.2.0)
|
||||||
rspec (~> 3.0)
|
rspec-core (~> 3.0)
|
||||||
ruby-progressbar (~> 1.4)
|
ruby-progressbar (~> 1.4)
|
||||||
globalid (0.3.7)
|
globalid (0.3.7)
|
||||||
activesupport (>= 4.1.0)
|
activesupport (>= 4.1.0)
|
||||||
@@ -158,20 +161,20 @@ GEM
|
|||||||
addressable (~> 2.4)
|
addressable (~> 2.4)
|
||||||
http (~> 2.0)
|
http (~> 2.0)
|
||||||
nokogiri (~> 1.6)
|
nokogiri (~> 1.6)
|
||||||
hamlit (2.7.2)
|
hamlit (2.8.1)
|
||||||
temple (~> 0.7.6)
|
temple (>= 0.8.0)
|
||||||
thor
|
thor
|
||||||
tilt
|
tilt
|
||||||
hamlit-rails (0.1.0)
|
hamlit-rails (0.2.0)
|
||||||
actionpack (>= 4.0.1)
|
actionpack (>= 4.0.1)
|
||||||
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.0)
|
hashdiff (0.3.2)
|
||||||
highline (1.7.8)
|
highline (1.7.8)
|
||||||
hiredis (0.6.1)
|
hiredis (0.6.1)
|
||||||
htmlentities (4.3.4)
|
htmlentities (4.3.4)
|
||||||
http (2.1.0)
|
http (2.2.1)
|
||||||
addressable (~> 2.3)
|
addressable (~> 2.3)
|
||||||
http-cookie (~> 1.0)
|
http-cookie (~> 1.0)
|
||||||
http-form_data (~> 1.0.1)
|
http-form_data (~> 1.0.1)
|
||||||
@@ -179,11 +182,12 @@ GEM
|
|||||||
http-cookie (1.0.3)
|
http-cookie (1.0.3)
|
||||||
domain_name (~> 0.5)
|
domain_name (~> 0.5)
|
||||||
http-form_data (1.0.1)
|
http-form_data (1.0.1)
|
||||||
|
http_accept_language (2.1.0)
|
||||||
http_parser.rb (0.6.0)
|
http_parser.rb (0.6.0)
|
||||||
httplog (0.3.2)
|
httplog (0.99.2)
|
||||||
colorize
|
colorize
|
||||||
i18n (0.7.0)
|
i18n (0.8.1)
|
||||||
i18n-tasks (0.9.6)
|
i18n-tasks (0.9.13)
|
||||||
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)
|
||||||
@@ -191,22 +195,31 @@ GEM
|
|||||||
highline (>= 1.7.3)
|
highline (>= 1.7.3)
|
||||||
i18n
|
i18n
|
||||||
parser (>= 2.2.3.0)
|
parser (>= 2.2.3.0)
|
||||||
term-ansicolor (>= 1.3.2)
|
rainbow (~> 2.2)
|
||||||
terminal-table (>= 1.5.1)
|
terminal-table (>= 1.5.1)
|
||||||
jbuilder (2.6.0)
|
|
||||||
activesupport (>= 3.0.0, < 5.1)
|
|
||||||
multi_json (~> 1.2)
|
|
||||||
jmespath (1.3.1)
|
jmespath (1.3.1)
|
||||||
jquery-rails (4.1.1)
|
jquery-rails (4.3.1)
|
||||||
rails-dom-testing (>= 1, < 3)
|
rails-dom-testing (>= 1, < 3)
|
||||||
railties (>= 4.2.0)
|
railties (>= 4.2.0)
|
||||||
thor (>= 0.14, < 2.0)
|
thor (>= 0.14, < 2.0)
|
||||||
json (1.8.3)
|
json (2.0.3)
|
||||||
|
kaminari (1.0.1)
|
||||||
|
activesupport (>= 4.1.0)
|
||||||
|
kaminari-actionview (= 1.0.1)
|
||||||
|
kaminari-activerecord (= 1.0.1)
|
||||||
|
kaminari-core (= 1.0.1)
|
||||||
|
kaminari-actionview (1.0.1)
|
||||||
|
actionview
|
||||||
|
kaminari-core (= 1.0.1)
|
||||||
|
kaminari-activerecord (1.0.1)
|
||||||
|
activerecord
|
||||||
|
kaminari-core (= 1.0.1)
|
||||||
|
kaminari-core (1.0.1)
|
||||||
launchy (2.4.3)
|
launchy (2.4.3)
|
||||||
addressable (~> 2.3)
|
addressable (~> 2.3)
|
||||||
letter_opener (1.4.1)
|
letter_opener (1.4.1)
|
||||||
launchy (~> 2.2)
|
launchy (~> 2.2)
|
||||||
letter_opener_web (1.3.0)
|
letter_opener_web (1.3.1)
|
||||||
actionmailer (>= 3.2)
|
actionmailer (>= 3.2)
|
||||||
letter_opener (~> 1.0)
|
letter_opener (~> 1.0)
|
||||||
railties (>= 3.2)
|
railties (>= 3.2)
|
||||||
@@ -220,25 +233,31 @@ GEM
|
|||||||
mail (2.6.4)
|
mail (2.6.4)
|
||||||
mime-types (>= 1.16, < 4)
|
mime-types (>= 1.16, < 4)
|
||||||
method_source (0.8.2)
|
method_source (0.8.2)
|
||||||
|
microformats2 (2.1.0)
|
||||||
|
activesupport
|
||||||
|
json
|
||||||
|
nokogiri
|
||||||
mime-types (3.1)
|
mime-types (3.1)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2016.0521)
|
mime-types-data (3.2016.0521)
|
||||||
mimemagic (0.3.2)
|
mimemagic (0.3.2)
|
||||||
mini_portile2 (2.1.0)
|
mini_portile2 (2.1.0)
|
||||||
minitest (5.10.1)
|
minitest (5.10.1)
|
||||||
multi_json (1.12.1)
|
|
||||||
net-scp (1.2.1)
|
net-scp (1.2.1)
|
||||||
net-ssh (>= 2.6.5)
|
net-ssh (>= 2.6.5)
|
||||||
net-ssh (4.0.1)
|
net-ssh (4.1.0)
|
||||||
nio4r (1.2.1)
|
nio4r (2.0.0)
|
||||||
nokogiri (1.7.0.1)
|
nokogiri (1.7.1)
|
||||||
mini_portile2 (~> 2.1.0)
|
mini_portile2 (~> 2.1.0)
|
||||||
oj (2.17.3)
|
oj (2.18.5)
|
||||||
|
openssl (2.0.3)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ostatus2 (1.0.2)
|
ostatus2 (1.1.0)
|
||||||
addressable (~> 2.4)
|
addressable (~> 2.4)
|
||||||
http (~> 2.0)
|
http (~> 2.0)
|
||||||
nokogiri (~> 1.6)
|
nokogiri (~> 1.6)
|
||||||
|
openssl (~> 2.0)
|
||||||
|
ox (2.4.11)
|
||||||
paperclip (5.1.0)
|
paperclip (5.1.0)
|
||||||
activemodel (>= 4.2.0)
|
activemodel (>= 4.2.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
@@ -248,52 +267,56 @@ GEM
|
|||||||
paperclip-av-transcoder (0.6.4)
|
paperclip-av-transcoder (0.6.4)
|
||||||
av (~> 0.9.0)
|
av (~> 0.9.0)
|
||||||
paperclip (>= 2.5.2)
|
paperclip (>= 2.5.2)
|
||||||
parser (2.3.1.2)
|
parser (2.4.0.0)
|
||||||
ast (~> 2.2)
|
ast (~> 2.2)
|
||||||
pg (0.18.4)
|
pg (0.20.0)
|
||||||
pg_search (1.0.6)
|
pghero (1.6.4)
|
||||||
activerecord (>= 3.1)
|
|
||||||
activesupport (>= 3.1)
|
|
||||||
arel
|
|
||||||
pghero (1.6.2)
|
|
||||||
activerecord
|
activerecord
|
||||||
|
pkg-config (1.1.7)
|
||||||
powerpack (0.1.1)
|
powerpack (0.1.1)
|
||||||
pry (0.10.4)
|
pry (0.10.4)
|
||||||
coderay (~> 1.1.0)
|
coderay (~> 1.1.0)
|
||||||
method_source (~> 0.8.1)
|
method_source (~> 0.8.1)
|
||||||
slop (~> 3.4)
|
slop (~> 3.4)
|
||||||
pry-rails (0.3.4)
|
pry-rails (0.3.6)
|
||||||
pry (>= 0.9.10)
|
pry (>= 0.10.4)
|
||||||
public_suffix (2.0.4)
|
public_suffix (2.0.5)
|
||||||
puma (3.6.0)
|
puma (3.8.2)
|
||||||
rabl (0.13.1)
|
rabl (0.13.1)
|
||||||
activesupport (>= 2.3.14)
|
activesupport (>= 2.3.14)
|
||||||
rack (2.0.1)
|
rack (2.0.1)
|
||||||
rack-attack (5.0.1)
|
rack-attack (5.0.1)
|
||||||
rack
|
rack
|
||||||
rack-cors (0.4.0)
|
rack-cors (0.4.1)
|
||||||
rack-protection (1.5.3)
|
rack-protection (1.5.3)
|
||||||
rack
|
rack
|
||||||
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.0.1)
|
rails (5.0.2)
|
||||||
actioncable (= 5.0.1)
|
actioncable (= 5.0.2)
|
||||||
actionmailer (= 5.0.1)
|
actionmailer (= 5.0.2)
|
||||||
actionpack (= 5.0.1)
|
actionpack (= 5.0.2)
|
||||||
actionview (= 5.0.1)
|
actionview (= 5.0.2)
|
||||||
activejob (= 5.0.1)
|
activejob (= 5.0.2)
|
||||||
activemodel (= 5.0.1)
|
activemodel (= 5.0.2)
|
||||||
activerecord (= 5.0.1)
|
activerecord (= 5.0.2)
|
||||||
activesupport (= 5.0.1)
|
activesupport (= 5.0.2)
|
||||||
bundler (>= 1.3.0, < 2.0)
|
bundler (>= 1.3.0, < 2.0)
|
||||||
railties (= 5.0.1)
|
railties (= 5.0.2)
|
||||||
sprockets-rails (>= 2.0.0)
|
sprockets-rails (>= 2.0.0)
|
||||||
|
rails-controller-testing (1.0.1)
|
||||||
|
actionpack (~> 5.x)
|
||||||
|
actionview (~> 5.x)
|
||||||
|
activesupport (~> 5.x)
|
||||||
rails-dom-testing (2.0.2)
|
rails-dom-testing (2.0.2)
|
||||||
activesupport (>= 4.2.0, < 6.0)
|
activesupport (>= 4.2.0, < 6.0)
|
||||||
nokogiri (~> 1.6)
|
nokogiri (~> 1.6)
|
||||||
rails-html-sanitizer (1.0.3)
|
rails-html-sanitizer (1.0.3)
|
||||||
loofah (~> 2.0)
|
loofah (~> 2.0)
|
||||||
|
rails-i18n (5.0.3)
|
||||||
|
i18n (~> 0.7)
|
||||||
|
railties (~> 5.0)
|
||||||
rails-settings-cached (0.6.5)
|
rails-settings-cached (0.6.5)
|
||||||
rails (>= 4.2.0)
|
rails (>= 4.2.0)
|
||||||
rails_12factor (0.0.3)
|
rails_12factor (0.0.3)
|
||||||
@@ -301,50 +324,43 @@ GEM
|
|||||||
rails_stdout_logging
|
rails_stdout_logging
|
||||||
rails_serve_static_assets (0.0.5)
|
rails_serve_static_assets (0.0.5)
|
||||||
rails_stdout_logging (0.0.5)
|
rails_stdout_logging (0.0.5)
|
||||||
railties (5.0.1)
|
railties (5.0.2)
|
||||||
actionpack (= 5.0.1)
|
actionpack (= 5.0.2)
|
||||||
activesupport (= 5.0.1)
|
activesupport (= 5.0.2)
|
||||||
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.1.0)
|
rainbow (2.2.1)
|
||||||
rake (12.0.0)
|
rake (12.0.0)
|
||||||
rdoc (4.2.2)
|
react-rails (1.11.0)
|
||||||
json (~> 1.4)
|
|
||||||
react-rails (1.8.2)
|
|
||||||
babel-transpiler (>= 0.7.0)
|
babel-transpiler (>= 0.7.0)
|
||||||
coffee-script-source (~> 1.8)
|
|
||||||
connection_pool
|
connection_pool
|
||||||
execjs
|
execjs
|
||||||
railties (>= 3.2)
|
railties (>= 3.2)
|
||||||
tilt
|
tilt
|
||||||
redis (3.3.2)
|
redis (3.3.3)
|
||||||
redis-actionpack (5.0.0)
|
redis-actionpack (5.0.1)
|
||||||
actionpack (>= 4.0.0, < 6)
|
actionpack (>= 4.0, < 6)
|
||||||
redis-rack (~> 2.0.0.pre)
|
redis-rack (>= 1, < 3)
|
||||||
redis-store (~> 1.2.0.pre)
|
redis-store (>= 1.1.0, < 1.4.0)
|
||||||
redis-activesupport (5.0.1)
|
redis-activesupport (5.0.2)
|
||||||
activesupport (>= 3, < 6)
|
activesupport (>= 3, < 6)
|
||||||
redis-store (~> 1.2.0)
|
redis-store (~> 1.3.0)
|
||||||
redis-rack (2.0.0)
|
redis-rack (2.0.1)
|
||||||
rack (~> 2.0)
|
rack (>= 2.0, < 3)
|
||||||
redis-store (~> 1.2.0)
|
redis-store (>= 1.2, < 1.4)
|
||||||
redis-rails (5.0.1)
|
redis-rails (5.0.2)
|
||||||
redis-actionpack (~> 5.0.0)
|
redis-actionpack (>= 5.0, < 6)
|
||||||
redis-activesupport (~> 5.0.0)
|
redis-activesupport (>= 5.0, < 6)
|
||||||
redis-store (~> 1.2.0)
|
redis-store (>= 1.2, < 2)
|
||||||
redis-store (1.2.0)
|
redis-store (1.3.0)
|
||||||
redis (>= 2.2)
|
redis (>= 2.2)
|
||||||
responders (2.3.0)
|
responders (2.3.0)
|
||||||
railties (>= 4.2.0, < 5.1)
|
railties (>= 4.2.0, < 5.1)
|
||||||
rotp (2.1.2)
|
rotp (2.1.2)
|
||||||
rqrcode (0.10.1)
|
rqrcode (0.10.1)
|
||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
rspec (3.5.0)
|
rspec-core (3.5.4)
|
||||||
rspec-core (~> 3.5.0)
|
|
||||||
rspec-expectations (~> 3.5.0)
|
|
||||||
rspec-mocks (~> 3.5.0)
|
|
||||||
rspec-core (3.5.2)
|
|
||||||
rspec-support (~> 3.5.0)
|
rspec-support (~> 3.5.0)
|
||||||
rspec-expectations (3.5.0)
|
rspec-expectations (3.5.0)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
@@ -352,7 +368,7 @@ GEM
|
|||||||
rspec-mocks (3.5.0)
|
rspec-mocks (3.5.0)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.5.0)
|
rspec-support (~> 3.5.0)
|
||||||
rspec-rails (3.5.1)
|
rspec-rails (3.5.2)
|
||||||
actionpack (>= 3.0)
|
actionpack (>= 3.0)
|
||||||
activesupport (>= 3.0)
|
activesupport (>= 3.0)
|
||||||
railties (>= 3.0)
|
railties (>= 3.0)
|
||||||
@@ -360,40 +376,40 @@ GEM
|
|||||||
rspec-expectations (~> 3.5.0)
|
rspec-expectations (~> 3.5.0)
|
||||||
rspec-mocks (~> 3.5.0)
|
rspec-mocks (~> 3.5.0)
|
||||||
rspec-support (~> 3.5.0)
|
rspec-support (~> 3.5.0)
|
||||||
rspec-sidekiq (2.2.0)
|
rspec-sidekiq (3.0.0)
|
||||||
rspec (~> 3.0, >= 3.0.0)
|
rspec-core (~> 3.0, >= 3.0.0)
|
||||||
sidekiq (>= 2.4.0)
|
sidekiq (>= 2.4.0)
|
||||||
rspec-support (3.5.0)
|
rspec-support (3.5.0)
|
||||||
rubocop (0.42.0)
|
rubocop (0.48.1)
|
||||||
parser (>= 2.3.1.1, < 3.0)
|
parser (>= 2.3.3.1, < 3.0)
|
||||||
powerpack (~> 0.1)
|
powerpack (~> 0.1)
|
||||||
rainbow (>= 1.99.1, < 3.0)
|
rainbow (>= 1.99.1, < 3.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (~> 1.0, >= 1.0.1)
|
unicode-display_width (~> 1.0, >= 1.0.1)
|
||||||
ruby-oembed (0.10.1)
|
ruby-oembed (0.12.0)
|
||||||
ruby-progressbar (1.8.1)
|
ruby-progressbar (1.8.1)
|
||||||
safe_yaml (1.0.4)
|
safe_yaml (1.0.4)
|
||||||
sass (3.4.22)
|
sass (3.4.23)
|
||||||
sass-rails (5.0.6)
|
sass-rails (5.0.6)
|
||||||
railties (>= 4.0.0, < 6)
|
railties (>= 4.0.0, < 6)
|
||||||
sass (~> 3.1)
|
sass (~> 3.1)
|
||||||
sprockets (>= 2.8, < 4.0)
|
sprockets (>= 2.8, < 4.0)
|
||||||
sprockets-rails (>= 2.0, < 4.0)
|
sprockets-rails (>= 2.0, < 4.0)
|
||||||
tilt (>= 1.1, < 3)
|
tilt (>= 1.1, < 3)
|
||||||
sdoc (0.4.1)
|
sidekiq (4.2.10)
|
||||||
json (~> 1.7, >= 1.7.7)
|
|
||||||
rdoc (~> 4.0)
|
|
||||||
sidekiq (4.2.7)
|
|
||||||
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)
|
||||||
redis (~> 3.2, >= 3.2.1)
|
redis (~> 3.2, >= 3.2.1)
|
||||||
simple-navigation (4.0.3)
|
sidekiq-unique-jobs (5.0.0)
|
||||||
|
sidekiq (>= 4.0)
|
||||||
|
thor
|
||||||
|
simple-navigation (4.0.5)
|
||||||
activesupport (>= 2.3.2)
|
activesupport (>= 2.3.2)
|
||||||
simple_form (3.2.1)
|
simple_form (3.4.0)
|
||||||
actionpack (> 4, < 5.1)
|
actionpack (> 4, < 5.1)
|
||||||
activemodel (> 4, < 5.1)
|
activemodel (> 4, < 5.1)
|
||||||
simplecov (0.12.0)
|
simplecov (0.14.1)
|
||||||
docile (~> 1.1.0)
|
docile (~> 1.1.0)
|
||||||
json (>= 1.8, < 3)
|
json (>= 1.8, < 3)
|
||||||
simplecov-html (~> 0.10.0)
|
simplecov-html (~> 0.10.0)
|
||||||
@@ -406,39 +422,42 @@ GEM
|
|||||||
actionpack (>= 4.0)
|
actionpack (>= 4.0)
|
||||||
activesupport (>= 4.0)
|
activesupport (>= 4.0)
|
||||||
sprockets (>= 3.0.0)
|
sprockets (>= 3.0.0)
|
||||||
sshkit (1.11.5)
|
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.2)
|
||||||
temple (0.7.7)
|
temple (0.8.0)
|
||||||
term-ansicolor (1.4.0)
|
terminal-table (1.7.3)
|
||||||
tins (~> 1.0)
|
unicode-display_width (~> 1.1.1)
|
||||||
terminal-table (1.7.0)
|
|
||||||
unicode-display_width (~> 1.1)
|
|
||||||
thor (0.19.4)
|
thor (0.19.4)
|
||||||
thread (0.2.2)
|
thread (0.2.2)
|
||||||
thread_safe (0.3.5)
|
thread_safe (0.3.6)
|
||||||
tilt (2.0.5)
|
tilt (2.0.7)
|
||||||
tins (1.12.0)
|
twitter-text (1.14.5)
|
||||||
tzinfo (1.2.2)
|
unf (~> 0.1.0)
|
||||||
|
tzinfo (1.2.3)
|
||||||
thread_safe (~> 0.1)
|
thread_safe (~> 0.1)
|
||||||
uglifier (3.0.1)
|
tzinfo-data (1.2017.2)
|
||||||
|
tzinfo (>= 1.0.0)
|
||||||
|
uglifier (3.2.0)
|
||||||
execjs (>= 0.3.0, < 3)
|
execjs (>= 0.3.0, < 3)
|
||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.7.2)
|
unf_ext (0.0.7.3)
|
||||||
unicode-display_width (1.1.0)
|
unicode-display_width (1.1.3)
|
||||||
uniform_notifier (1.10.0)
|
uniform_notifier (1.10.0)
|
||||||
warden (1.2.6)
|
warden (1.2.7)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
webmock (2.1.0)
|
webmock (2.3.2)
|
||||||
addressable (>= 2.3.6)
|
addressable (>= 2.3.6)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff
|
hashdiff
|
||||||
websocket-driver (0.6.4)
|
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)
|
||||||
will_paginate (3.1.0)
|
whatlanguage (1.0.6)
|
||||||
|
xpath (2.0.0)
|
||||||
|
nokogiri (~> 1.3)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
@@ -453,16 +472,18 @@ DEPENDENCIES
|
|||||||
binding_of_caller
|
binding_of_caller
|
||||||
browserify-rails
|
browserify-rails
|
||||||
bullet
|
bullet
|
||||||
capistrano
|
capistrano (= 3.8.0)
|
||||||
|
capistrano-faster-assets (~> 1.0)
|
||||||
capistrano-rails
|
capistrano-rails
|
||||||
capistrano-rbenv
|
capistrano-rbenv
|
||||||
capistrano-yarn
|
capistrano-yarn
|
||||||
coffee-rails (~> 4.1.0)
|
capybara
|
||||||
devise
|
devise
|
||||||
devise-two-factor
|
devise-two-factor
|
||||||
doorkeeper
|
doorkeeper
|
||||||
dotenv-rails
|
dotenv-rails
|
||||||
fabrication
|
fabrication
|
||||||
|
faker
|
||||||
fast_blank
|
fast_blank
|
||||||
font-awesome-rails
|
font-awesome-rails
|
||||||
fuubar
|
fuubar
|
||||||
@@ -471,29 +492,34 @@ DEPENDENCIES
|
|||||||
hiredis
|
hiredis
|
||||||
htmlentities
|
htmlentities
|
||||||
http
|
http
|
||||||
|
http_accept_language
|
||||||
httplog
|
httplog
|
||||||
i18n-tasks (~> 0.9.6)
|
i18n-tasks (~> 0.9.6)
|
||||||
jbuilder (~> 2.0)
|
|
||||||
jquery-rails
|
jquery-rails
|
||||||
|
kaminari
|
||||||
letter_opener
|
letter_opener
|
||||||
letter_opener_web
|
letter_opener_web
|
||||||
link_header
|
link_header
|
||||||
lograge
|
lograge
|
||||||
|
microformats2
|
||||||
nokogiri
|
nokogiri
|
||||||
oj
|
oj
|
||||||
ostatus2
|
ostatus2 (~> 1.1)
|
||||||
|
ox
|
||||||
paperclip (~> 5.1)
|
paperclip (~> 5.1)
|
||||||
paperclip-av-transcoder
|
paperclip-av-transcoder
|
||||||
pg
|
pg
|
||||||
pg_search
|
|
||||||
pghero
|
pghero
|
||||||
|
pkg-config
|
||||||
pry-rails
|
pry-rails
|
||||||
puma
|
puma
|
||||||
rabl
|
rabl
|
||||||
rack-attack
|
rack-attack
|
||||||
rack-cors
|
rack-cors
|
||||||
rack-timeout
|
rack-timeout
|
||||||
rails (~> 5.0.1.0)
|
rails (~> 5.0.2)
|
||||||
|
rails-controller-testing
|
||||||
|
rails-i18n
|
||||||
rails-settings-cached
|
rails-settings-cached
|
||||||
rails_12factor
|
rails_12factor
|
||||||
react-rails
|
react-rails
|
||||||
@@ -505,18 +531,21 @@ DEPENDENCIES
|
|||||||
rubocop
|
rubocop
|
||||||
ruby-oembed
|
ruby-oembed
|
||||||
sass-rails (~> 5.0)
|
sass-rails (~> 5.0)
|
||||||
sdoc (~> 0.4.0)
|
|
||||||
sidekiq
|
sidekiq
|
||||||
|
sidekiq-unique-jobs
|
||||||
simple-navigation
|
simple-navigation
|
||||||
simple_form
|
simple_form
|
||||||
simplecov
|
simplecov
|
||||||
|
sprockets-rails
|
||||||
statsd-instrument
|
statsd-instrument
|
||||||
|
twitter-text
|
||||||
|
tzinfo-data
|
||||||
uglifier (>= 1.3.0)
|
uglifier (>= 1.3.0)
|
||||||
webmock
|
webmock
|
||||||
will_paginate
|
whatlanguage
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 2.3.1p112
|
ruby 2.4.1p111
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
1.14.3
|
1.14.6
|
||||||
|
|||||||
6
ISSUE_TEMPLATE.md
Normal file
6
ISSUE_TEMPLATE.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[Issue text goes here].
|
||||||
|
|
||||||
|
* * * *
|
||||||
|
|
||||||
|
- [ ] I searched or browsed the repo’s other issues to ensure this is not a duplicate.
|
||||||
|
- [ ] This bug happens on a [tagged release](https://github.com/tootsuite/mastodon/releases) and not on `master` (If you're a user, don't worry about this).
|
||||||
2
Procfile
2
Procfile
@@ -1,2 +1,2 @@
|
|||||||
web: bundle exec puma -C config/puma.rb
|
web: bundle exec puma -C config/puma.rb
|
||||||
worker: bundle exec sidekiq -q default -q mailers -q push
|
worker: bundle exec sidekiq -q default -q push -q pull -q mailers
|
||||||
|
|||||||
94
README.md
94
README.md
@@ -7,7 +7,7 @@ Mastodon
|
|||||||
[travis]: https://travis-ci.org/tootsuite/mastodon
|
[travis]: https://travis-ci.org/tootsuite/mastodon
|
||||||
[code_climate]: https://codeclimate.com/github/tootsuite/mastodon
|
[code_climate]: https://codeclimate.com/github/tootsuite/mastodon
|
||||||
|
|
||||||
Mastodon is a free, open-source social network server. A decentralized alternative to commercial platforms, it avoids the risks of a single company monopolizing your communication. Anyone can run Mastodon and participate in the social network seamlessly.
|
Mastodon is a free, open-source social network server. A decentralized solution to commercial platforms, it avoids the risks of a single company monopolizing your communication. Anyone can run Mastodon and participate in the social network seamlessly.
|
||||||
|
|
||||||
An alternative implementation of the GNU social project. Based on ActivityStreams, Webfinger, PubsubHubbub and Salmon.
|
An alternative implementation of the GNU social project. Based on ActivityStreams, Webfinger, PubsubHubbub and Salmon.
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ Click on the screenshot to watch a demo of the UI:
|
|||||||
|
|
||||||
[youtube_demo]: https://www.youtube.com/watch?v=YO1jQ8_rAMU
|
[youtube_demo]: https://www.youtube.com/watch?v=YO1jQ8_rAMU
|
||||||
|
|
||||||
Focus of the project on a clean REST API and a good user interface. Ruby on Rails is used for the back-end, while React.js and Redux are used for the dynamic front-end. A static front-end for public resources (profiles and statuses) is also provided.
|
The project focus is a clean REST API and a good user interface. Ruby on Rails is used for the back-end, while React.js and Redux are used for the dynamic front-end. A static front-end for public resources (profiles and statuses) is also provided.
|
||||||
|
|
||||||
If you would like, you can [support the development of this project on Patreon][patreon]. Alternatively, you can donate to this BTC address: `17j2g7vpgHhLuXhN4bueZFCvdxxieyRVWd`
|
If you would like, you can [support the development of this project on Patreon][patreon]. Alternatively, you can donate to this BTC address: `17j2g7vpgHhLuXhN4bueZFCvdxxieyRVWd`
|
||||||
|
|
||||||
@@ -25,11 +25,11 @@ If you would like, you can [support the development of this project on Patreon][
|
|||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
- [List of Mastodon instances](docs/Using-Mastodon/List-of-Mastodon-instances.md)
|
- [List of Mastodon instances](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/List-of-Mastodon-instances.md)
|
||||||
- [Use this tool to find Twitter friends on Mastodon](https://mastodon-bridge.herokuapp.com)
|
- [Use this tool to find Twitter friends on Mastodon](https://mastodon-bridge.herokuapp.com)
|
||||||
- [API overview](docs/Using-the-API/API.md)
|
- [API overview](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md)
|
||||||
- [Frequently Asked Questions](docs/Using-Mastodon/FAQ.md)
|
- [Frequently Asked Questions](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md)
|
||||||
- [List of apps](docs/Using-Mastodon/Apps.md)
|
- [List of apps](https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/Apps.md)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -48,6 +48,14 @@ If you would like, you can [support the development of this project on Patreon][
|
|||||||
- **Deployable via Docker**
|
- **Deployable via Docker**
|
||||||
You don't need to mess with dependencies and configuration if you want to try Mastodon, if you have Docker and Docker Compose the deployment is extremely easy
|
You don't need to mess with dependencies and configuration if you want to try Mastodon, if you have Docker and Docker Compose the deployment is extremely easy
|
||||||
|
|
||||||
|
## Checking out
|
||||||
|
|
||||||
|
If you want a stable release for production use, you should use tagged releases. To checkout the latest available tagged version:
|
||||||
|
|
||||||
|
git clone https://github.com/tootsuite/mastodon.git
|
||||||
|
cd mastodon
|
||||||
|
git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
- `LOCAL_DOMAIN` should be the domain/hostname of your instance. This is **absolutely required** as it is used for generating unique IDs for everything federation-related
|
- `LOCAL_DOMAIN` should be the domain/hostname of your instance. This is **absolutely required** as it is used for generating unique IDs for everything federation-related
|
||||||
@@ -65,23 +73,55 @@ Consult the example configuration file, `.env.production.sample` for the full li
|
|||||||
|
|
||||||
## Running with Docker and Docker-Compose
|
## Running with Docker and Docker-Compose
|
||||||
|
|
||||||
The project now includes a `Dockerfile` and a `docker-compose.yml`. You need to turn `.env.production.sample` into `.env.production` with all the variables set before you can:
|
[](https://microbadger.com/images/gargron/mastodon "Get your own version badge on microbadger.com") [](https://microbadger.com/images/gargron/mastodon "Get your own image badge on microbadger.com")
|
||||||
|
|
||||||
|
The project now includes a `Dockerfile` and a `docker-compose.yml` file (which requires at least docker-compose version `1.10.0`).
|
||||||
|
|
||||||
|
Review the settings in `docker-compose.yml`. Note that it is not default to store the postgresql database and redis databases in a persistent storage location,
|
||||||
|
so you may need or want to adjust the settings there.
|
||||||
|
|
||||||
|
Then, you need to fill in the `.env.production` file:
|
||||||
|
|
||||||
|
cp .env.production.sample .env.production
|
||||||
|
nano .env.production
|
||||||
|
|
||||||
|
Do NOT change the `REDIS_*` or `DB_*` settings when running with the default docker configurations.
|
||||||
|
|
||||||
|
You will need to fill in, at least: `LOCAL_DOMAIN`, `LOCAL_HTTPS`, `PAPERCLIP_SECRET`, `SECRET_KEY_BASE`, `OTP_SECRET`, and the `SMTP_*` settings. To generate the `PAPERCLIP_SECRET`, `SECRET_KEY_BASE`, and `OTP_SECRET`, you may use:
|
||||||
|
|
||||||
|
Before running the first time, you need to build the images:
|
||||||
|
|
||||||
docker-compose build
|
docker-compose build
|
||||||
|
|
||||||
And finally
|
|
||||||
|
|
||||||
docker-compose up -d
|
docker-compose run --rm web rake secret
|
||||||
|
|
||||||
As usual, the first thing you would need to do would be to run migrations:
|
Do this once for each of those keys, and copy the result into the `.env.production` file in the appropriate field.
|
||||||
|
|
||||||
|
Then you should run the `db:migrate` command to create the database, or migrate it from an older release:
|
||||||
|
|
||||||
docker-compose run --rm web rails db:migrate
|
docker-compose run --rm web rails db:migrate
|
||||||
|
|
||||||
And since the instance running in the container will be running in production mode, you need to pre-compile assets:
|
Then, you will also need to precompile the assets:
|
||||||
|
|
||||||
docker-compose run --rm web rails assets:precompile
|
docker-compose run --rm web rails assets:precompile
|
||||||
|
|
||||||
The container has two volumes, for the assets and for user uploads. The default docker-compose.yml maps them to the repository's `public/assets` and `public/system` directories, you may wish to put them somewhere else. Likewise, the PostgreSQL and Redis images have data containers that you may wish to map somewhere where you know how to find them and back them up.
|
before you can launch the docker image with:
|
||||||
|
|
||||||
|
docker-compose up
|
||||||
|
|
||||||
|
If you wish to run this as a daemon process instead of monitoring it on console, use instead:
|
||||||
|
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
Then you may login to your new Mastodon instance by browsing to http://localhost:3000/
|
||||||
|
|
||||||
|
Following that, make sure that you read the [production guide](docs/Running-Mastodon/Production-guide.md). You are probably going to want to understand how
|
||||||
|
to configure Nginx to make your Mastodon instance available to the rest of the world.
|
||||||
|
|
||||||
|
The container has two volumes, for the assets and for user uploads, and optionally two more, for the postgresql and redis databases.
|
||||||
|
|
||||||
|
The default docker-compose.yml maps them to the repository's `public/assets` and `public/system` directories, you may wish to put them somewhere else. Likewise, the PostgreSQL and Redis images have data containers that you may wish to map somewhere where you know how to find them and back them up.
|
||||||
|
|
||||||
**Note**: The `--rm` option for docker-compose will remove the container that is created to run a one-off command after it completes. As data is stored in volumes it is not affected by that container clean-up.
|
**Note**: The `--rm` option for docker-compose will remove the container that is created to run a one-off command after it completes. As data is stored in volumes it is not affected by that container clean-up.
|
||||||
|
|
||||||
@@ -101,37 +141,37 @@ Running any of these tasks via docker-compose would look like this:
|
|||||||
|
|
||||||
This approach makes updating to the latest version a real breeze.
|
This approach makes updating to the latest version a real breeze.
|
||||||
|
|
||||||
git pull
|
1. `git pull` to download updates from the repository
|
||||||
|
2. `docker-compose build` to compile the Docker image out of the changed source files
|
||||||
To pull down the updates, re-run
|
3. (optional) `docker-compose run --rm web rails db:migrate` to perform database migrations. Does nothing if your database is up to date
|
||||||
|
4. (optional) `docker-compose run --rm web rails assets:precompile` to compile new JS and CSS assets
|
||||||
docker-compose build
|
5. `docker-compose up -d` to re-create (restart) containers and pick up the changes
|
||||||
|
|
||||||
And finally,
|
|
||||||
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
Which will re-create the updated containers, leaving databases and data as is. Depending on what files have been updated, you might need to re-run migrations and asset compilation.
|
|
||||||
|
|
||||||
## Deployment without Docker
|
## Deployment without Docker
|
||||||
|
|
||||||
Docker is great for quickly trying out software, but it has its drawbacks too. If you prefer to run Mastodon without using Docker, refer to the [production guide](docs/Running-Mastodon/Production-guide.md) for examples, configuration and instructions.
|
Docker is great for quickly trying out software, but it has its drawbacks too. If you prefer to run Mastodon without using Docker, refer to the [production guide](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Production-guide.md) for examples, configuration and instructions.
|
||||||
|
|
||||||
|
## Deployment on Scalingo
|
||||||
|
|
||||||
|
[](https://my.scalingo.com/deploy?source=https://github.com/tootsuite/mastodon#master)
|
||||||
|
|
||||||
|
[You can view a guide for deployment on Scalingo here.](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Scalingo-guide.md)
|
||||||
|
|
||||||
## Deployment on Heroku (experimental)
|
## Deployment on Heroku (experimental)
|
||||||
|
|
||||||
[](https://heroku.com/deploy)
|
[](https://heroku.com/deploy)
|
||||||
|
|
||||||
Mastodon can theoretically run indefinitely on a free [Heroku](https://heroku.com) app. [You can view a guide for deployment on Heroku here.](docs/Running-Mastodon/Heroku-guide.md)
|
Mastodon can run on [Heroku](https://heroku.com), but it gets expensive and impractical due to how Heroku prices resource usage. [You can view a guide for deployment on Heroku here](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Heroku-guide.md), but you have been warned.
|
||||||
|
|
||||||
## Development with Vagrant
|
## Development with Vagrant
|
||||||
|
|
||||||
A quick way to get a development environment up and running is with Vagrant. You will need recent versions of [Vagrant](https://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/) installed.
|
A quick way to get a development environment up and running is with Vagrant. You will need recent versions of [Vagrant](https://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/) installed.
|
||||||
|
|
||||||
[You can find the guide for setting up a Vagrant development environment here.](docs/Running-Mastodon/Vagrant-guide.md)
|
[You can find the guide for setting up a Vagrant development environment here.](https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Vagrant-guide.md)
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository. This section may be updated with more details in the future.
|
You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository. [Here are the guidelines for code contributions](CONTRIBUTING.md)
|
||||||
|
|
||||||
**IRC channel**: #mastodon on irc.freenode.net
|
**IRC channel**: #mastodon on irc.freenode.net
|
||||||
|
|
||||||
|
|||||||
26
Vagrantfile
vendored
26
Vagrantfile
vendored
@@ -43,15 +43,15 @@ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
|
|||||||
|
|
||||||
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
|
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
|
||||||
|
|
||||||
export PATH="$HOME/.rbenv/bin::$PATH"
|
export PATH="$HOME/.rbenv/bin:$PATH"
|
||||||
eval "$(rbenv init -)"
|
eval "$(rbenv init -)"
|
||||||
|
|
||||||
echo "Compiling Ruby 2.3.1: warning, this takes a while!!!"
|
|
||||||
rbenv install 2.3.1
|
|
||||||
rbenv global 2.3.1
|
|
||||||
|
|
||||||
cd /vagrant
|
cd /vagrant
|
||||||
|
|
||||||
|
echo "Compiling Ruby $(cat .ruby-version): warning, this takes a while!!!"
|
||||||
|
rbenv install $(cat .ruby-version)
|
||||||
|
rbenv global $(cat .ruby-version)
|
||||||
|
|
||||||
# Configure database
|
# Configure database
|
||||||
sudo -u postgres createuser -U postgres vagrant -s
|
sudo -u postgres createuser -U postgres vagrant -s
|
||||||
sudo -u postgres createdb -U postgres mastodon_development
|
sudo -u postgres createdb -U postgres mastodon_development
|
||||||
@@ -84,6 +84,16 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
|||||||
config.vm.provider :virtualbox do |vb|
|
config.vm.provider :virtualbox do |vb|
|
||||||
vb.name = "mastodon"
|
vb.name = "mastodon"
|
||||||
vb.customize ["modifyvm", :id, "--memory", "1024"]
|
vb.customize ["modifyvm", :id, "--memory", "1024"]
|
||||||
|
|
||||||
|
# Disable VirtualBox DNS proxy to skip long-delay IPv6 resolutions.
|
||||||
|
# https://github.com/mitchellh/vagrant/issues/1172
|
||||||
|
vb.customize ["modifyvm", :id, "--natdnsproxy1", "off"]
|
||||||
|
vb.customize ["modifyvm", :id, "--natdnshostresolver1", "off"]
|
||||||
|
|
||||||
|
# Use "virtio" network interfaces for better performance.
|
||||||
|
vb.customize ["modifyvm", :id, "--nictype1", "virtio"]
|
||||||
|
vb.customize ["modifyvm", :id, "--nictype2", "virtio"]
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
config.vm.hostname = "mastodon.dev"
|
config.vm.hostname = "mastodon.dev"
|
||||||
@@ -91,12 +101,14 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
|||||||
# This uses the vagrant-hostsupdater plugin, and lets you
|
# This uses the vagrant-hostsupdater plugin, and lets you
|
||||||
# access the development site at http://mastodon.dev.
|
# access the development site at http://mastodon.dev.
|
||||||
# To install:
|
# To install:
|
||||||
# $ vagrant plugin install hostsupdater
|
# $ vagrant plugin install vagrant-hostsupdater
|
||||||
if defined?(VagrantPlugins::HostsUpdater)
|
if defined?(VagrantPlugins::HostsUpdater)
|
||||||
config.vm.network :private_network, ip: "192.168.42.42"
|
config.vm.network :private_network, ip: "192.168.42.42", nictype: "virtio"
|
||||||
config.hostsupdater.remove_on_suspend = false
|
config.hostsupdater.remove_on_suspend = false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
config.vm.synced_folder ".", "/vagrant", type: "nfs", mount_options: ['rw', 'vers=3', 'tcp']
|
||||||
|
|
||||||
# Otherwise, you can access the site at http://localhost:3000
|
# Otherwise, you can access the site at http://localhost:3000
|
||||||
config.vm.network :forwarded_port, guest: 80, host: 3000
|
config.vm.network :forwarded_port, guest: 80, host: 3000
|
||||||
|
|
||||||
|
|||||||
22
app.json
22
app.json
@@ -26,6 +26,10 @@
|
|||||||
"description": "The secret key base",
|
"description": "The secret key base",
|
||||||
"generator": "secret"
|
"generator": "secret"
|
||||||
},
|
},
|
||||||
|
"OTP_SECRET": {
|
||||||
|
"description": "One-time password secret",
|
||||||
|
"generator": "secret"
|
||||||
|
},
|
||||||
"SINGLE_USER_MODE": {
|
"SINGLE_USER_MODE": {
|
||||||
"description": "Should the instance run in single user mode? (Disable registrations, redirect to front page)",
|
"description": "Should the instance run in single user mode? (Disable registrations, redirect to front page)",
|
||||||
"value": "false",
|
"value": "false",
|
||||||
@@ -71,6 +75,22 @@
|
|||||||
"SMTP_DOMAIN": {
|
"SMTP_DOMAIN": {
|
||||||
"description": "Domain for SMTP server. Will default to instance domain if blank.",
|
"description": "Domain for SMTP server. Will default to instance domain if blank.",
|
||||||
"required": false
|
"required": false
|
||||||
|
},
|
||||||
|
"SMTP_FROM_ADDRESS": {
|
||||||
|
"description": "Address to send emails from",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"SMTP_AUTH_METHOD": {
|
||||||
|
"description": "Authentication method to use with SMTP server. Default is 'plain'.",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"SMTP_OPENSSL_VERIFY_MODE": {
|
||||||
|
"description": "SMTP server certificate verification mode. Defaults is 'peer'.",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"SMTP_ENABLE_STARTTLS_AUTO": {
|
||||||
|
"description": "Enable STARTTLS if SMTP server supports it? Default is true.",
|
||||||
|
"required": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"buildpacks": [
|
"buildpacks": [
|
||||||
@@ -88,4 +108,4 @@
|
|||||||
"heroku-postgresql",
|
"heroku-postgresql",
|
||||||
"heroku-redis"
|
"heroku-redis"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 874 KiB After Width: | Height: | Size: 258 KiB |
BIN
app/assets/images/elephant-friend.png
Normal file
BIN
app/assets/images/elephant-friend.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
app/assets/images/fluffy-elephant-friend.png
Normal file
BIN
app/assets/images/fluffy-elephant-friend.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
1
app/assets/images/logo.svg
Normal file
1
app/assets/images/logo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" height="1000" width="1000"><g fill="#189efc"><path d="M500 0A500 500 0 0 0 0 500a500 500 0 0 0 500 500 500 500 0 0 0 500-500A500 500 0 0 0 500 0zm-2.5 271.1h107.24c-20.56 14.471-27.24 57.064-27.24 78.927v202.145c0 43.726-35.202 78.928-80 78.928s-80-35.202-80-78.928V350.027c0-43.725 35.202-78.927 80-78.927zm-276 48.9c44.798 0 80 35.202 80 78.928v202.144c0 21.863 6.68 64.456 27.24 78.928H221.5c-44.798 0-80-35.202-80-78.928V398.928c0-43.726 35.202-78.928 80-78.928zm550.24 0c44.799 0 80 35.202 80 78.928v202.144c0 43.726-35.201 78.928-80 78.928H664.5c20.56-14.472 27.24-57.065 27.24-78.928V398.928c0-43.726 35.202-78.928 80-78.928z"/><g transform="translate(-2)"><circle cx="223.5" cy="410.5" r="27.5"/><circle cx="223.5" cy="500.5" r="27.5"/><circle cx="223.5" cy="590.5" r="27.5"/></g><g transform="matrix(1 0 0 -1 274 951)"><circle cx="223.5" cy="410.5" r="27.5"/><circle cx="223.5" cy="500.5" r="27.5"/><circle cx="223.5" cy="590.5" r="27.5"/></g><g transform="matrix(-1 0 0 1 995 0)"><circle cx="223.5" cy="410.5" r="27.5"/><circle cx="223.5" cy="500.5" r="27.5"/><circle cx="223.5" cy="590.5" r="27.5"/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
BIN
app/assets/images/mastodon-not-found.png
Normal file
BIN
app/assets/images/mastodon-not-found.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -21,6 +21,14 @@ export const ACCOUNT_UNBLOCK_REQUEST = 'ACCOUNT_UNBLOCK_REQUEST';
|
|||||||
export const ACCOUNT_UNBLOCK_SUCCESS = 'ACCOUNT_UNBLOCK_SUCCESS';
|
export const ACCOUNT_UNBLOCK_SUCCESS = 'ACCOUNT_UNBLOCK_SUCCESS';
|
||||||
export const ACCOUNT_UNBLOCK_FAIL = 'ACCOUNT_UNBLOCK_FAIL';
|
export const ACCOUNT_UNBLOCK_FAIL = 'ACCOUNT_UNBLOCK_FAIL';
|
||||||
|
|
||||||
|
export const ACCOUNT_MUTE_REQUEST = 'ACCOUNT_MUTE_REQUEST';
|
||||||
|
export const ACCOUNT_MUTE_SUCCESS = 'ACCOUNT_MUTE_SUCCESS';
|
||||||
|
export const ACCOUNT_MUTE_FAIL = 'ACCOUNT_MUTE_FAIL';
|
||||||
|
|
||||||
|
export const ACCOUNT_UNMUTE_REQUEST = 'ACCOUNT_UNMUTE_REQUEST';
|
||||||
|
export const ACCOUNT_UNMUTE_SUCCESS = 'ACCOUNT_UNMUTE_SUCCESS';
|
||||||
|
export const ACCOUNT_UNMUTE_FAIL = 'ACCOUNT_UNMUTE_FAIL';
|
||||||
|
|
||||||
export const ACCOUNT_TIMELINE_FETCH_REQUEST = 'ACCOUNT_TIMELINE_FETCH_REQUEST';
|
export const ACCOUNT_TIMELINE_FETCH_REQUEST = 'ACCOUNT_TIMELINE_FETCH_REQUEST';
|
||||||
export const ACCOUNT_TIMELINE_FETCH_SUCCESS = 'ACCOUNT_TIMELINE_FETCH_SUCCESS';
|
export const ACCOUNT_TIMELINE_FETCH_SUCCESS = 'ACCOUNT_TIMELINE_FETCH_SUCCESS';
|
||||||
export const ACCOUNT_TIMELINE_FETCH_FAIL = 'ACCOUNT_TIMELINE_FETCH_FAIL';
|
export const ACCOUNT_TIMELINE_FETCH_FAIL = 'ACCOUNT_TIMELINE_FETCH_FAIL';
|
||||||
@@ -67,11 +75,16 @@ export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
|
|||||||
|
|
||||||
export function fetchAccount(id) {
|
export function fetchAccount(id) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
|
dispatch(fetchRelationships([id]));
|
||||||
|
|
||||||
|
if (getState().getIn(['accounts', id], null) !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(fetchAccountRequest(id));
|
dispatch(fetchAccountRequest(id));
|
||||||
|
|
||||||
api(getState).get(`/api/v1/accounts/${id}`).then(response => {
|
api(getState).get(`/api/v1/accounts/${id}`).then(response => {
|
||||||
dispatch(fetchAccountSuccess(response.data));
|
dispatch(fetchAccountSuccess(response.data));
|
||||||
dispatch(fetchRelationships([id]));
|
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchAccountFail(id, error));
|
dispatch(fetchAccountFail(id, error));
|
||||||
});
|
});
|
||||||
@@ -138,7 +151,8 @@ export function fetchAccountFail(id, error) {
|
|||||||
return {
|
return {
|
||||||
type: ACCOUNT_FETCH_FAIL,
|
type: ACCOUNT_FETCH_FAIL,
|
||||||
id,
|
id,
|
||||||
error
|
error,
|
||||||
|
skipAlert: true
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -231,7 +245,8 @@ export function fetchAccountTimelineFail(id, error, skipLoading) {
|
|||||||
type: ACCOUNT_TIMELINE_FETCH_FAIL,
|
type: ACCOUNT_TIMELINE_FETCH_FAIL,
|
||||||
id,
|
id,
|
||||||
error,
|
error,
|
||||||
skipLoading
|
skipLoading,
|
||||||
|
skipAlert: error.response.status === 404
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -326,6 +341,76 @@ export function unblockAccountFail(error) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export function muteAccount(id) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch(muteAccountRequest(id));
|
||||||
|
|
||||||
|
api(getState).post(`/api/v1/accounts/${id}/mute`).then(response => {
|
||||||
|
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
|
||||||
|
dispatch(muteAccountSuccess(response.data, getState().get('statuses')));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(muteAccountFail(id, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function unmuteAccount(id) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch(unmuteAccountRequest(id));
|
||||||
|
|
||||||
|
api(getState).post(`/api/v1/accounts/${id}/unmute`).then(response => {
|
||||||
|
dispatch(unmuteAccountSuccess(response.data));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(unmuteAccountFail(id, error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function muteAccountRequest(id) {
|
||||||
|
return {
|
||||||
|
type: ACCOUNT_MUTE_REQUEST,
|
||||||
|
id
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function muteAccountSuccess(relationship, statuses) {
|
||||||
|
return {
|
||||||
|
type: ACCOUNT_MUTE_SUCCESS,
|
||||||
|
relationship,
|
||||||
|
statuses
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function muteAccountFail(error) {
|
||||||
|
return {
|
||||||
|
type: ACCOUNT_MUTE_FAIL,
|
||||||
|
error
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function unmuteAccountRequest(id) {
|
||||||
|
return {
|
||||||
|
type: ACCOUNT_UNMUTE_REQUEST,
|
||||||
|
id
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function unmuteAccountSuccess(relationship) {
|
||||||
|
return {
|
||||||
|
type: ACCOUNT_UNMUTE_SUCCESS,
|
||||||
|
relationship
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function unmuteAccountFail(error) {
|
||||||
|
return {
|
||||||
|
type: ACCOUNT_UNMUTE_FAIL,
|
||||||
|
error
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export function fetchFollowers(id) {
|
export function fetchFollowers(id) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(fetchFollowersRequest(id));
|
dispatch(fetchFollowersRequest(id));
|
||||||
@@ -494,15 +579,18 @@ export function expandFollowingFail(id, error) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function fetchRelationships(account_ids) {
|
export function fetchRelationships(accountIds) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
if (account_ids.length === 0) {
|
const loadedRelationships = getState().get('relationships');
|
||||||
|
const newAccountIds = accountIds.filter(id => loadedRelationships.get(id, null) === null);
|
||||||
|
|
||||||
|
if (newAccountIds.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(fetchRelationshipsRequest(account_ids));
|
dispatch(fetchRelationshipsRequest(newAccountIds));
|
||||||
|
|
||||||
api(getState).get(`/api/v1/accounts/relationships?${account_ids.map(id => `id[]=${id}`).join('&')}`).then(response => {
|
api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
|
||||||
dispatch(fetchRelationshipsSuccess(response.data));
|
dispatch(fetchRelationshipsSuccess(response.data));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchRelationshipsFail(error));
|
dispatch(fetchRelationshipsFail(error));
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ export const STATUS_CARD_FETCH_FAIL = 'STATUS_CARD_FETCH_FAIL';
|
|||||||
|
|
||||||
export function fetchStatusCard(id) {
|
export function fetchStatusCard(id) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
|
if (getState().getIn(['cards', id], null) !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(fetchStatusCardRequest(id));
|
dispatch(fetchStatusCardRequest(id));
|
||||||
|
|
||||||
api(getState).get(`/api/v1/statuses/${id}/card`).then(response => {
|
api(getState).get(`/api/v1/statuses/${id}/card`).then(response => {
|
||||||
@@ -42,6 +46,7 @@ export function fetchStatusCardFail(id, error) {
|
|||||||
type: STATUS_CARD_FETCH_FAIL,
|
type: STATUS_CARD_FETCH_FAIL,
|
||||||
id,
|
id,
|
||||||
error,
|
error,
|
||||||
skipLoading: true
|
skipLoading: true,
|
||||||
|
skipAlert: true
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import api from '../api'
|
import api from '../api';
|
||||||
|
|
||||||
import { updateTimeline } from './timelines';
|
import { updateTimeline } from './timelines';
|
||||||
|
|
||||||
|
import * as emojione from 'emojione';
|
||||||
|
|
||||||
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
|
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
|
||||||
export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
|
export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
|
||||||
export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
|
export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS';
|
||||||
@@ -28,6 +30,8 @@ export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
|
|||||||
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
|
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
|
||||||
export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
|
export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
|
||||||
|
|
||||||
|
export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT';
|
||||||
|
|
||||||
export function changeCompose(text) {
|
export function changeCompose(text) {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_CHANGE,
|
type: COMPOSE_CHANGE,
|
||||||
@@ -70,22 +74,27 @@ export function mentionCompose(account, router) {
|
|||||||
export function submitCompose() {
|
export function submitCompose() {
|
||||||
return function (dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
dispatch(submitComposeRequest());
|
dispatch(submitComposeRequest());
|
||||||
|
|
||||||
api(getState).post('/api/v1/statuses', {
|
api(getState).post('/api/v1/statuses', {
|
||||||
status: getState().getIn(['compose', 'text'], ''),
|
status: emojione.shortnameToUnicode(getState().getIn(['compose', 'text'], '')),
|
||||||
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
|
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
|
||||||
media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')),
|
media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')),
|
||||||
sensitive: getState().getIn(['compose', 'sensitive']),
|
sensitive: getState().getIn(['compose', 'sensitive']),
|
||||||
spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
|
spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
|
||||||
visibility: getState().getIn(['compose', 'private']) ? 'private' : (getState().getIn(['compose', 'unlisted']) ? 'unlisted' : 'public')
|
visibility: getState().getIn(['compose', 'privacy'])
|
||||||
}).then(function (response) {
|
}).then(function (response) {
|
||||||
dispatch(submitComposeSuccess({ ...response.data }));
|
dispatch(submitComposeSuccess({ ...response.data }));
|
||||||
|
|
||||||
// To make the app more responsive, immediately get the status into the columns
|
// To make the app more responsive, immediately get the status into the columns
|
||||||
dispatch(updateTimeline('home', { ...response.data }));
|
dispatch(updateTimeline('home', { ...response.data }));
|
||||||
|
|
||||||
if (response.data.in_reply_to_id === null && !getState().getIn(['compose', 'private']) && !getState().getIn(['compose', 'unlisted'])) {
|
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
|
||||||
dispatch(updateTimeline('public', { ...response.data }));
|
if (getState().getIn(['timelines', 'community', 'loaded'])) {
|
||||||
|
dispatch(updateTimeline('community', { ...response.data }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getState().getIn(['timelines', 'public', 'loaded'])) {
|
||||||
|
dispatch(updateTimeline('public', { ...response.data }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}).catch(function (error) {
|
}).catch(function (error) {
|
||||||
dispatch(submitComposeFail(error));
|
dispatch(submitComposeFail(error));
|
||||||
@@ -115,6 +124,10 @@ export function submitComposeFail(error) {
|
|||||||
|
|
||||||
export function uploadCompose(files) {
|
export function uploadCompose(files) {
|
||||||
return function (dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
|
if (getState().getIn(['compose', 'media_attachments']).size > 3) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(uploadComposeRequest());
|
dispatch(uploadComposeRequest());
|
||||||
|
|
||||||
let data = new FormData();
|
let data = new FormData();
|
||||||
@@ -134,7 +147,8 @@ export function uploadCompose(files) {
|
|||||||
|
|
||||||
export function uploadComposeRequest() {
|
export function uploadComposeRequest() {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_UPLOAD_REQUEST
|
type: COMPOSE_UPLOAD_REQUEST,
|
||||||
|
skipLoading: true
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -149,14 +163,16 @@ export function uploadComposeProgress(loaded, total) {
|
|||||||
export function uploadComposeSuccess(media) {
|
export function uploadComposeSuccess(media) {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_UPLOAD_SUCCESS,
|
type: COMPOSE_UPLOAD_SUCCESS,
|
||||||
media: media
|
media: media,
|
||||||
|
skipLoading: true
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function uploadComposeFail(error) {
|
export function uploadComposeFail(error) {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_UPLOAD_FAIL,
|
type: COMPOSE_UPLOAD_FAIL,
|
||||||
error: error
|
error: error,
|
||||||
|
skipLoading: true
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -220,17 +236,15 @@ export function unmountCompose() {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function changeComposeSensitivity(checked) {
|
export function changeComposeSensitivity() {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_SENSITIVITY_CHANGE,
|
type: COMPOSE_SENSITIVITY_CHANGE,
|
||||||
checked
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function changeComposeSpoilerness(checked) {
|
export function changeComposeSpoilerness() {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_SPOILERNESS_CHANGE,
|
type: COMPOSE_SPOILERNESS_CHANGE
|
||||||
checked
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -241,16 +255,17 @@ export function changeComposeSpoilerText(text) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function changeComposeVisibility(checked) {
|
export function changeComposeVisibility(value) {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_VISIBILITY_CHANGE,
|
type: COMPOSE_VISIBILITY_CHANGE,
|
||||||
checked
|
value
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function changeComposeListability(checked) {
|
export function insertEmojiCompose(position, emoji) {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_LISTABILITY_CHANGE,
|
type: COMPOSE_EMOJI_INSERT,
|
||||||
checked
|
position,
|
||||||
|
emoji
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
export const MEDIA_OPEN = 'MEDIA_OPEN';
|
export const MODAL_OPEN = 'MODAL_OPEN';
|
||||||
export const MODAL_CLOSE = 'MODAL_CLOSE';
|
export const MODAL_CLOSE = 'MODAL_CLOSE';
|
||||||
|
|
||||||
export const MODAL_INDEX_DECREASE = 'MODAL_INDEX_DECREASE';
|
export function openModal(type, props) {
|
||||||
export const MODAL_INDEX_INCREASE = 'MODAL_INDEX_INCREASE';
|
|
||||||
|
|
||||||
export function openMedia(media, index) {
|
|
||||||
return {
|
return {
|
||||||
type: MEDIA_OPEN,
|
type: MODAL_OPEN,
|
||||||
media,
|
modalType: type,
|
||||||
index
|
modalProps: props
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -17,15 +14,3 @@ export function closeModal() {
|
|||||||
type: MODAL_CLOSE
|
type: MODAL_CLOSE
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function decreaseIndexInModal() {
|
|
||||||
return {
|
|
||||||
type: MODAL_INDEX_DECREASE
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function increaseIndexInModal() {
|
|
||||||
return {
|
|
||||||
type: MODAL_INDEX_INCREASE
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
82
app/assets/javascripts/components/actions/mutes.jsx
Normal file
82
app/assets/javascripts/components/actions/mutes.jsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import api, { getLinks } from '../api'
|
||||||
|
import { fetchRelationships } from './accounts';
|
||||||
|
|
||||||
|
export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
|
||||||
|
export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
|
||||||
|
export const MUTES_FETCH_FAIL = 'MUTES_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST';
|
||||||
|
export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
|
||||||
|
export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
|
||||||
|
|
||||||
|
export function fetchMutes() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch(fetchMutesRequest());
|
||||||
|
|
||||||
|
api(getState).get('/api/v1/mutes').then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(fetchMutesSuccess(response.data, next ? next.uri : null));
|
||||||
|
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||||
|
}).catch(error => dispatch(fetchMutesFail(error)));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchMutesRequest() {
|
||||||
|
return {
|
||||||
|
type: MUTES_FETCH_REQUEST
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchMutesSuccess(accounts, next) {
|
||||||
|
return {
|
||||||
|
type: MUTES_FETCH_SUCCESS,
|
||||||
|
accounts,
|
||||||
|
next
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchMutesFail(error) {
|
||||||
|
return {
|
||||||
|
type: MUTES_FETCH_FAIL,
|
||||||
|
error
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function expandMutes() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const url = getState().getIn(['user_lists', 'mutes', 'next']);
|
||||||
|
|
||||||
|
if (url === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(expandMutesRequest());
|
||||||
|
|
||||||
|
api(getState).get(url).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(expandMutesSuccess(response.data, next ? next.uri : null));
|
||||||
|
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||||
|
}).catch(error => dispatch(expandMutesFail(error)));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function expandMutesRequest() {
|
||||||
|
return {
|
||||||
|
type: MUTES_EXPAND_REQUEST
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function expandMutesSuccess(accounts, next) {
|
||||||
|
return {
|
||||||
|
type: MUTES_EXPAND_SUCCESS,
|
||||||
|
accounts,
|
||||||
|
next
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function expandMutesFail(error) {
|
||||||
|
return {
|
||||||
|
type: MUTES_EXPAND_FAIL,
|
||||||
|
error
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -14,6 +14,9 @@ export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
|
|||||||
export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
|
export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
|
||||||
export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL';
|
export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL';
|
||||||
|
|
||||||
|
export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
|
||||||
|
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
|
||||||
|
|
||||||
const fetchRelatedRelationships = (dispatch, notifications) => {
|
const fetchRelatedRelationships = (dispatch, notifications) => {
|
||||||
const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.account.id);
|
const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.account.id);
|
||||||
|
|
||||||
@@ -47,6 +50,8 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
|
||||||
|
|
||||||
export function refreshNotifications() {
|
export function refreshNotifications() {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(refreshNotificationsRequest());
|
dispatch(refreshNotificationsRequest());
|
||||||
@@ -58,6 +63,8 @@ export function refreshNotifications() {
|
|||||||
params.since_id = ids.first().get('id');
|
params.since_id = ids.first().get('id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
params.exclude_types = excludeTypesFromSettings(getState());
|
||||||
|
|
||||||
api(getState).get('/api/v1/notifications', { params }).then(response => {
|
api(getState).get('/api/v1/notifications', { params }).then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
@@ -102,11 +109,11 @@ export function expandNotifications() {
|
|||||||
|
|
||||||
dispatch(expandNotificationsRequest());
|
dispatch(expandNotificationsRequest());
|
||||||
|
|
||||||
api(getState).get(url, {
|
const params = {};
|
||||||
params: {
|
|
||||||
limit: 5
|
params.exclude_types = excludeTypesFromSettings(getState());
|
||||||
}
|
|
||||||
}).then(response => {
|
api(getState).get(url, params).then(response => {
|
||||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
|
||||||
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null));
|
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null));
|
||||||
@@ -139,3 +146,20 @@ export function expandNotificationsFail(error) {
|
|||||||
error
|
error
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function clearNotifications() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({
|
||||||
|
type: NOTIFICATIONS_CLEAR
|
||||||
|
});
|
||||||
|
|
||||||
|
api(getState).post('/api/v1/notifications/clear');
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function scrollTopNotifications(top) {
|
||||||
|
return {
|
||||||
|
type: NOTIFICATIONS_SCROLL_TOP,
|
||||||
|
top
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
14
app/assets/javascripts/components/actions/onboarding.jsx
Normal file
14
app/assets/javascripts/components/actions/onboarding.jsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { openModal } from './modal';
|
||||||
|
import { changeSetting, saveSettings } from './settings';
|
||||||
|
|
||||||
|
export function showOnboardingOnce() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const alreadySeen = getState().getIn(['settings', 'onboarded']);
|
||||||
|
|
||||||
|
if (!alreadySeen) {
|
||||||
|
dispatch(openModal('ONBOARDING'));
|
||||||
|
dispatch(changeSetting(['onboarded'], true));
|
||||||
|
dispatch(saveSettings());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
72
app/assets/javascripts/components/actions/reports.jsx
Normal file
72
app/assets/javascripts/components/actions/reports.jsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import api from '../api';
|
||||||
|
|
||||||
|
export const REPORT_INIT = 'REPORT_INIT';
|
||||||
|
export const REPORT_CANCEL = 'REPORT_CANCEL';
|
||||||
|
|
||||||
|
export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST';
|
||||||
|
export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS';
|
||||||
|
export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL';
|
||||||
|
|
||||||
|
export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE';
|
||||||
|
export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE';
|
||||||
|
|
||||||
|
export function initReport(account, status) {
|
||||||
|
return {
|
||||||
|
type: REPORT_INIT,
|
||||||
|
account,
|
||||||
|
status
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function cancelReport() {
|
||||||
|
return {
|
||||||
|
type: REPORT_CANCEL
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function toggleStatusReport(statusId, checked) {
|
||||||
|
return {
|
||||||
|
type: REPORT_STATUS_TOGGLE,
|
||||||
|
statusId,
|
||||||
|
checked,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function submitReport() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch(submitReportRequest());
|
||||||
|
|
||||||
|
api(getState).post('/api/v1/reports', {
|
||||||
|
account_id: getState().getIn(['reports', 'new', 'account_id']),
|
||||||
|
status_ids: getState().getIn(['reports', 'new', 'status_ids']),
|
||||||
|
comment: getState().getIn(['reports', 'new', 'comment'])
|
||||||
|
}).then(response => dispatch(submitReportSuccess(response.data))).catch(error => dispatch(submitReportFail(error)));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function submitReportRequest() {
|
||||||
|
return {
|
||||||
|
type: REPORT_SUBMIT_REQUEST
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function submitReportSuccess(report) {
|
||||||
|
return {
|
||||||
|
type: REPORT_SUBMIT_SUCCESS,
|
||||||
|
report
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function submitReportFail(error) {
|
||||||
|
return {
|
||||||
|
type: REPORT_SUBMIT_FAIL,
|
||||||
|
error
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function changeReportComment(comment) {
|
||||||
|
return {
|
||||||
|
type: REPORT_COMMENT_CHANGE,
|
||||||
|
comment
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import api from '../api'
|
import api from '../api'
|
||||||
|
|
||||||
export const SEARCH_CHANGE = 'SEARCH_CHANGE';
|
export const SEARCH_CHANGE = 'SEARCH_CHANGE';
|
||||||
export const SEARCH_SUGGESTIONS_CLEAR = 'SEARCH_SUGGESTIONS_CLEAR';
|
export const SEARCH_CLEAR = 'SEARCH_CLEAR';
|
||||||
export const SEARCH_SUGGESTIONS_READY = 'SEARCH_SUGGESTIONS_READY';
|
export const SEARCH_SHOW = 'SEARCH_SHOW';
|
||||||
export const SEARCH_RESET = 'SEARCH_RESET';
|
|
||||||
|
export const SEARCH_FETCH_REQUEST = 'SEARCH_FETCH_REQUEST';
|
||||||
|
export const SEARCH_FETCH_SUCCESS = 'SEARCH_FETCH_SUCCESS';
|
||||||
|
export const SEARCH_FETCH_FAIL = 'SEARCH_FETCH_FAIL';
|
||||||
|
|
||||||
export function changeSearch(value) {
|
export function changeSearch(value) {
|
||||||
return {
|
return {
|
||||||
@@ -12,40 +15,59 @@ export function changeSearch(value) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function clearSearchSuggestions() {
|
export function clearSearch() {
|
||||||
return {
|
return {
|
||||||
type: SEARCH_SUGGESTIONS_CLEAR
|
type: SEARCH_CLEAR
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function readySearchSuggestions(value, accounts) {
|
export function submitSearch() {
|
||||||
return {
|
|
||||||
type: SEARCH_SUGGESTIONS_READY,
|
|
||||||
value,
|
|
||||||
accounts
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function fetchSearchSuggestions(value) {
|
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
if (getState().getIn(['search', 'loaded_value']) === value) {
|
const value = getState().getIn(['search', 'value']);
|
||||||
|
|
||||||
|
if (value.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
api(getState).get('/api/v1/accounts/search', {
|
dispatch(fetchSearchRequest());
|
||||||
|
|
||||||
|
api(getState).get('/api/v1/search', {
|
||||||
params: {
|
params: {
|
||||||
q: value,
|
q: value,
|
||||||
resolve: true,
|
resolve: true
|
||||||
limit: 4
|
|
||||||
}
|
}
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
dispatch(readySearchSuggestions(value, response.data));
|
dispatch(fetchSearchSuccess(response.data));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(fetchSearchFail(error));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function resetSearch() {
|
export function fetchSearchRequest() {
|
||||||
return {
|
return {
|
||||||
type: SEARCH_RESET
|
type: SEARCH_FETCH_REQUEST
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchSearchSuccess(results) {
|
||||||
|
return {
|
||||||
|
type: SEARCH_FETCH_SUCCESS,
|
||||||
|
results,
|
||||||
|
accounts: results.accounts,
|
||||||
|
statuses: results.statuses
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchSearchFail(error) {
|
||||||
|
return {
|
||||||
|
type: SEARCH_FETCH_FAIL,
|
||||||
|
error
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function showSearch() {
|
||||||
|
return {
|
||||||
|
type: SEARCH_SHOW
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,12 +27,17 @@ export function fetchStatus(id) {
|
|||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const skipLoading = getState().getIn(['statuses', id], null) !== null;
|
const skipLoading = getState().getIn(['statuses', id], null) !== null;
|
||||||
|
|
||||||
|
dispatch(fetchContext(id));
|
||||||
|
dispatch(fetchStatusCard(id));
|
||||||
|
|
||||||
|
if (skipLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(fetchStatusRequest(id, skipLoading));
|
dispatch(fetchStatusRequest(id, skipLoading));
|
||||||
|
|
||||||
api(getState).get(`/api/v1/statuses/${id}`).then(response => {
|
api(getState).get(`/api/v1/statuses/${id}`).then(response => {
|
||||||
dispatch(fetchStatusSuccess(response.data, skipLoading));
|
dispatch(fetchStatusSuccess(response.data, skipLoading));
|
||||||
dispatch(fetchContext(id));
|
|
||||||
dispatch(fetchStatusCard(id));
|
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchStatusFail(id, error, skipLoading));
|
dispatch(fetchStatusFail(id, error, skipLoading));
|
||||||
});
|
});
|
||||||
@@ -52,7 +57,8 @@ export function fetchStatusFail(id, error, skipLoading) {
|
|||||||
type: STATUS_FETCH_FAIL,
|
type: STATUS_FETCH_FAIL,
|
||||||
id,
|
id,
|
||||||
error,
|
error,
|
||||||
skipLoading
|
skipLoading,
|
||||||
|
skipAlert: true
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -97,7 +103,12 @@ export function fetchContext(id) {
|
|||||||
|
|
||||||
api(getState).get(`/api/v1/statuses/${id}/context`).then(response => {
|
api(getState).get(`/api/v1/statuses/${id}/context`).then(response => {
|
||||||
dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants));
|
dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants));
|
||||||
|
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
if (error.response.status === 404) {
|
||||||
|
dispatch(deleteFromTimelines(id));
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(fetchContextFail(id, error));
|
dispatch(fetchContextFail(id, error));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -124,6 +135,7 @@ export function fetchContextFail(id, error) {
|
|||||||
return {
|
return {
|
||||||
type: CONTEXT_FETCH_FAIL,
|
type: CONTEXT_FETCH_FAIL,
|
||||||
id,
|
id,
|
||||||
error
|
error,
|
||||||
|
skipAlert: true
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import api from '../api'
|
import api, { getLinks } from '../api'
|
||||||
import Immutable from 'immutable';
|
import Immutable from 'immutable';
|
||||||
|
|
||||||
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
|
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
|
||||||
@@ -14,12 +14,16 @@ export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL';
|
|||||||
|
|
||||||
export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
|
export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
|
||||||
|
|
||||||
export function refreshTimelineSuccess(timeline, statuses, skipLoading) {
|
export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
|
||||||
|
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
|
||||||
|
|
||||||
|
export function refreshTimelineSuccess(timeline, statuses, skipLoading, next) {
|
||||||
return {
|
return {
|
||||||
type: TIMELINE_REFRESH_SUCCESS,
|
type: TIMELINE_REFRESH_SUCCESS,
|
||||||
timeline,
|
timeline,
|
||||||
statuses,
|
statuses,
|
||||||
skipLoading
|
skipLoading,
|
||||||
|
next
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -69,25 +73,27 @@ export function refreshTimeline(timeline, id = null) {
|
|||||||
|
|
||||||
const ids = getState().getIn(['timelines', timeline, 'items'], Immutable.List());
|
const ids = getState().getIn(['timelines', timeline, 'items'], Immutable.List());
|
||||||
const newestId = ids.size > 0 ? ids.first() : null;
|
const newestId = ids.size > 0 ? ids.first() : null;
|
||||||
|
let params = getState().getIn(['timelines', timeline, 'params'], {});
|
||||||
|
const path = getState().getIn(['timelines', timeline, 'path'])(id);
|
||||||
|
|
||||||
let params = '';
|
|
||||||
let path = timeline;
|
|
||||||
let skipLoading = false;
|
let skipLoading = false;
|
||||||
|
|
||||||
if (newestId !== null && getState().getIn(['timelines', timeline, 'loaded']) && (id === null || getState().getIn(['timelines', timeline, 'id']) === id)) {
|
if (newestId !== null && getState().getIn(['timelines', timeline, 'loaded']) && (id === null || getState().getIn(['timelines', timeline, 'id']) === id)) {
|
||||||
params = `?since_id=${newestId}`;
|
if (id === null && getState().getIn(['timelines', timeline, 'online'])) {
|
||||||
skipLoading = true;
|
// Skip refreshing when timeline is live anyway
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (id) {
|
params = { ...params, since_id: newestId };
|
||||||
path = `${path}/${id}`
|
skipLoading = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(refreshTimelineRequest(timeline, id, skipLoading));
|
dispatch(refreshTimelineRequest(timeline, id, skipLoading));
|
||||||
|
|
||||||
api(getState).get(`/api/v1/timelines/${path}${params}`).then(function (response) {
|
api(getState).get(path, { params }).then(response => {
|
||||||
dispatch(refreshTimelineSuccess(timeline, response.data, skipLoading));
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
}).catch(function (error) {
|
dispatch(refreshTimelineSuccess(timeline, response.data, skipLoading, next ? next.uri : null));
|
||||||
|
}).catch(error => {
|
||||||
dispatch(refreshTimelineFail(timeline, error, skipLoading));
|
dispatch(refreshTimelineFail(timeline, error, skipLoading));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -102,50 +108,50 @@ export function refreshTimelineFail(timeline, error, skipLoading) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function expandTimeline(timeline, id = null) {
|
export function expandTimeline(timeline) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const lastId = getState().getIn(['timelines', timeline, 'items'], Immutable.List()).last();
|
if (getState().getIn(['timelines', timeline, 'isLoading'])) {
|
||||||
|
|
||||||
if (!lastId || getState().getIn(['timelines', timeline, 'isLoading'])) {
|
|
||||||
// If timeline is empty, don't try to load older posts since there are none
|
|
||||||
// Also if already loading
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(expandTimelineRequest(timeline, id));
|
if (getState().getIn(['timelines', timeline, 'items']).size === 0) {
|
||||||
|
return;
|
||||||
let path = timeline;
|
|
||||||
|
|
||||||
if (id) {
|
|
||||||
path = `${path}/${id}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
api(getState).get(`/api/v1/timelines/${path}`, {
|
const path = getState().getIn(['timelines', timeline, 'path'])(getState().getIn(['timelines', timeline, 'id']));
|
||||||
|
const params = getState().getIn(['timelines', timeline, 'params'], {});
|
||||||
|
const lastId = getState().getIn(['timelines', timeline, 'items']).last();
|
||||||
|
|
||||||
|
dispatch(expandTimelineRequest(timeline));
|
||||||
|
|
||||||
|
api(getState).get(path, {
|
||||||
params: {
|
params: {
|
||||||
limit: 10,
|
...params,
|
||||||
max_id: lastId
|
max_id: lastId,
|
||||||
|
limit: 10
|
||||||
}
|
}
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
dispatch(expandTimelineSuccess(timeline, response.data));
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(expandTimelineSuccess(timeline, response.data, next ? next.uri : null));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(expandTimelineFail(timeline, error));
|
dispatch(expandTimelineFail(timeline, error));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function expandTimelineRequest(timeline, id) {
|
export function expandTimelineRequest(timeline) {
|
||||||
return {
|
return {
|
||||||
type: TIMELINE_EXPAND_REQUEST,
|
type: TIMELINE_EXPAND_REQUEST,
|
||||||
timeline,
|
timeline
|
||||||
id
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function expandTimelineSuccess(timeline, statuses) {
|
export function expandTimelineSuccess(timeline, statuses, next) {
|
||||||
return {
|
return {
|
||||||
type: TIMELINE_EXPAND_SUCCESS,
|
type: TIMELINE_EXPAND_SUCCESS,
|
||||||
timeline,
|
timeline,
|
||||||
statuses
|
statuses,
|
||||||
|
next
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -164,3 +170,17 @@ export function scrollTopTimeline(timeline, top) {
|
|||||||
top
|
top
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function connectTimeline(timeline) {
|
||||||
|
return {
|
||||||
|
type: TIMELINE_CONNECT,
|
||||||
|
timeline
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function disconnectTimeline(timeline) {
|
||||||
|
return {
|
||||||
|
type: TIMELINE_DISCONNECT,
|
||||||
|
timeline
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import LinkHeader from 'http-link-header';
|
import LinkHeader from './link_header';
|
||||||
|
|
||||||
export const getLinks = response => {
|
export const getLinks = response => {
|
||||||
const value = response.headers.link;
|
const value = response.headers.link;
|
||||||
|
|||||||
@@ -10,29 +10,10 @@ const messages = defineMessages({
|
|||||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
||||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock' }
|
unblock: { id: 'account.unblock', defaultMessage: 'Unblock' },
|
||||||
|
unmute: { id: 'account.unmute', defaultMessage: 'Unmute' }
|
||||||
});
|
});
|
||||||
|
|
||||||
const outerStyle = {
|
|
||||||
padding: '10px',
|
|
||||||
borderBottom: '1px solid #363c4b'
|
|
||||||
};
|
|
||||||
|
|
||||||
const itemStyle = {
|
|
||||||
flex: '1 1 auto',
|
|
||||||
display: 'block',
|
|
||||||
color: '#9baec8',
|
|
||||||
overflow: 'hidden',
|
|
||||||
textDecoration: 'none',
|
|
||||||
fontSize: '14px'
|
|
||||||
};
|
|
||||||
|
|
||||||
const noteStyle = {
|
|
||||||
paddingTop: '5px',
|
|
||||||
fontSize: '12px',
|
|
||||||
color: '#616b86'
|
|
||||||
};
|
|
||||||
|
|
||||||
const buttonsStyle = {
|
const buttonsStyle = {
|
||||||
padding: '10px',
|
padding: '10px',
|
||||||
height: '18px'
|
height: '18px'
|
||||||
@@ -45,16 +26,10 @@ const Account = React.createClass({
|
|||||||
me: React.PropTypes.number.isRequired,
|
me: React.PropTypes.number.isRequired,
|
||||||
onFollow: React.PropTypes.func.isRequired,
|
onFollow: React.PropTypes.func.isRequired,
|
||||||
onBlock: React.PropTypes.func.isRequired,
|
onBlock: React.PropTypes.func.isRequired,
|
||||||
withNote: React.PropTypes.bool,
|
onMute: React.PropTypes.func.isRequired,
|
||||||
intl: React.PropTypes.object.isRequired
|
intl: React.PropTypes.object.isRequired
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps () {
|
|
||||||
return {
|
|
||||||
withNote: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
handleFollow () {
|
handleFollow () {
|
||||||
@@ -65,38 +40,41 @@ const Account = React.createClass({
|
|||||||
this.props.onBlock(this.props.account);
|
this.props.onBlock(this.props.account);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleMute () {
|
||||||
|
this.props.onMute(this.props.account);
|
||||||
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, me, withNote, intl } = this.props;
|
const { account, me, intl } = this.props;
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return <div />;
|
return <div />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let note, buttons;
|
let buttons;
|
||||||
|
|
||||||
if (account.get('note').length > 0 && withNote) {
|
|
||||||
note = <div style={noteStyle}>{account.get('note')}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (account.get('id') !== me && account.get('relationship', null) !== null) {
|
if (account.get('id') !== me && account.get('relationship', null) !== null) {
|
||||||
const following = account.getIn(['relationship', 'following']);
|
const following = account.getIn(['relationship', 'following']);
|
||||||
const requested = account.getIn(['relationship', 'requested']);
|
const requested = account.getIn(['relationship', 'requested']);
|
||||||
const blocking = account.getIn(['relationship', 'blocking']);
|
const blocking = account.getIn(['relationship', 'blocking']);
|
||||||
|
const muting = account.getIn(['relationship', 'muting']);
|
||||||
|
|
||||||
if (requested) {
|
if (requested) {
|
||||||
buttons = <IconButton disabled={true} icon='hourglass' title={intl.formatMessage(messages.requested)} />
|
buttons = <IconButton disabled={true} icon='hourglass' title={intl.formatMessage(messages.requested)} />
|
||||||
} else if (blocking) {
|
} else if (blocking) {
|
||||||
buttons = <IconButton active={true} icon='unlock-alt' title={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
|
buttons = <IconButton active={true} icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
|
||||||
|
} else if (muting) {
|
||||||
|
buttons = <IconButton active={true} icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />;
|
||||||
} else {
|
} else {
|
||||||
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
|
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={outerStyle}>
|
<div className='account'>
|
||||||
<div style={{ display: 'flex' }}>
|
<div style={{ display: 'flex' }}>
|
||||||
<Permalink key={account.get('id')} style={itemStyle} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}>
|
<Permalink key={account.get('id')} className='account__display-name' href={account.get('url')} to={`/accounts/${account.get('id')}`}>
|
||||||
<div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} size={36} /></div>
|
<div style={{ float: 'left', marginLeft: '12px', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={36} /></div>
|
||||||
<DisplayName account={account} />
|
<DisplayName account={account} />
|
||||||
</Permalink>
|
</Permalink>
|
||||||
|
|
||||||
@@ -104,8 +82,6 @@ const Account = React.createClass({
|
|||||||
{buttons}
|
{buttons}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{note}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
|
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { isRtl } from '../rtl';
|
||||||
|
|
||||||
const textAtCursorMatchesToken = (str, caretPosition) => {
|
const textAtCursorMatchesToken = (str, caretPosition) => {
|
||||||
let word;
|
let word;
|
||||||
@@ -32,20 +33,18 @@ const AutosuggestTextarea = React.createClass({
|
|||||||
value: React.PropTypes.string,
|
value: React.PropTypes.string,
|
||||||
suggestions: ImmutablePropTypes.list,
|
suggestions: ImmutablePropTypes.list,
|
||||||
disabled: React.PropTypes.bool,
|
disabled: React.PropTypes.bool,
|
||||||
fileDropDate: React.PropTypes.instanceOf(Date),
|
|
||||||
placeholder: React.PropTypes.string,
|
placeholder: React.PropTypes.string,
|
||||||
onSuggestionSelected: React.PropTypes.func.isRequired,
|
onSuggestionSelected: React.PropTypes.func.isRequired,
|
||||||
onSuggestionsClearRequested: React.PropTypes.func.isRequired,
|
onSuggestionsClearRequested: React.PropTypes.func.isRequired,
|
||||||
onSuggestionsFetchRequested: React.PropTypes.func.isRequired,
|
onSuggestionsFetchRequested: React.PropTypes.func.isRequired,
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: React.PropTypes.func.isRequired,
|
||||||
onKeyUp: React.PropTypes.func,
|
onKeyUp: React.PropTypes.func,
|
||||||
onKeyDown: React.PropTypes.func
|
onKeyDown: React.PropTypes.func,
|
||||||
|
onPaste: React.PropTypes.func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState () {
|
getInitialState () {
|
||||||
return {
|
return {
|
||||||
isFileDragging: false,
|
|
||||||
fileDraggingDate: undefined,
|
|
||||||
suggestionsHidden: false,
|
suggestionsHidden: false,
|
||||||
selectedSuggestion: 0,
|
selectedSuggestion: 0,
|
||||||
lastToken: null,
|
lastToken: null,
|
||||||
@@ -137,45 +136,28 @@ const AutosuggestTextarea = React.createClass({
|
|||||||
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden) {
|
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden) {
|
||||||
this.setState({ suggestionsHidden: false });
|
this.setState({ suggestionsHidden: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileDropDate = nextProps.fileDropDate;
|
|
||||||
const { isFileDragging, fileDraggingDate } = this.state;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* We can't detect drop events, because they might not be on the textarea (the app allows dropping anywhere in the
|
|
||||||
* window). Instead, on-drop, we notify this textarea to stop its hover effect by passing in a prop with the
|
|
||||||
* drop-date.
|
|
||||||
*/
|
|
||||||
if (isFileDragging && fileDraggingDate && fileDropDate // if dragging when props updated, and dates aren't undefined
|
|
||||||
&& fileDropDate > fileDraggingDate) { // and if the drop date is now greater than when we started dragging
|
|
||||||
// then we should stop dragging
|
|
||||||
this.setState({
|
|
||||||
isFileDragging: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setTextarea (c) {
|
setTextarea (c) {
|
||||||
this.textarea = c;
|
this.textarea = c;
|
||||||
},
|
},
|
||||||
|
|
||||||
onDragEnter () {
|
onPaste (e) {
|
||||||
this.setState({
|
if (e.clipboardData && e.clipboardData.files.length === 1) {
|
||||||
isFileDragging: true,
|
this.props.onPaste(e.clipboardData.files)
|
||||||
fileDraggingDate: new Date()
|
e.preventDefault();
|
||||||
})
|
}
|
||||||
},
|
|
||||||
|
|
||||||
onDragExit () {
|
|
||||||
this.setState({
|
|
||||||
isFileDragging: false
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { value, suggestions, fileDropDate, disabled, placeholder, onKeyUp } = this.props;
|
const { value, suggestions, disabled, placeholder, onKeyUp } = this.props;
|
||||||
const { isFileDragging, suggestionsHidden, selectedSuggestion } = this.state;
|
const { suggestionsHidden, selectedSuggestion } = this.state;
|
||||||
const className = isFileDragging ? 'autosuggest-textarea__textarea file-drop' : 'autosuggest-textarea__textarea';
|
const className = 'autosuggest-textarea__textarea';
|
||||||
|
const style = { direction: 'ltr' };
|
||||||
|
|
||||||
|
if (isRtl(value)) {
|
||||||
|
style.direction = 'rtl';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='autosuggest-textarea'>
|
<div className='autosuggest-textarea'>
|
||||||
@@ -190,13 +172,18 @@ const AutosuggestTextarea = React.createClass({
|
|||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
onKeyUp={onKeyUp}
|
onKeyUp={onKeyUp}
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
onDragEnter={this.onDragEnter}
|
onPaste={this.onPaste}
|
||||||
onDragExit={this.onDragExit}
|
style={style}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div style={{ display: (suggestions.size > 0 && !suggestionsHidden) ? 'block' : 'none' }} className='autosuggest-textarea__suggestions'>
|
<div style={{ display: (suggestions.size > 0 && !suggestionsHidden) ? 'block' : 'none' }} className='autosuggest-textarea__suggestions'>
|
||||||
{suggestions.map((suggestion, i) => (
|
{suggestions.map((suggestion, i) => (
|
||||||
<div key={suggestion} className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`} onClick={this.onSuggestionClick.bind(this, suggestion)}>
|
<div
|
||||||
|
role='button'
|
||||||
|
tabIndex='0'
|
||||||
|
key={suggestion}
|
||||||
|
className={`autosuggest-textarea__suggestions__item ${i === selectedSuggestion ? 'selected' : ''}`}
|
||||||
|
onClick={this.onSuggestionClick.bind(this, suggestion)}>
|
||||||
<AutosuggestAccountContainer id={suggestion} />
|
<AutosuggestAccountContainer id={suggestion} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,103 +1,18 @@
|
|||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
|
|
||||||
// From: http://stackoverflow.com/a/18320662
|
|
||||||
const resample = (canvas, width, height, resize_canvas) => {
|
|
||||||
let width_source = canvas.width;
|
|
||||||
let height_source = canvas.height;
|
|
||||||
width = Math.round(width);
|
|
||||||
height = Math.round(height);
|
|
||||||
|
|
||||||
let ratio_w = width_source / width;
|
|
||||||
let ratio_h = height_source / height;
|
|
||||||
let ratio_w_half = Math.ceil(ratio_w / 2);
|
|
||||||
let ratio_h_half = Math.ceil(ratio_h / 2);
|
|
||||||
|
|
||||||
let ctx = canvas.getContext("2d");
|
|
||||||
let img = ctx.getImageData(0, 0, width_source, height_source);
|
|
||||||
let img2 = ctx.createImageData(width, height);
|
|
||||||
let data = img.data;
|
|
||||||
let data2 = img2.data;
|
|
||||||
|
|
||||||
for (let j = 0; j < height; j++) {
|
|
||||||
for (let i = 0; i < width; i++) {
|
|
||||||
let x2 = (i + j * width) * 4;
|
|
||||||
let weight = 0;
|
|
||||||
let weights = 0;
|
|
||||||
let weights_alpha = 0;
|
|
||||||
let gx_r = 0;
|
|
||||||
let gx_g = 0;
|
|
||||||
let gx_b = 0;
|
|
||||||
let gx_a = 0;
|
|
||||||
let center_y = (j + 0.5) * ratio_h;
|
|
||||||
let yy_start = Math.floor(j * ratio_h);
|
|
||||||
let yy_stop = Math.ceil((j + 1) * ratio_h);
|
|
||||||
|
|
||||||
for (let yy = yy_start; yy < yy_stop; yy++) {
|
|
||||||
let dy = Math.abs(center_y - (yy + 0.5)) / ratio_h_half;
|
|
||||||
let center_x = (i + 0.5) * ratio_w;
|
|
||||||
let w0 = dy * dy; //pre-calc part of w
|
|
||||||
let xx_start = Math.floor(i * ratio_w);
|
|
||||||
let xx_stop = Math.ceil((i + 1) * ratio_w);
|
|
||||||
|
|
||||||
for (let xx = xx_start; xx < xx_stop; xx++) {
|
|
||||||
let dx = Math.abs(center_x - (xx + 0.5)) / ratio_w_half;
|
|
||||||
let w = Math.sqrt(w0 + dx * dx);
|
|
||||||
|
|
||||||
if (w >= 1) {
|
|
||||||
// pixel too far
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// hermite filter
|
|
||||||
weight = 2 * w * w * w - 3 * w * w + 1;
|
|
||||||
let pos_x = 4 * (xx + yy * width_source);
|
|
||||||
|
|
||||||
// alpha
|
|
||||||
gx_a += weight * data[pos_x + 3];
|
|
||||||
weights_alpha += weight;
|
|
||||||
|
|
||||||
// colors
|
|
||||||
if (data[pos_x + 3] < 255)
|
|
||||||
weight = weight * data[pos_x + 3] / 250;
|
|
||||||
|
|
||||||
gx_r += weight * data[pos_x];
|
|
||||||
gx_g += weight * data[pos_x + 1];
|
|
||||||
gx_b += weight * data[pos_x + 2];
|
|
||||||
weights += weight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data2[x2] = gx_r / weights;
|
|
||||||
data2[x2 + 1] = gx_g / weights;
|
|
||||||
data2[x2 + 2] = gx_b / weights;
|
|
||||||
data2[x2 + 3] = gx_a / weights_alpha;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear and resize canvas
|
|
||||||
if (resize_canvas === true) {
|
|
||||||
canvas.width = width;
|
|
||||||
canvas.height = height;
|
|
||||||
} else {
|
|
||||||
ctx.clearRect(0, 0, width_source, height_source);
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw
|
|
||||||
ctx.putImageData(img2, 0, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Avatar = React.createClass({
|
const Avatar = React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
src: React.PropTypes.string.isRequired,
|
src: React.PropTypes.string.isRequired,
|
||||||
|
staticSrc: React.PropTypes.string,
|
||||||
size: React.PropTypes.number.isRequired,
|
size: React.PropTypes.number.isRequired,
|
||||||
style: React.PropTypes.object,
|
style: React.PropTypes.object,
|
||||||
animated: React.PropTypes.bool
|
animate: React.PropTypes.bool
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps () {
|
getDefaultProps () {
|
||||||
return {
|
return {
|
||||||
animated: true
|
animate: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -117,38 +32,30 @@ const Avatar = React.createClass({
|
|||||||
this.setState({ hovering: false });
|
this.setState({ hovering: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
handleLoad () {
|
|
||||||
this.canvas.width = this.image.naturalWidth;
|
|
||||||
this.canvas.height = this.image.naturalHeight;
|
|
||||||
this.canvas.getContext('2d').drawImage(this.image, 0, 0);
|
|
||||||
|
|
||||||
resample(this.canvas, this.props.size * window.devicePixelRatio, this.props.size * window.devicePixelRatio, true);
|
|
||||||
},
|
|
||||||
|
|
||||||
setImageRef (c) {
|
|
||||||
this.image = c;
|
|
||||||
},
|
|
||||||
|
|
||||||
setCanvasRef (c) {
|
|
||||||
this.canvas = c;
|
|
||||||
},
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
const { src, size, staticSrc, animate } = this.props;
|
||||||
const { hovering } = this.state;
|
const { hovering } = this.state;
|
||||||
|
|
||||||
if (this.props.animated) {
|
const style = {
|
||||||
return (
|
...this.props.style,
|
||||||
<div style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px` }}>
|
width: `${size}px`,
|
||||||
<img src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ borderRadius: '4px' }} />
|
height: `${size}px`,
|
||||||
</div>
|
backgroundSize: `${size}px ${size}px`
|
||||||
);
|
};
|
||||||
|
|
||||||
|
if (hovering || animate) {
|
||||||
|
style.backgroundImage = `url(${src})`;
|
||||||
|
} else {
|
||||||
|
style.backgroundImage = `url(${staticSrc})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} style={{ ...this.props.style, width: `${this.props.size}px`, height: `${this.props.size}px`, position: 'relative' }}>
|
<div
|
||||||
<img ref={this.setImageRef} onLoad={this.handleLoad} src={this.props.src} width={this.props.size} height={this.props.size} alt='' style={{ position: 'absolute', top: '0', left: '0', opacity: hovering ? '1' : '0', borderRadius: '4px' }} />
|
className='avatar'
|
||||||
<canvas ref={this.setCanvasRef} style={{ borderRadius: '4px', width: this.props.size, height: this.props.size, opacity: hovering ? '0' : '1' }} />
|
onMouseEnter={this.handleMouseEnter}
|
||||||
</div>
|
onMouseLeave={this.handleMouseLeave}
|
||||||
|
style={style}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import PureRenderMixin from 'react-addons-pure-render-mixin';
|
|||||||
const Button = React.createClass({
|
const Button = React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
text: React.PropTypes.string,
|
text: React.PropTypes.node,
|
||||||
onClick: React.PropTypes.func,
|
onClick: React.PropTypes.func,
|
||||||
disabled: React.PropTypes.bool,
|
disabled: React.PropTypes.bool,
|
||||||
block: React.PropTypes.bool,
|
block: React.PropTypes.bool,
|
||||||
secondary: React.PropTypes.bool,
|
secondary: React.PropTypes.bool,
|
||||||
size: React.PropTypes.number,
|
size: React.PropTypes.number,
|
||||||
|
style: React.PropTypes.object,
|
||||||
|
children: React.PropTypes.node
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps () {
|
getDefaultProps () {
|
||||||
@@ -34,11 +36,9 @@ const Button = React.createClass({
|
|||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
border: '10px none',
|
border: '10px none',
|
||||||
color: '#fff',
|
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
letterSpacing: '0',
|
letterSpacing: '0',
|
||||||
textTransform: 'uppercase',
|
|
||||||
padding: `0 ${this.props.size / 2.25}px`,
|
padding: `0 ${this.props.size / 2.25}px`,
|
||||||
height: `${this.props.size}px`,
|
height: `${this.props.size}px`,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
|||||||
19
app/assets/javascripts/components/components/collapsable.jsx
Normal file
19
app/assets/javascripts/components/components/collapsable.jsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Motion, spring } from 'react-motion';
|
||||||
|
|
||||||
|
const Collapsable = ({ fullHeight, isVisible, children }) => (
|
||||||
|
<Motion defaultStyle={{ opacity: !isVisible ? 0 : 100, height: isVisible ? fullHeight : 0 }} style={{ opacity: spring(!isVisible ? 0 : 100), height: spring(!isVisible ? 0 : fullHeight) }}>
|
||||||
|
{({ opacity, height }) =>
|
||||||
|
<div style={{ height: `${height}px`, overflow: 'hidden', opacity: opacity / 100, display: Math.floor(opacity) === 0 ? 'none' : 'block' }}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</Motion>
|
||||||
|
);
|
||||||
|
|
||||||
|
Collapsable.propTypes = {
|
||||||
|
fullHeight: React.PropTypes.number.isRequired,
|
||||||
|
isVisible: React.PropTypes.bool.isRequired,
|
||||||
|
children: React.PropTypes.node.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Collapsable;
|
||||||
@@ -1,15 +1,6 @@
|
|||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
const outerStyle = {
|
|
||||||
padding: '15px',
|
|
||||||
fontSize: '16px',
|
|
||||||
background: '#2f3441',
|
|
||||||
flex: '0 0 auto',
|
|
||||||
cursor: 'pointer',
|
|
||||||
color: '#2b90d9'
|
|
||||||
};
|
|
||||||
|
|
||||||
const iconStyle = {
|
const iconStyle = {
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
marginRight: '5px'
|
marginRight: '5px'
|
||||||
@@ -24,12 +15,13 @@ const ColumnBackButton = React.createClass({
|
|||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
handleClick () {
|
handleClick () {
|
||||||
this.context.router.goBack();
|
if (window.history && window.history.length === 1) this.context.router.push("/");
|
||||||
|
else this.context.router.goBack();
|
||||||
},
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div onClick={this.handleClick} style={outerStyle} className='column-back-button'>
|
<div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button'>
|
||||||
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
|
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
|
||||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ const outerStyle = {
|
|||||||
top: '-48px',
|
top: '-48px',
|
||||||
padding: '15px',
|
padding: '15px',
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
background: '#2f3441',
|
|
||||||
flex: '0 0 auto',
|
flex: '0 0 auto',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer'
|
||||||
color: '#2b90d9'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const iconStyle = {
|
const iconStyle = {
|
||||||
@@ -33,7 +31,7 @@ const ColumnBackButtonSlim = React.createClass({
|
|||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
<div style={outerStyle} onClick={this.handleClick} className='column-back-button'>
|
<div role='button' tabIndex='0' style={outerStyle} onClick={this.handleClick} className='column-back-button'>
|
||||||
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
|
<i className='fa fa-fw fa-chevron-left' style={iconStyle} />
|
||||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,13 +7,15 @@ const iconStyle = {
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
right: '0',
|
right: '0',
|
||||||
top: '-48px',
|
top: '-48px',
|
||||||
cursor: 'pointer'
|
cursor: 'pointer',
|
||||||
|
zIndex: '3'
|
||||||
};
|
};
|
||||||
|
|
||||||
const ColumnCollapsable = React.createClass({
|
const ColumnCollapsable = React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
icon: React.PropTypes.string.isRequired,
|
icon: React.PropTypes.string.isRequired,
|
||||||
|
title: React.PropTypes.string,
|
||||||
fullHeight: React.PropTypes.number.isRequired,
|
fullHeight: React.PropTypes.number.isRequired,
|
||||||
children: React.PropTypes.node,
|
children: React.PropTypes.node,
|
||||||
onCollapse: React.PropTypes.func
|
onCollapse: React.PropTypes.func
|
||||||
@@ -38,12 +40,15 @@ const ColumnCollapsable = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { icon, fullHeight, children } = this.props;
|
const { icon, title, fullHeight, children } = this.props;
|
||||||
const { collapsed } = this.state;
|
const { collapsed } = this.state;
|
||||||
|
const collapsedClassName = collapsed ? 'collapsable-collapsed' : 'collapsable';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
<div style={{...iconStyle, color: collapsed ? '#9baec8' : '#fff', background: collapsed ? '#2f3441' : '#373b4a' }} onClick={this.handleToggleCollapsed}><i className={`fa fa-${icon}`} /></div>
|
<div role='button' tabIndex='0' title={`${title}`} style={{...iconStyle }} className={`column-icon ${collapsedClassName}`} onClick={this.handleToggleCollapsed}>
|
||||||
|
<i className={`fa fa-${icon}`} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : fullHeight, collapsed ? undefined : { stiffness: 150, damping: 9 }) }}>
|
<Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(collapsed ? 0 : 100), height: spring(collapsed ? 0 : fullHeight, collapsed ? undefined : { stiffness: 150, damping: 9 }) }}>
|
||||||
{({ opacity, height }) =>
|
{({ opacity, height }) =>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
import escapeTextContentForBrowser from 'react/lib/escapeTextContentForBrowser';
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
import emojify from '../emoji';
|
import emojify from '../emoji';
|
||||||
|
|
||||||
const DisplayName = React.createClass({
|
const DisplayName = React.createClass({
|
||||||
|
|||||||
@@ -1,32 +1,72 @@
|
|||||||
import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
|
import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
|
||||||
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
|
|
||||||
const DropdownMenu = ({ icon, items, size, direction }) => {
|
const DropdownMenu = React.createClass({
|
||||||
const directionClass = (direction == "left") ? "dropdown__left" : "dropdown__right";
|
|
||||||
|
|
||||||
return (
|
propTypes: {
|
||||||
<Dropdown>
|
icon: React.PropTypes.string.isRequired,
|
||||||
<DropdownTrigger className='icon-button' style={{ fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` }}>
|
items: React.PropTypes.array.isRequired,
|
||||||
<i className={`fa fa-fw fa-${icon}`} style={{ verticalAlign: 'middle' }} />
|
size: React.PropTypes.number.isRequired,
|
||||||
</DropdownTrigger>
|
direction: React.PropTypes.string
|
||||||
|
},
|
||||||
|
|
||||||
<DropdownContent className={directionClass} style={{ lineHeight: '18px', textAlign: 'left' }}>
|
getDefaultProps () {
|
||||||
<ul>
|
return {
|
||||||
{items.map(({ text, action, href = '#' }, i) => <li key={i}><a href={href} target='_blank' rel='noopener' onClick={e => {
|
direction: 'left'
|
||||||
if (typeof action === 'function') {
|
};
|
||||||
e.preventDefault();
|
},
|
||||||
action();
|
|
||||||
}
|
|
||||||
}}>{text}</a></li>)}
|
|
||||||
</ul>
|
|
||||||
</DropdownContent>
|
|
||||||
</Dropdown>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
DropdownMenu.propTypes = {
|
mixins: [PureRenderMixin],
|
||||||
icon: React.PropTypes.string.isRequired,
|
|
||||||
items: React.PropTypes.array.isRequired,
|
setRef (c) {
|
||||||
size: React.PropTypes.number.isRequired
|
this.dropdown = c;
|
||||||
};
|
},
|
||||||
|
|
||||||
|
handleClick (i, e) {
|
||||||
|
const { action } = this.props.items[i];
|
||||||
|
|
||||||
|
if (typeof action === 'function') {
|
||||||
|
e.preventDefault();
|
||||||
|
action();
|
||||||
|
this.dropdown.hide();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderItem (item, i) {
|
||||||
|
if (item === null) {
|
||||||
|
return <li key={i} className='dropdown__sep' />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { text, action, href = '#' } = item;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={i}>
|
||||||
|
<a href={href} target='_blank' rel='noopener' onClick={this.handleClick.bind(this, i)}>
|
||||||
|
{text}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { icon, items, size, direction } = this.props;
|
||||||
|
const directionClass = (direction === "left") ? "dropdown__left" : "dropdown__right";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown ref={this.setRef}>
|
||||||
|
<DropdownTrigger className='icon-button' style={{ fontSize: `${size}px`, width: `${size}px`, lineHeight: `${size}px` }}>
|
||||||
|
<i className={`fa fa-fw fa-${icon}`} style={{ verticalAlign: 'middle' }} />
|
||||||
|
</DropdownTrigger>
|
||||||
|
|
||||||
|
<DropdownContent className={directionClass} style={{ lineHeight: '18px', textAlign: 'left' }}>
|
||||||
|
<ul>
|
||||||
|
{items.map(this.renderItem)}
|
||||||
|
</ul>
|
||||||
|
</DropdownContent>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
export default DropdownMenu;
|
export default DropdownMenu;
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
|
|
||||||
|
const ExtendedVideoPlayer = React.createClass({
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
src: React.PropTypes.string.isRequired,
|
||||||
|
time: React.PropTypes.number,
|
||||||
|
controls: React.PropTypes.bool.isRequired,
|
||||||
|
muted: React.PropTypes.bool.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
|
handleLoadedData () {
|
||||||
|
if (this.props.time) {
|
||||||
|
this.video.currentTime = this.props.time;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.video.addEventListener('loadeddata', this.handleLoadedData);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
this.video.removeEventListener('loadeddata', this.handleLoadedData);
|
||||||
|
},
|
||||||
|
|
||||||
|
setRef (c) {
|
||||||
|
this.video = c;
|
||||||
|
},
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<div className='extended-video-player'>
|
||||||
|
<video
|
||||||
|
ref={this.setRef}
|
||||||
|
src={this.props.src}
|
||||||
|
autoPlay
|
||||||
|
muted={this.props.muted}
|
||||||
|
controls={this.props.controls}
|
||||||
|
loop={!this.props.controls}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ExtendedVideoPlayer;
|
||||||
@@ -12,7 +12,9 @@ const IconButton = React.createClass({
|
|||||||
style: React.PropTypes.object,
|
style: React.PropTypes.object,
|
||||||
activeStyle: React.PropTypes.object,
|
activeStyle: React.PropTypes.object,
|
||||||
disabled: React.PropTypes.bool,
|
disabled: React.PropTypes.bool,
|
||||||
animate: React.PropTypes.bool
|
inverted: React.PropTypes.bool,
|
||||||
|
animate: React.PropTypes.bool,
|
||||||
|
overlay: React.PropTypes.bool
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps () {
|
getDefaultProps () {
|
||||||
@@ -20,7 +22,8 @@ const IconButton = React.createClass({
|
|||||||
size: 18,
|
size: 18,
|
||||||
active: false,
|
active: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
animate: false
|
animate: false,
|
||||||
|
overlay: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -30,19 +33,15 @@ const IconButton = React.createClass({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!this.props.disabled) {
|
if (!this.props.disabled) {
|
||||||
this.props.onClick();
|
this.props.onClick(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let style = {
|
let style = {
|
||||||
display: 'inline-block',
|
|
||||||
border: 'none',
|
|
||||||
padding: '0',
|
|
||||||
background: 'transparent',
|
|
||||||
fontSize: `${this.props.size}px`,
|
fontSize: `${this.props.size}px`,
|
||||||
width: `${this.props.size * 1.28571429}px`,
|
width: `${this.props.size * 1.28571429}px`,
|
||||||
height: `${this.props.size}px`,
|
height: `${this.props.size * 1.28571429}px`,
|
||||||
lineHeight: `${this.props.size}px`,
|
lineHeight: `${this.props.size}px`,
|
||||||
...this.props.style
|
...this.props.style
|
||||||
};
|
};
|
||||||
@@ -51,13 +50,31 @@ const IconButton = React.createClass({
|
|||||||
style = { ...style, ...this.props.activeStyle };
|
style = { ...style, ...this.props.activeStyle };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const classes = ['icon-button'];
|
||||||
|
|
||||||
|
if (this.props.active) {
|
||||||
|
classes.push('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.disabled) {
|
||||||
|
classes.push('disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.inverted) {
|
||||||
|
classes.push('inverted');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.overlay) {
|
||||||
|
classes.push('overlayed');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Motion defaultStyle={{ rotate: this.props.active ? -360 : 0 }} style={{ rotate: this.props.animate ? spring(this.props.active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
|
<Motion defaultStyle={{ rotate: this.props.active ? -360 : 0 }} style={{ rotate: this.props.animate ? spring(this.props.active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
|
||||||
{({ rotate }) =>
|
{({ rotate }) =>
|
||||||
<button
|
<button
|
||||||
aria-label={this.props.title}
|
aria-label={this.props.title}
|
||||||
title={this.props.title}
|
title={this.props.title}
|
||||||
className={`icon-button ${this.props.active ? 'active' : ''} ${this.props.disabled ? 'disabled' : ''}`}
|
className={classes.join(' ')}
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
style={style}>
|
style={style}>
|
||||||
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' />
|
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${this.props.icon}`} aria-hidden='true' />
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
|
||||||
import IconButton from './icon_button';
|
|
||||||
import { Motion, spring } from 'react-motion';
|
|
||||||
import { injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
const overlayStyle = {
|
|
||||||
position: 'fixed',
|
|
||||||
top: '0',
|
|
||||||
left: '0',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
background: 'rgba(0, 0, 0, 0.5)',
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignContent: 'center',
|
|
||||||
flexDirection: 'row',
|
|
||||||
zIndex: '9999'
|
|
||||||
};
|
|
||||||
|
|
||||||
const dialogStyle = {
|
|
||||||
color: '#282c37',
|
|
||||||
boxShadow: '0 0 30px rgba(0, 0, 0, 0.8)',
|
|
||||||
margin: 'auto',
|
|
||||||
position: 'relative'
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeStyle = {
|
|
||||||
position: 'absolute',
|
|
||||||
top: '4px',
|
|
||||||
right: '4px'
|
|
||||||
};
|
|
||||||
|
|
||||||
const Lightbox = React.createClass({
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
isVisible: React.PropTypes.bool,
|
|
||||||
onOverlayClicked: React.PropTypes.func,
|
|
||||||
onCloseClicked: React.PropTypes.func,
|
|
||||||
intl: React.PropTypes.object.isRequired,
|
|
||||||
children: React.PropTypes.node
|
|
||||||
},
|
|
||||||
|
|
||||||
mixins: [PureRenderMixin],
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
this._listener = e => {
|
|
||||||
if (this.props.isVisible && e.key === 'Escape') {
|
|
||||||
this.props.onCloseClicked();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('keyup', this._listener);
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
window.removeEventListener('keyup', this._listener);
|
|
||||||
},
|
|
||||||
|
|
||||||
stopPropagation (e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
},
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { intl, isVisible, onOverlayClicked, onCloseClicked, children } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Motion defaultStyle={{ backgroundOpacity: 0, opacity: 0, y: -400 }} style={{ backgroundOpacity: spring(isVisible ? 50 : 0), opacity: isVisible ? spring(200) : 0, y: spring(isVisible ? 0 : -400, { stiffness: 150, damping: 12 }) }}>
|
|
||||||
{({ backgroundOpacity, opacity, y }) =>
|
|
||||||
<div className='lightbox' style={{...overlayStyle, background: `rgba(0, 0, 0, ${backgroundOpacity / 100})`, display: Math.floor(backgroundOpacity) === 0 ? 'none' : 'flex'}} onClick={onOverlayClicked}>
|
|
||||||
<div style={{...dialogStyle, transform: `translateY(${y}px)`, opacity: opacity / 100 }} onClick={this.stopPropagation}>
|
|
||||||
<IconButton title={intl.formatMessage({ id: 'lightbox.close', defaultMessage: 'Close' })} icon='times' onClick={onCloseClicked} size={16} style={closeStyle} />
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</Motion>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
export default injectIntl(Lightbox);
|
|
||||||
@@ -1,15 +1,7 @@
|
|||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
const loadMoreStyle = {
|
|
||||||
display: 'block',
|
|
||||||
color: '#616b86',
|
|
||||||
textAlign: 'center',
|
|
||||||
padding: '15px',
|
|
||||||
textDecoration: 'none'
|
|
||||||
};
|
|
||||||
|
|
||||||
const LoadMore = ({ onClick }) => (
|
const LoadMore = ({ onClick }) => (
|
||||||
<a href='#' className='load-more' onClick={onClick} style={loadMoreStyle}>
|
<a href="#" className='load-more' role='button' onClick={onClick}>
|
||||||
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
|
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ const style = {
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
color: '#616b86',
|
|
||||||
paddingTop: '120px'
|
paddingTop: '120px'
|
||||||
};
|
};
|
||||||
|
|
||||||
const LoadingIndicator = () => (
|
const LoadingIndicator = () => (
|
||||||
<div style={style}>
|
<div className='loading-indicator' style={style}>
|
||||||
<FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />
|
<FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
import IconButton from './icon_button';
|
import IconButton from './icon_button';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import { isIOS } from '../is_mobile';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' }
|
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' }
|
||||||
@@ -16,8 +17,6 @@ const outerStyle = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const spoilerStyle = {
|
const spoilerStyle = {
|
||||||
background: '#000',
|
|
||||||
color: '#fff',
|
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
@@ -40,11 +39,146 @@ const spoilerSubSpanStyle = {
|
|||||||
|
|
||||||
const spoilerButtonStyle = {
|
const spoilerButtonStyle = {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '6px',
|
top: '4px',
|
||||||
left: '8px',
|
left: '4px',
|
||||||
zIndex: '100'
|
zIndex: '100'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const itemStyle = {
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
position: 'relative',
|
||||||
|
float: 'left',
|
||||||
|
border: 'none',
|
||||||
|
display: 'block'
|
||||||
|
};
|
||||||
|
|
||||||
|
const thumbStyle = {
|
||||||
|
display: 'block',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
textDecoration: 'none',
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
cursor: 'zoom-in'
|
||||||
|
};
|
||||||
|
|
||||||
|
const gifvThumbStyle = {
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: '1',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'cover',
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translateY(-50%)',
|
||||||
|
cursor: 'zoom-in'
|
||||||
|
};
|
||||||
|
|
||||||
|
const Item = React.createClass({
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
attachment: ImmutablePropTypes.map.isRequired,
|
||||||
|
index: React.PropTypes.number.isRequired,
|
||||||
|
size: React.PropTypes.number.isRequired,
|
||||||
|
onClick: React.PropTypes.func.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
|
handleClick (e) {
|
||||||
|
const { index, onClick } = this.props;
|
||||||
|
|
||||||
|
if (e.button === 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
onClick(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
e.stopPropagation();
|
||||||
|
},
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { attachment, index, size } = this.props;
|
||||||
|
|
||||||
|
let width = 50;
|
||||||
|
let height = 100;
|
||||||
|
let top = 'auto';
|
||||||
|
let left = 'auto';
|
||||||
|
let bottom = 'auto';
|
||||||
|
let right = 'auto';
|
||||||
|
|
||||||
|
if (size === 1) {
|
||||||
|
width = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size === 4 || (size === 3 && index > 0)) {
|
||||||
|
height = 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size === 2) {
|
||||||
|
if (index === 0) {
|
||||||
|
right = '2px';
|
||||||
|
} else {
|
||||||
|
left = '2px';
|
||||||
|
}
|
||||||
|
} else if (size === 3) {
|
||||||
|
if (index === 0) {
|
||||||
|
right = '2px';
|
||||||
|
} else if (index > 0) {
|
||||||
|
left = '2px';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === 1) {
|
||||||
|
bottom = '2px';
|
||||||
|
} else if (index > 1) {
|
||||||
|
top = '2px';
|
||||||
|
}
|
||||||
|
} else if (size === 4) {
|
||||||
|
if (index === 0 || index === 2) {
|
||||||
|
right = '2px';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === 1 || index === 3) {
|
||||||
|
left = '2px';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 2) {
|
||||||
|
bottom = '2px';
|
||||||
|
} else {
|
||||||
|
top = '2px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let thumbnail = '';
|
||||||
|
|
||||||
|
if (attachment.get('type') === 'image') {
|
||||||
|
thumbnail = (
|
||||||
|
<a
|
||||||
|
href={attachment.get('remote_url') ? attachment.get('remote_url') : attachment.get('url')}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
target='_blank'
|
||||||
|
style={{ background: `url(${attachment.get('preview_url')}) no-repeat center`, ...thumbStyle }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (attachment.get('type') === 'gifv') {
|
||||||
|
thumbnail = (
|
||||||
|
<video
|
||||||
|
src={attachment.get('url')}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
autoPlay={!isIOS()}
|
||||||
|
loop={true}
|
||||||
|
muted={true}
|
||||||
|
style={gifvThumbStyle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={attachment.get('id')} style={{ ...itemStyle, left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
|
||||||
|
{thumbnail}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
const MediaGallery = React.createClass({
|
const MediaGallery = React.createClass({
|
||||||
|
|
||||||
getInitialState () {
|
getInitialState () {
|
||||||
@@ -63,17 +197,12 @@ const MediaGallery = React.createClass({
|
|||||||
|
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
handleClick (index, e) {
|
handleOpen (e) {
|
||||||
if (e.button === 0) {
|
this.setState({ visible: !this.state.visible });
|
||||||
e.preventDefault();
|
|
||||||
this.props.onOpenMedia(this.props.media, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
e.stopPropagation();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
handleOpen () {
|
handleClick (index) {
|
||||||
this.setState({ visible: !this.state.visible });
|
this.props.onOpenMedia(this.props.media, index);
|
||||||
},
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
@@ -82,87 +211,31 @@ const MediaGallery = React.createClass({
|
|||||||
let children;
|
let children;
|
||||||
|
|
||||||
if (!this.state.visible) {
|
if (!this.state.visible) {
|
||||||
|
let warning;
|
||||||
|
|
||||||
if (sensitive) {
|
if (sensitive) {
|
||||||
children = (
|
warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
|
||||||
<div style={spoilerStyle} onClick={this.handleOpen}>
|
|
||||||
<span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
|
|
||||||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
children = (
|
warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
|
||||||
<div style={spoilerStyle} onClick={this.handleOpen}>
|
|
||||||
<span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
|
|
||||||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
children = (
|
||||||
|
<div role='button' tabIndex='0' style={spoilerStyle} className='media-spoiler' onClick={this.handleOpen}>
|
||||||
|
<span style={spoilerSpanStyle}>{warning}</span>
|
||||||
|
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const size = media.take(4).size;
|
const size = media.take(4).size;
|
||||||
|
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} />);
|
||||||
children = media.take(4).map((attachment, i) => {
|
|
||||||
let width = 50;
|
|
||||||
let height = 100;
|
|
||||||
let top = 'auto';
|
|
||||||
let left = 'auto';
|
|
||||||
let bottom = 'auto';
|
|
||||||
let right = 'auto';
|
|
||||||
|
|
||||||
if (size === 1) {
|
|
||||||
width = 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (size === 4 || (size === 3 && i > 0)) {
|
|
||||||
height = 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (size === 2) {
|
|
||||||
if (i === 0) {
|
|
||||||
right = '2px';
|
|
||||||
} else {
|
|
||||||
left = '2px';
|
|
||||||
}
|
|
||||||
} else if (size === 3) {
|
|
||||||
if (i === 0) {
|
|
||||||
right = '2px';
|
|
||||||
} else if (i > 0) {
|
|
||||||
left = '2px';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i === 1) {
|
|
||||||
bottom = '2px';
|
|
||||||
} else if (i > 1) {
|
|
||||||
top = '2px';
|
|
||||||
}
|
|
||||||
} else if (size === 4) {
|
|
||||||
if (i === 0 || i === 2) {
|
|
||||||
right = '2px';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i === 1 || i === 3) {
|
|
||||||
left = '2px';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i < 2) {
|
|
||||||
bottom = '2px';
|
|
||||||
} else {
|
|
||||||
top = '2px';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={attachment.get('id')} style={{ boxSizing: 'border-box', position: 'relative', left: left, top: top, right: right, bottom: bottom, float: 'left', border: 'none', display: 'block', width: `${width}%`, height: `${height}%` }}>
|
|
||||||
<a href={attachment.get('remote_url') ? attachment.get('remote_url') : attachment.get('url')} onClick={this.handleClick.bind(this, i)} target='_blank' style={{ display: 'block', width: '100%', height: '100%', background: `url(${attachment.get('preview_url')}) no-repeat center`, textDecoration: 'none', backgroundSize: 'cover', cursor: 'zoom-in' }} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ ...outerStyle, height: `${this.props.height}px` }}>
|
<div style={{ ...outerStyle, height: `${this.props.height}px` }}>
|
||||||
<div style={spoilerButtonStyle} >
|
<div style={{ ...spoilerButtonStyle, display: !this.state.visible ? 'none' : 'block' }}>
|
||||||
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleOpen} />
|
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
const style = {
|
|
||||||
textAlign: 'center',
|
|
||||||
fontSize: '16px',
|
|
||||||
fontWeight: '500',
|
|
||||||
color: '#616b86',
|
|
||||||
paddingTop: '120px'
|
|
||||||
};
|
|
||||||
|
|
||||||
const MissingIndicator = () => (
|
const MissingIndicator = () => (
|
||||||
<div style={style}>
|
<div className='missing-indicator'>
|
||||||
<FormattedMessage id='missing_indicator.label' defaultMessage='Not found' />
|
<FormattedMessage id='missing_indicator.label' defaultMessage='Not found' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ const Permalink = React.createClass({
|
|||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
href: React.PropTypes.string.isRequired,
|
href: React.PropTypes.string.isRequired,
|
||||||
to: React.PropTypes.string.isRequired
|
to: React.PropTypes.string.isRequired,
|
||||||
|
children: React.PropTypes.node
|
||||||
},
|
},
|
||||||
|
|
||||||
handleClick (e) {
|
handleClick (e) {
|
||||||
|
|||||||
@@ -9,16 +9,7 @@ import StatusContent from './status_content';
|
|||||||
import StatusActionBar from './status_action_bar';
|
import StatusActionBar from './status_action_bar';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import emojify from '../emoji';
|
import emojify from '../emoji';
|
||||||
import escapeTextContentForBrowser from 'react/lib/escapeTextContentForBrowser';
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
|
|
||||||
const outerStyle = {
|
|
||||||
padding: '8px 10px',
|
|
||||||
paddingLeft: '68px',
|
|
||||||
position: 'relative',
|
|
||||||
minHeight: '48px',
|
|
||||||
borderBottom: '1px solid #363c4b',
|
|
||||||
cursor: 'default'
|
|
||||||
};
|
|
||||||
|
|
||||||
const Status = React.createClass({
|
const Status = React.createClass({
|
||||||
|
|
||||||
@@ -34,8 +25,10 @@ const Status = React.createClass({
|
|||||||
onReblog: React.PropTypes.func,
|
onReblog: React.PropTypes.func,
|
||||||
onDelete: React.PropTypes.func,
|
onDelete: React.PropTypes.func,
|
||||||
onOpenMedia: React.PropTypes.func,
|
onOpenMedia: React.PropTypes.func,
|
||||||
|
onOpenVideo: React.PropTypes.func,
|
||||||
onBlock: React.PropTypes.func,
|
onBlock: React.PropTypes.func,
|
||||||
me: React.PropTypes.number,
|
me: React.PropTypes.number,
|
||||||
|
boostModal: React.PropTypes.bool,
|
||||||
muted: React.PropTypes.bool
|
muted: React.PropTypes.bool
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -55,7 +48,7 @@ const Status = React.createClass({
|
|||||||
|
|
||||||
render () {
|
render () {
|
||||||
let media = '';
|
let media = '';
|
||||||
const { status, now, ...other } = this.props;
|
const { status, ...other } = this.props;
|
||||||
|
|
||||||
if (status === null) {
|
if (status === null) {
|
||||||
return <div />;
|
return <div />;
|
||||||
@@ -72,9 +65,9 @@ const Status = React.createClass({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ cursor: 'default' }}>
|
<div style={{ cursor: 'default' }}>
|
||||||
<div style={{ marginLeft: '68px', color: '#616b86', padding: '8px 0', paddingBottom: '2px', fontSize: '14px', position: 'relative' }}>
|
<div className='status__prepend'>
|
||||||
<div style={{ position: 'absolute', 'left': '-26px'}}><i className='fa fa-fw fa-retweet' /></div>
|
<div style={{ position: 'absolute', 'left': '-26px'}}><i className='fa fa-fw fa-retweet' /></div>
|
||||||
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} reblogged' values={{ name: <a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong style={{ color: '#616b86'}} dangerouslySetInnerHTML={displayNameHTML} /></a> }} />
|
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} reblogged' values={{ name: <a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={displayNameHTML} /></a> }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Status {...other} wrapped={true} status={status.get('reblog')} />
|
<Status {...other} wrapped={true} status={status.get('reblog')} />
|
||||||
@@ -84,22 +77,22 @@ const Status = React.createClass({
|
|||||||
|
|
||||||
if (status.get('media_attachments').size > 0 && !this.props.muted) {
|
if (status.get('media_attachments').size > 0 && !this.props.muted) {
|
||||||
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||||
media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} />;
|
media = <VideoPlayer media={status.getIn(['media_attachments', 0])} sensitive={status.get('sensitive')} onOpenVideo={this.props.onOpenVideo} />;
|
||||||
} else {
|
} else {
|
||||||
media = <MediaGallery media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />;
|
media = <MediaGallery media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={this.props.muted ? 'muted' : ''} style={outerStyle}>
|
<div className={this.props.muted ? 'status muted' : 'status'}>
|
||||||
<div style={{ fontSize: '15px' }}>
|
<div style={{ fontSize: '15px' }}>
|
||||||
<div style={{ float: 'right', fontSize: '14px' }}>
|
<div style={{ float: 'right', fontSize: '14px' }}>
|
||||||
<a href={status.get('url')} className='status__relative-time' style={{ color: '#616b86' }} target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} now={now} /></a>
|
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', color: '#616b86' }}>
|
<a onClick={this.handleAccountClick.bind(this, status.getIn(['account', 'id']))} href={status.getIn(['account', 'url'])} className='status__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px' }}>
|
||||||
<div className='status__avatar' style={{ position: 'absolute', left: '10px', top: '10px', width: '48px', height: '48px' }}>
|
<div className='status__avatar' style={{ position: 'absolute', left: '10px', top: '10px', width: '48px', height: '48px' }}>
|
||||||
<Avatar src={status.getIn(['account', 'avatar'])} size={48} />
|
<Avatar src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} size={48} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DisplayName account={status.get('account')} />
|
<DisplayName account={status.get('account')} />
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import { defineMessages, injectIntl } from 'react-intl';
|
|||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||||
mention: { id: 'status.mention', defaultMessage: 'Mention' },
|
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||||
block: { id: 'account.block', defaultMessage: 'Block' },
|
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
||||||
|
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
|
||||||
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||||
reblog: { id: 'status.reblog', defaultMessage: 'Reblog' },
|
reblog: { id: 'status.reblog', defaultMessage: 'Reblog' },
|
||||||
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
||||||
open: { id: 'status.open', defaultMessage: 'Expand' }
|
open: { id: 'status.open', defaultMessage: 'Expand this status' },
|
||||||
|
report: { id: 'status.report', defaultMessage: 'Report @{name}' }
|
||||||
});
|
});
|
||||||
|
|
||||||
const StatusActionBar = React.createClass({
|
const StatusActionBar = React.createClass({
|
||||||
@@ -27,7 +29,11 @@ const StatusActionBar = React.createClass({
|
|||||||
onReblog: React.PropTypes.func,
|
onReblog: React.PropTypes.func,
|
||||||
onDelete: React.PropTypes.func,
|
onDelete: React.PropTypes.func,
|
||||||
onMention: React.PropTypes.func,
|
onMention: React.PropTypes.func,
|
||||||
onBlock: React.PropTypes.func
|
onMute: React.PropTypes.func,
|
||||||
|
onBlock: React.PropTypes.func,
|
||||||
|
onReport: React.PropTypes.func,
|
||||||
|
me: React.PropTypes.number.isRequired,
|
||||||
|
intl: React.PropTypes.object.isRequired
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
@@ -40,8 +46,8 @@ const StatusActionBar = React.createClass({
|
|||||||
this.props.onFavourite(this.props.status);
|
this.props.onFavourite(this.props.status);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleReblogClick () {
|
handleReblogClick (e) {
|
||||||
this.props.onReblog(this.props.status);
|
this.props.onReblog(this.props.status, e);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleDeleteClick () {
|
handleDeleteClick () {
|
||||||
@@ -52,6 +58,10 @@ const StatusActionBar = React.createClass({
|
|||||||
this.props.onMention(this.props.status.get('account'), this.context.router);
|
this.props.onMention(this.props.status.get('account'), this.context.router);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleMuteClick () {
|
||||||
|
this.props.onMute(this.props.status.get('account'));
|
||||||
|
},
|
||||||
|
|
||||||
handleBlockClick () {
|
handleBlockClick () {
|
||||||
this.props.onBlock(this.props.status.get('account'));
|
this.props.onBlock(this.props.status.get('account'));
|
||||||
},
|
},
|
||||||
@@ -60,23 +70,36 @@ const StatusActionBar = React.createClass({
|
|||||||
this.context.router.push(`/statuses/${this.props.status.get('id')}`);
|
this.context.router.push(`/statuses/${this.props.status.get('id')}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleReport () {
|
||||||
|
this.props.onReport(this.props.status);
|
||||||
|
this.context.router.push('/report');
|
||||||
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, me, intl } = this.props;
|
const { status, me, intl } = this.props;
|
||||||
let menu = [];
|
let menu = [];
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
|
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
|
||||||
|
menu.push(null);
|
||||||
|
|
||||||
if (status.getIn(['account', 'id']) === me) {
|
if (status.getIn(['account', 'id']) === me) {
|
||||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||||
} else {
|
} else {
|
||||||
menu.push({ text: intl.formatMessage(messages.mention), action: this.handleMentionClick });
|
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.block), action: this.handleBlockClick });
|
menu.push(null);
|
||||||
|
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
|
||||||
|
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
|
||||||
|
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let reblogIcon = 'retweet';
|
||||||
|
if (status.get('visibility') === 'direct') reblogIcon = 'envelope';
|
||||||
|
else if (status.get('visibility') === 'private') reblogIcon = 'lock';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginTop: '10px', overflow: 'hidden' }}>
|
<div style={{ marginTop: '10px', overflow: 'hidden' }}>
|
||||||
<div style={{ float: 'left', marginRight: '18px'}}><IconButton title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReplyClick} /></div>
|
<div style={{ float: 'left', marginRight: '18px'}}><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div>
|
||||||
<div style={{ float: 'left', marginRight: '18px'}}><IconButton disabled={status.get('visibility') === 'private'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon={status.get('visibility') === 'private' ? 'lock' : 'retweet'} onClick={this.handleReblogClick} /></div>
|
<div style={{ float: 'left', marginRight: '18px'}}><IconButton disabled={status.get('visibility') === 'private' || status.get('visibility') === 'direct'} active={status.get('reblogged')} title={intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
|
||||||
<div style={{ float: 'left', marginRight: '18px'}}><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div>
|
<div style={{ float: 'left', marginRight: '18px'}}><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div>
|
||||||
|
|
||||||
<div style={{ width: '18px', height: '18px', float: 'left' }}>
|
<div style={{ width: '18px', height: '18px', float: 'left' }}>
|
||||||
|
|||||||
@@ -1,21 +1,11 @@
|
|||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
import escapeTextContentForBrowser from 'react/lib/escapeTextContentForBrowser';
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
import emojify from '../emoji';
|
import emojify from '../emoji';
|
||||||
|
import { isRtl } from '../rtl';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import Permalink from './permalink';
|
import Permalink from './permalink';
|
||||||
|
|
||||||
const spoilerStyle = {
|
|
||||||
display: 'inline-block',
|
|
||||||
borderRadius: '2px',
|
|
||||||
color: '#363c4b',
|
|
||||||
fontWeight: '500',
|
|
||||||
fontSize: '11px',
|
|
||||||
padding: '0px 6px',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
lineHeight: 'inherit'
|
|
||||||
};
|
|
||||||
|
|
||||||
const StatusContent = React.createClass({
|
const StatusContent = React.createClass({
|
||||||
|
|
||||||
contextTypes: {
|
contextTypes: {
|
||||||
@@ -42,10 +32,11 @@ const StatusContent = React.createClass({
|
|||||||
for (var i = 0; i < links.length; ++i) {
|
for (var i = 0; i < links.length; ++i) {
|
||||||
let link = links[i];
|
let link = links[i];
|
||||||
let mention = this.props.status.get('mentions').find(item => link.href === item.get('url'));
|
let mention = this.props.status.get('mentions').find(item => link.href === item.get('url'));
|
||||||
let media = this.props.status.get('media_attachments').find(item => link.href === item.get('text_url') || link.href === item.get('remote_url'));
|
let media = this.props.status.get('media_attachments').find(item => link.href === item.get('text_url') || (item.get('remote_url').length > 0 && link.href === item.get('remote_url')));
|
||||||
|
|
||||||
if (mention) {
|
if (mention) {
|
||||||
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
|
||||||
|
link.setAttribute('title', mention.get('acct'));
|
||||||
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
|
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
|
||||||
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
|
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
|
||||||
} else if (media) {
|
} else if (media) {
|
||||||
@@ -53,6 +44,7 @@ const StatusContent = React.createClass({
|
|||||||
} else {
|
} else {
|
||||||
link.setAttribute('target', '_blank');
|
link.setAttribute('target', '_blank');
|
||||||
link.setAttribute('rel', 'noopener');
|
link.setAttribute('rel', 'noopener');
|
||||||
|
link.setAttribute('title', link.href);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -92,7 +84,8 @@ const StatusContent = React.createClass({
|
|||||||
this.startXY = null;
|
this.startXY = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSpoilerClick () {
|
handleSpoilerClick (e) {
|
||||||
|
e.preventDefault();
|
||||||
this.setState({ hidden: !this.state.hidden });
|
this.setState({ hidden: !this.state.hidden });
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -102,6 +95,11 @@ const StatusContent = React.createClass({
|
|||||||
|
|
||||||
const content = { __html: emojify(status.get('content')) };
|
const content = { __html: emojify(status.get('content')) };
|
||||||
const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) };
|
const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) };
|
||||||
|
const directionStyle = { direction: 'ltr' };
|
||||||
|
|
||||||
|
if (isRtl(status.get('content'))) {
|
||||||
|
directionStyle.direction = 'rtl';
|
||||||
|
}
|
||||||
|
|
||||||
if (status.get('spoiler_text').length > 0) {
|
if (status.get('spoiler_text').length > 0) {
|
||||||
let mentionsPlaceholder = '';
|
let mentionsPlaceholder = '';
|
||||||
@@ -121,21 +119,29 @@ const StatusContent = React.createClass({
|
|||||||
return (
|
return (
|
||||||
<div className='status__content' style={{ cursor: 'pointer' }} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
|
<div className='status__content' style={{ cursor: 'pointer' }} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
|
||||||
<p style={{ marginBottom: hidden && status.get('mentions').size === 0 ? '0px' : '' }} >
|
<p style={{ marginBottom: hidden && status.get('mentions').size === 0 ? '0px' : '' }} >
|
||||||
<span dangerouslySetInnerHTML={spoilerContent} /> <a className='status__content__spoiler-link' style={spoilerStyle} onClick={this.handleSpoilerClick}>{toggleText}</a>
|
<span dangerouslySetInnerHTML={spoilerContent} /> <a tabIndex='0' className='status__content__spoiler-link' role='button' onClick={this.handleSpoilerClick}>{toggleText}</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{mentionsPlaceholder}
|
{mentionsPlaceholder}
|
||||||
|
|
||||||
<div style={{ display: hidden ? 'none' : 'block' }} dangerouslySetInnerHTML={content} />
|
<div style={{ display: hidden ? 'none' : 'block', ...directionStyle }} dangerouslySetInnerHTML={content} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
} else if (this.props.onClick) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='status__content'
|
||||||
|
style={{ cursor: 'pointer', ...directionStyle }}
|
||||||
|
onMouseDown={this.handleMouseDown}
|
||||||
|
onMouseUp={this.handleMouseUp}
|
||||||
|
dangerouslySetInnerHTML={content}
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='status__content'
|
className='status__content'
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ ...directionStyle }}
|
||||||
onMouseDown={this.handleMouseDown}
|
|
||||||
onMouseUp={this.handleMouseUp}
|
|
||||||
dangerouslySetInnerHTML={content}
|
dangerouslySetInnerHTML={content}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ const StatusList = React.createClass({
|
|||||||
onScroll: React.PropTypes.func,
|
onScroll: React.PropTypes.func,
|
||||||
trackScroll: React.PropTypes.bool,
|
trackScroll: React.PropTypes.bool,
|
||||||
isLoading: React.PropTypes.bool,
|
isLoading: React.PropTypes.bool,
|
||||||
prepend: React.PropTypes.node
|
isUnread: React.PropTypes.bool,
|
||||||
|
hasMore: React.PropTypes.bool,
|
||||||
|
prepend: React.PropTypes.node,
|
||||||
|
emptyMessage: React.PropTypes.node
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps () {
|
getDefaultProps () {
|
||||||
@@ -71,27 +74,43 @@ const StatusList = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { statusIds, onScrollToBottom, trackScroll, isLoading, prepend } = this.props;
|
const { statusIds, onScrollToBottom, trackScroll, isLoading, isUnread, hasMore, prepend, emptyMessage } = this.props;
|
||||||
|
|
||||||
let loadMore = '';
|
let loadMore = '';
|
||||||
|
let scrollableArea = '';
|
||||||
|
let unread = '';
|
||||||
|
|
||||||
if (!isLoading && statusIds.size > 0) {
|
if (!isLoading && statusIds.size > 0 && hasMore) {
|
||||||
loadMore = <LoadMore onClick={this.handleLoadMore} />;
|
loadMore = <LoadMore onClick={this.handleLoadMore} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollableArea = (
|
if (isUnread) {
|
||||||
<div className='scrollable' ref={this.setRef}>
|
unread = <div className='status-list__unread-indicator' />;
|
||||||
<div>
|
}
|
||||||
{prepend}
|
|
||||||
|
|
||||||
{statusIds.map((statusId) => {
|
if (isLoading || statusIds.size > 0 || !emptyMessage) {
|
||||||
return <StatusContainer key={statusId} id={statusId} />;
|
scrollableArea = (
|
||||||
})}
|
<div className='scrollable' ref={this.setRef}>
|
||||||
|
{unread}
|
||||||
|
|
||||||
{loadMore}
|
<div>
|
||||||
|
{prepend}
|
||||||
|
|
||||||
|
{statusIds.map((statusId) => {
|
||||||
|
return <StatusContainer key={statusId} id={statusId} />;
|
||||||
|
})}
|
||||||
|
|
||||||
|
{loadMore}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
} else {
|
||||||
|
scrollableArea = (
|
||||||
|
<div className='empty-column-indicator' ref={this.setRef}>
|
||||||
|
{emptyMessage}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (trackScroll) {
|
if (trackScroll) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
import IconButton from './icon_button';
|
import IconButton from './icon_button';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import { isIOS } from '../is_mobile';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
toggle_sound: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' },
|
toggle_sound: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' },
|
||||||
toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' }
|
toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' },
|
||||||
|
expand_video: { id: 'video_player.expand', defaultMessage: 'Expand video' },
|
||||||
|
expand_video: { id: 'video_player.video_error', defaultMessage: 'Video could not be played' }
|
||||||
});
|
});
|
||||||
|
|
||||||
const videoStyle = {
|
const videoStyle = {
|
||||||
@@ -20,16 +23,16 @@ const videoStyle = {
|
|||||||
|
|
||||||
const muteStyle = {
|
const muteStyle = {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '10px',
|
top: '4px',
|
||||||
right: '10px',
|
right: '4px',
|
||||||
|
color: 'white',
|
||||||
|
textShadow: "0px 1px 1px black, 1px 0px 1px black",
|
||||||
opacity: '0.8',
|
opacity: '0.8',
|
||||||
zIndex: '5'
|
zIndex: '5'
|
||||||
};
|
};
|
||||||
|
|
||||||
const spoilerStyle = {
|
const coverStyle = {
|
||||||
marginTop: '8px',
|
marginTop: '8px',
|
||||||
background: '#000',
|
|
||||||
color: '#fff',
|
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
@@ -53,8 +56,19 @@ const spoilerSubSpanStyle = {
|
|||||||
|
|
||||||
const spoilerButtonStyle = {
|
const spoilerButtonStyle = {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '6px',
|
top: '4px',
|
||||||
left: '8px',
|
left: '4px',
|
||||||
|
color: 'white',
|
||||||
|
textShadow: "0px 1px 1px black, 1px 0px 1px black",
|
||||||
|
zIndex: '100'
|
||||||
|
};
|
||||||
|
|
||||||
|
const expandButtonStyle = {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '4px',
|
||||||
|
right: '4px',
|
||||||
|
color: 'white',
|
||||||
|
textShadow: "0px 1px 1px black, 1px 0px 1px black",
|
||||||
zIndex: '100'
|
zIndex: '100'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -63,12 +77,15 @@ const VideoPlayer = React.createClass({
|
|||||||
media: ImmutablePropTypes.map.isRequired,
|
media: ImmutablePropTypes.map.isRequired,
|
||||||
width: React.PropTypes.number,
|
width: React.PropTypes.number,
|
||||||
height: React.PropTypes.number,
|
height: React.PropTypes.number,
|
||||||
sensitive: React.PropTypes.bool
|
sensitive: React.PropTypes.bool,
|
||||||
|
intl: React.PropTypes.object.isRequired,
|
||||||
|
autoplay: React.PropTypes.bool,
|
||||||
|
onOpenVideo: React.PropTypes.func.isRequired
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps () {
|
getDefaultProps () {
|
||||||
return {
|
return {
|
||||||
width: 196,
|
width: 239,
|
||||||
height: 110
|
height: 110
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -77,7 +94,9 @@ const VideoPlayer = React.createClass({
|
|||||||
return {
|
return {
|
||||||
visible: !this.props.sensitive,
|
visible: !this.props.sensitive,
|
||||||
preview: true,
|
preview: true,
|
||||||
muted: true
|
muted: true,
|
||||||
|
hasAudio: true,
|
||||||
|
videoError: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -110,19 +129,81 @@ const VideoPlayer = React.createClass({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleExpand () {
|
||||||
|
this.video.pause();
|
||||||
|
this.props.onOpenVideo(this.props.media, this.video.currentTime);
|
||||||
|
},
|
||||||
|
|
||||||
|
setRef (c) {
|
||||||
|
this.video = c;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleLoadedData () {
|
||||||
|
if (('WebkitAppearance' in document.documentElement.style && this.video.audioTracks.length === 0) || this.video.mozHasAudio === false) {
|
||||||
|
this.setState({ hasAudio: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleVideoError () {
|
||||||
|
this.setState({ videoError: true });
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
if (!this.video) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.video.addEventListener('loadeddata', this.handleLoadedData);
|
||||||
|
this.video.addEventListener('error', this.handleVideoError);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidUpdate () {
|
||||||
|
if (!this.video) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.video.addEventListener('loadeddata', this.handleLoadedData);
|
||||||
|
this.video.addEventListener('error', this.handleVideoError);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
if (!this.video) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.video.removeEventListener('loadeddata', this.handleLoadedData);
|
||||||
|
this.video.removeEventListener('error', this.handleVideoError);
|
||||||
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { media, intl, width, height, sensitive } = this.props;
|
const { media, intl, width, height, sensitive, autoplay } = this.props;
|
||||||
|
|
||||||
let spoilerButton = (
|
let spoilerButton = (
|
||||||
<div style={spoilerButtonStyle} >
|
<div style={{...spoilerButtonStyle, display: !this.state.visible ? 'none' : 'block'}} >
|
||||||
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleVisibility} />
|
<IconButton overlay title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleVisibility} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let expandButton = (
|
||||||
|
<div style={expandButtonStyle} >
|
||||||
|
<IconButton overlay title={intl.formatMessage(messages.expand_video)} icon='expand' onClick={this.handleExpand} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
let muteButton = '';
|
||||||
|
|
||||||
|
if (this.state.hasAudio) {
|
||||||
|
muteButton = (
|
||||||
|
<div style={muteStyle}>
|
||||||
|
<IconButton overlay title={intl.formatMessage(messages.toggle_sound)} icon={this.state.muted ? 'volume-off' : 'volume-up'} onClick={this.handleClick} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.state.visible) {
|
if (!this.state.visible) {
|
||||||
if (sensitive) {
|
if (sensitive) {
|
||||||
return (
|
return (
|
||||||
<div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} onClick={this.handleVisibility}>
|
<div role='button' tabIndex='0' style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
|
||||||
{spoilerButton}
|
{spoilerButton}
|
||||||
<span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
|
<span style={spoilerSpanStyle}><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
|
||||||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||||
@@ -130,7 +211,7 @@ const VideoPlayer = React.createClass({
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div style={{...spoilerStyle, width: `${width}px`, height: `${height}px` }} onClick={this.handleOpen}>
|
<div role='button' tabIndex='0' style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='media-spoiler' onClick={this.handleVisibility}>
|
||||||
{spoilerButton}
|
{spoilerButton}
|
||||||
<span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
|
<span style={spoilerSpanStyle}><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
|
||||||
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
<span style={spoilerSubSpanStyle}><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
||||||
@@ -139,20 +220,29 @@ const VideoPlayer = React.createClass({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.preview) {
|
if (this.state.preview && !autoplay) {
|
||||||
return (
|
return (
|
||||||
<div style={{ cursor: 'pointer', position: 'relative', marginTop: '8px', width: `${width}px`, height: `${height}px`, background: `url(${media.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }} onClick={this.handleOpen}>
|
<div role='button' tabIndex='0' style={{ cursor: 'pointer', position: 'relative', marginTop: '8px', width: `${width}px`, height: `${height}px`, background: `url(${media.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }} onClick={this.handleOpen}>
|
||||||
{spoilerButton}
|
{spoilerButton}
|
||||||
<div style={{ position: 'absolute', top: '50%', left: '50%', fontSize: '36px', transform: 'translate(-50%, -50%)', padding: '5px', borderRadius: '100px', color: 'rgba(255, 255, 255, 0.8)' }}><i className='fa fa-play' /></div>
|
<div style={{ position: 'absolute', top: '50%', left: '50%', fontSize: '36px', transform: 'translate(-50%, -50%)', padding: '5px', borderRadius: '100px', color: 'rgba(255, 255, 255, 0.8)' }}><i className='fa fa-play' /></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.state.videoError) {
|
||||||
|
return (
|
||||||
|
<div style={{...coverStyle, width: `${width}px`, height: `${height}px` }} className='video-error-cover' >
|
||||||
|
<span style={spoilerSpanStyle}><FormattedMessage id='video_player.video_error' defaultMessage='Video could not be played' /></span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}>
|
<div style={{ cursor: 'default', marginTop: '8px', overflow: 'hidden', width: `${width}px`, height: `${height}px`, boxSizing: 'border-box', background: '#000', position: 'relative' }}>
|
||||||
{spoilerButton}
|
{spoilerButton}
|
||||||
<div style={muteStyle}><IconButton title={intl.formatMessage(messages.toggle_sound)} icon={this.state.muted ? 'volume-off' : 'volume-up'} onClick={this.handleClick} /></div>
|
{muteButton}
|
||||||
<video src={media.get('url')} autoPlay='true' loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
|
{expandButton}
|
||||||
|
<video role='button' tabIndex='0' ref={this.setRef} src={media.get('url')} autoPlay={!isIOS()} loop={true} muted={this.state.muted} style={videoStyle} onClick={this.handleVideoClick} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import {
|
|||||||
followAccount,
|
followAccount,
|
||||||
unfollowAccount,
|
unfollowAccount,
|
||||||
blockAccount,
|
blockAccount,
|
||||||
unblockAccount
|
unblockAccount,
|
||||||
|
muteAccount,
|
||||||
|
unmuteAccount,
|
||||||
} from '../actions/accounts';
|
} from '../actions/accounts';
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
@@ -34,6 +36,14 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
} else {
|
} else {
|
||||||
dispatch(blockAccount(account.get('id')));
|
dispatch(blockAccount(account.get('id')));
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onMute (account) {
|
||||||
|
if (account.getIn(['relationship', 'muting'])) {
|
||||||
|
dispatch(unmuteAccount(account.get('id')));
|
||||||
|
} else {
|
||||||
|
dispatch(muteAccount(account.get('id')));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import {
|
|||||||
refreshTimelineSuccess,
|
refreshTimelineSuccess,
|
||||||
updateTimeline,
|
updateTimeline,
|
||||||
deleteFromTimelines,
|
deleteFromTimelines,
|
||||||
refreshTimeline
|
refreshTimeline,
|
||||||
|
connectTimeline,
|
||||||
|
disconnectTimeline
|
||||||
} from '../actions/timelines';
|
} from '../actions/timelines';
|
||||||
import { updateNotifications } from '../actions/notifications';
|
import { showOnboardingOnce } from '../actions/onboarding';
|
||||||
|
import { updateNotifications, refreshNotifications } from '../actions/notifications';
|
||||||
import createBrowserHistory from 'history/lib/createBrowserHistory';
|
import createBrowserHistory from 'history/lib/createBrowserHistory';
|
||||||
import {
|
import {
|
||||||
applyRouterMiddleware,
|
applyRouterMiddleware,
|
||||||
@@ -21,6 +24,7 @@ import UI from '../features/ui';
|
|||||||
import Status from '../features/status';
|
import Status from '../features/status';
|
||||||
import GettingStarted from '../features/getting_started';
|
import GettingStarted from '../features/getting_started';
|
||||||
import PublicTimeline from '../features/public_timeline';
|
import PublicTimeline from '../features/public_timeline';
|
||||||
|
import CommunityTimeline from '../features/community_timeline';
|
||||||
import AccountTimeline from '../features/account_timeline';
|
import AccountTimeline from '../features/account_timeline';
|
||||||
import HomeTimeline from '../features/home_timeline';
|
import HomeTimeline from '../features/home_timeline';
|
||||||
import Compose from '../features/compose';
|
import Compose from '../features/compose';
|
||||||
@@ -34,27 +38,55 @@ import FollowRequests from '../features/follow_requests';
|
|||||||
import GenericNotFound from '../features/generic_not_found';
|
import GenericNotFound from '../features/generic_not_found';
|
||||||
import FavouritedStatuses from '../features/favourited_statuses';
|
import FavouritedStatuses from '../features/favourited_statuses';
|
||||||
import Blocks from '../features/blocks';
|
import Blocks from '../features/blocks';
|
||||||
|
import Mutes from '../features/mutes';
|
||||||
|
import Report from '../features/report';
|
||||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||||
import en from 'react-intl/locale-data/en';
|
import en from 'react-intl/locale-data/en';
|
||||||
import de from 'react-intl/locale-data/de';
|
import de from 'react-intl/locale-data/de';
|
||||||
|
import eo from 'react-intl/locale-data/eo';
|
||||||
import es from 'react-intl/locale-data/es';
|
import es from 'react-intl/locale-data/es';
|
||||||
|
import fi from 'react-intl/locale-data/fi';
|
||||||
import fr from 'react-intl/locale-data/fr';
|
import fr from 'react-intl/locale-data/fr';
|
||||||
import pt from 'react-intl/locale-data/pt';
|
|
||||||
import hu from 'react-intl/locale-data/hu';
|
import hu from 'react-intl/locale-data/hu';
|
||||||
|
import ja from 'react-intl/locale-data/ja';
|
||||||
|
import pt from 'react-intl/locale-data/pt';
|
||||||
|
import nl from 'react-intl/locale-data/nl';
|
||||||
|
import no from 'react-intl/locale-data/no';
|
||||||
|
import ru from 'react-intl/locale-data/ru';
|
||||||
import uk from 'react-intl/locale-data/uk';
|
import uk from 'react-intl/locale-data/uk';
|
||||||
|
import zh from 'react-intl/locale-data/zh';
|
||||||
|
import bg from 'react-intl/locale-data/bg';
|
||||||
|
import { localeData as zh_hk } from '../locales/zh-hk';
|
||||||
import getMessagesForLocale from '../locales';
|
import getMessagesForLocale from '../locales';
|
||||||
import { hydrateStore } from '../actions/store';
|
import { hydrateStore } from '../actions/store';
|
||||||
import createStream from '../stream';
|
import createStream from '../stream';
|
||||||
|
|
||||||
const store = configureStore();
|
const store = configureStore();
|
||||||
|
const initialState = JSON.parse(document.getElementById("initial-state").textContent);
|
||||||
store.dispatch(hydrateStore(window.INITIAL_STATE));
|
store.dispatch(hydrateStore(initialState));
|
||||||
|
|
||||||
const browserHistory = useRouterHistory(createBrowserHistory)({
|
const browserHistory = useRouterHistory(createBrowserHistory)({
|
||||||
basename: '/web'
|
basename: '/web'
|
||||||
});
|
});
|
||||||
|
|
||||||
addLocaleData([...en, ...de, ...es, ...fr, ...pt, ...hu, ...uk]);
|
addLocaleData([
|
||||||
|
...en,
|
||||||
|
...de,
|
||||||
|
...eo,
|
||||||
|
...es,
|
||||||
|
...fi,
|
||||||
|
...fr,
|
||||||
|
...hu,
|
||||||
|
...ja,
|
||||||
|
...pt,
|
||||||
|
...nl,
|
||||||
|
...no,
|
||||||
|
...ru,
|
||||||
|
...uk,
|
||||||
|
...zh,
|
||||||
|
...zh_hk,
|
||||||
|
...bg,
|
||||||
|
]);
|
||||||
|
|
||||||
const Mastodon = React.createClass({
|
const Mastodon = React.createClass({
|
||||||
|
|
||||||
@@ -64,9 +96,18 @@ const Mastodon = React.createClass({
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { locale } = this.props;
|
const { locale } = this.props;
|
||||||
|
const streamingAPIBaseURL = store.getState().getIn(['meta', 'streaming_api_base_url']);
|
||||||
const accessToken = store.getState().getIn(['meta', 'access_token']);
|
const accessToken = store.getState().getIn(['meta', 'access_token']);
|
||||||
|
|
||||||
this.subscription = createStream(accessToken, 'user', {
|
this.subscription = createStream(streamingAPIBaseURL, accessToken, 'user', {
|
||||||
|
|
||||||
|
connected () {
|
||||||
|
store.dispatch(connectTimeline('home'));
|
||||||
|
},
|
||||||
|
|
||||||
|
disconnected () {
|
||||||
|
store.dispatch(disconnectTimeline('home'));
|
||||||
|
},
|
||||||
|
|
||||||
received (data) {
|
received (data) {
|
||||||
switch(data.event) {
|
switch(data.event) {
|
||||||
@@ -80,6 +121,12 @@ const Mastodon = React.createClass({
|
|||||||
store.dispatch(updateNotifications(JSON.parse(data.payload), getMessagesForLocale(locale), locale));
|
store.dispatch(updateNotifications(JSON.parse(data.payload), getMessagesForLocale(locale), locale));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
reconnected () {
|
||||||
|
store.dispatch(connectTimeline('home'));
|
||||||
|
store.dispatch(refreshTimeline('home'));
|
||||||
|
store.dispatch(refreshNotifications());
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -88,6 +135,8 @@ const Mastodon = React.createClass({
|
|||||||
if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') {
|
if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') {
|
||||||
Notification.requestPermission();
|
Notification.requestPermission();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
store.dispatch(showOnboardingOnce());
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
@@ -110,6 +159,7 @@ const Mastodon = React.createClass({
|
|||||||
<Route path='getting-started' component={GettingStarted} />
|
<Route path='getting-started' component={GettingStarted} />
|
||||||
<Route path='timelines/home' component={HomeTimeline} />
|
<Route path='timelines/home' component={HomeTimeline} />
|
||||||
<Route path='timelines/public' component={PublicTimeline} />
|
<Route path='timelines/public' component={PublicTimeline} />
|
||||||
|
<Route path='timelines/public/local' component={CommunityTimeline} />
|
||||||
<Route path='timelines/tag/:id' component={HashtagTimeline} />
|
<Route path='timelines/tag/:id' component={HashtagTimeline} />
|
||||||
|
|
||||||
<Route path='notifications' component={Notifications} />
|
<Route path='notifications' component={Notifications} />
|
||||||
@@ -126,6 +176,8 @@ const Mastodon = React.createClass({
|
|||||||
|
|
||||||
<Route path='follow_requests' component={FollowRequests} />
|
<Route path='follow_requests' component={FollowRequests} />
|
||||||
<Route path='blocks' component={Blocks} />
|
<Route path='blocks' component={Blocks} />
|
||||||
|
<Route path='mutes' component={Mutes} />
|
||||||
|
<Route path='report' component={Report} />
|
||||||
|
|
||||||
<Route path='*' component={GenericNotFound} />
|
<Route path='*' component={GenericNotFound} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -11,51 +11,23 @@ import {
|
|||||||
unreblog,
|
unreblog,
|
||||||
unfavourite
|
unfavourite
|
||||||
} from '../actions/interactions';
|
} from '../actions/interactions';
|
||||||
import { blockAccount } from '../actions/accounts';
|
import {
|
||||||
|
blockAccount,
|
||||||
|
muteAccount
|
||||||
|
} from '../actions/accounts';
|
||||||
import { deleteStatus } from '../actions/statuses';
|
import { deleteStatus } from '../actions/statuses';
|
||||||
import { openMedia } from '../actions/modal';
|
import { initReport } from '../actions/reports';
|
||||||
|
import { openModal } from '../actions/modal';
|
||||||
import { createSelector } from 'reselect'
|
import { createSelector } from 'reselect'
|
||||||
import { isMobile } from '../is_mobile'
|
import { isMobile } from '../is_mobile'
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const makeMapStateToProps = () => {
|
||||||
statusBase: state.getIn(['statuses', props.id]),
|
const getStatus = makeGetStatus();
|
||||||
me: state.getIn(['meta', 'me'])
|
|
||||||
});
|
|
||||||
|
|
||||||
const makeMapStateToPropsInner = () => {
|
const mapStateToProps = (state, props) => ({
|
||||||
const getStatus = (() => {
|
status: getStatus(state, props.id),
|
||||||
return createSelector(
|
me: state.getIn(['meta', 'me']),
|
||||||
[
|
boostModal: state.getIn(['meta', 'boost_modal'])
|
||||||
(_, base) => base,
|
|
||||||
(state, base) => (base ? state.getIn(['accounts', base.get('account')]) : null),
|
|
||||||
(state, base) => (base ? state.getIn(['statuses', base.get('reblog')], null) : null)
|
|
||||||
],
|
|
||||||
|
|
||||||
(base, account, reblog) => (base ? base.set('account', account).set('reblog', reblog) : null)
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
|
|
||||||
const mapStateToProps = (state, { statusBase }) => ({
|
|
||||||
status: getStatus(state, statusBase)
|
|
||||||
});
|
|
||||||
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
const makeMapStateToPropsLast = () => {
|
|
||||||
const getStatus = (() => {
|
|
||||||
return createSelector(
|
|
||||||
[
|
|
||||||
(_, status) => status,
|
|
||||||
(state, status) => (status ? state.getIn(['accounts', status.getIn(['reblog', 'account'])], null) : null)
|
|
||||||
],
|
|
||||||
|
|
||||||
(status, reblogAccount) => (status && status.get('reblog') ? status.setIn(['reblog', 'account'], reblogAccount) : status)
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
|
|
||||||
const mapStateToProps = (state, { status }) => ({
|
|
||||||
status: getStatus(state, status)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapStateToProps;
|
return mapStateToProps;
|
||||||
@@ -67,11 +39,19 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
dispatch(replyCompose(status, router));
|
dispatch(replyCompose(status, router));
|
||||||
},
|
},
|
||||||
|
|
||||||
onReblog (status) {
|
onModalReblog (status) {
|
||||||
|
dispatch(reblog(status));
|
||||||
|
},
|
||||||
|
|
||||||
|
onReblog (status, e) {
|
||||||
if (status.get('reblogged')) {
|
if (status.get('reblogged')) {
|
||||||
dispatch(unreblog(status));
|
dispatch(unreblog(status));
|
||||||
} else {
|
} else {
|
||||||
dispatch(reblog(status));
|
if (e.shiftKey || !this.boostModal) {
|
||||||
|
this.onModalReblog(status);
|
||||||
|
} else {
|
||||||
|
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -92,17 +72,25 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onOpenMedia (media, index) {
|
onOpenMedia (media, index) {
|
||||||
dispatch(openMedia(media, index));
|
dispatch(openModal('MEDIA', { media, index }));
|
||||||
|
},
|
||||||
|
|
||||||
|
onOpenVideo (media, time) {
|
||||||
|
dispatch(openModal('VIDEO', { media, time }));
|
||||||
},
|
},
|
||||||
|
|
||||||
onBlock (account) {
|
onBlock (account) {
|
||||||
dispatch(blockAccount(account.get('id')));
|
dispatch(blockAccount(account.get('id')));
|
||||||
}
|
},
|
||||||
|
|
||||||
|
onReport (status) {
|
||||||
|
dispatch(initReport(status.get('account'), status));
|
||||||
|
},
|
||||||
|
|
||||||
|
onMute (account) {
|
||||||
|
dispatch(muteAccount(account.get('id')));
|
||||||
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(
|
export default connect(makeMapStateToProps, mapDispatchToProps)(Status);
|
||||||
connect(makeMapStateToPropsInner)(
|
|
||||||
connect(makeMapStateToPropsLast)(Status)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -1,9 +1,35 @@
|
|||||||
import emojione from 'emojione';
|
import emojione from 'emojione';
|
||||||
|
|
||||||
emojione.imageType = 'png';
|
const toImage = str => shortnameToImage(unicodeToImage(str));
|
||||||
emojione.sprites = false;
|
|
||||||
emojione.imagePathPNG = '/emoji/';
|
const unicodeToImage = str => {
|
||||||
|
const mappedUnicode = emojione.mapUnicodeToShort();
|
||||||
|
|
||||||
|
return str.replace(emojione.regUnicode, unicodeChar => {
|
||||||
|
if (typeof unicodeChar === 'undefined' || unicodeChar === '' || !(unicodeChar in emojione.jsEscapeMap)) {
|
||||||
|
return unicodeChar;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unicode = emojione.jsEscapeMap[unicodeChar];
|
||||||
|
const short = mappedUnicode[unicode];
|
||||||
|
const filename = emojione.emojioneList[short].fname;
|
||||||
|
const alt = emojione.convert(unicode.toUpperCase());
|
||||||
|
|
||||||
|
return `<img draggable="false" class="emojione" alt="${alt}" src="/emoji/${filename}.svg" />`;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const shortnameToImage = str => str.replace(emojione.regShortNames, shortname => {
|
||||||
|
if (typeof shortname === 'undefined' || shortname === '' || !(shortname in emojione.emojioneList)) {
|
||||||
|
return shortname;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unicode = emojione.emojioneList[shortname].unicode[emojione.emojioneList[shortname].unicode.length - 1];
|
||||||
|
const alt = emojione.convert(unicode.toUpperCase());
|
||||||
|
|
||||||
|
return `<img draggable="false" class="emojione" alt="${alt}" src="/emoji/${unicode}.svg" />`;
|
||||||
|
});
|
||||||
|
|
||||||
export default function emojify(text) {
|
export default function emojify(text) {
|
||||||
return emojione.toImage(text);
|
return toImage(text);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,24 +5,18 @@ import { Link } from 'react-router';
|
|||||||
import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
mention: { id: 'account.mention', defaultMessage: 'Mention' },
|
mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
|
||||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock' },
|
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||||
block: { id: 'account.block', defaultMessage: 'Block' },
|
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||||
|
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
|
||||||
|
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
||||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||||
block: { id: 'account.block', defaultMessage: 'Block' }
|
report: { id: 'account.report', defaultMessage: 'Report @{name}' },
|
||||||
|
disclaimer: { id: 'account.disclaimer', defaultMessage: 'This user is from another instance. This number may be larger.' }
|
||||||
});
|
});
|
||||||
|
|
||||||
const outerStyle = {
|
|
||||||
borderTop: '1px solid #363c4b',
|
|
||||||
borderBottom: '1px solid #363c4b',
|
|
||||||
lineHeight: '36px',
|
|
||||||
overflow: 'hidden',
|
|
||||||
flex: '0 0 auto',
|
|
||||||
display: 'flex'
|
|
||||||
};
|
|
||||||
|
|
||||||
const outerDropdownStyle = {
|
const outerDropdownStyle = {
|
||||||
padding: '10px',
|
padding: '10px',
|
||||||
flex: '1 1 auto'
|
flex: '1 1 auto'
|
||||||
@@ -41,7 +35,10 @@ const ActionBar = React.createClass({
|
|||||||
me: React.PropTypes.number.isRequired,
|
me: React.PropTypes.number.isRequired,
|
||||||
onFollow: React.PropTypes.func,
|
onFollow: React.PropTypes.func,
|
||||||
onBlock: React.PropTypes.func.isRequired,
|
onBlock: React.PropTypes.func.isRequired,
|
||||||
onMention: React.PropTypes.func.isRequired
|
onMention: React.PropTypes.func.isRequired,
|
||||||
|
onReport: React.PropTypes.func.isRequired,
|
||||||
|
onMute: React.PropTypes.func.isRequired,
|
||||||
|
intl: React.PropTypes.object.isRequired
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
@@ -50,39 +47,53 @@ const ActionBar = React.createClass({
|
|||||||
const { account, me, intl } = this.props;
|
const { account, me, intl } = this.props;
|
||||||
|
|
||||||
let menu = [];
|
let menu = [];
|
||||||
|
let extraInfo = '';
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(messages.mention), action: this.props.onMention });
|
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
|
||||||
|
menu.push(null);
|
||||||
|
|
||||||
if (account.get('id') === me) {
|
if (account.get('id') === me) {
|
||||||
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
|
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
|
||||||
} else if (account.getIn(['relationship', 'blocking'])) {
|
|
||||||
menu.push({ text: intl.formatMessage(messages.unblock), action: this.props.onBlock });
|
|
||||||
} else if (account.getIn(['relationship', 'following'])) {
|
|
||||||
menu.push({ text: intl.formatMessage(messages.block), action: this.props.onBlock });
|
|
||||||
} else {
|
} else {
|
||||||
menu.push({ text: intl.formatMessage(messages.block), action: this.props.onBlock });
|
if (account.getIn(['relationship', 'muting'])) {
|
||||||
|
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute });
|
||||||
|
} else {
|
||||||
|
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account.getIn(['relationship', 'blocking'])) {
|
||||||
|
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock });
|
||||||
|
} else {
|
||||||
|
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock });
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account.get('acct') !== account.get('username')) {
|
||||||
|
extraInfo = <abbr title={intl.formatMessage(messages.disclaimer)}>*</abbr>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={outerStyle}>
|
<div className='account__action-bar'>
|
||||||
<div style={outerDropdownStyle}>
|
<div style={outerDropdownStyle}>
|
||||||
<DropdownMenu items={menu} icon='bars' size={24} direction="right" />
|
<DropdownMenu items={menu} icon='bars' size={24} direction="right" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={outerLinksStyle}>
|
<div style={outerLinksStyle}>
|
||||||
<Link to={`/accounts/${account.get('id')}`} style={{ textDecoration: 'none', overflow: 'hidden', width: '80px', borderLeft: '1px solid #363c4b', padding: '10px', paddingRight: '5px' }}>
|
<Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}>
|
||||||
<span style={{ display: 'block', textTransform: 'uppercase', fontSize: '11px', color: '#616b86' }}><FormattedMessage id='account.posts' defaultMessage='Posts' /></span>
|
<span><FormattedMessage id='account.posts' defaultMessage='Posts' /></span>
|
||||||
<span style={{ display: 'block', fontSize: '15px', fontWeight: '500', color: '#fff' }}><FormattedNumber value={account.get('statuses_count')} /></span>
|
<strong><FormattedNumber value={account.get('statuses_count')} /> {extraInfo}</strong>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link to={`/accounts/${account.get('id')}/following`} style={{ textDecoration: 'none', overflow: 'hidden', width: '80px', borderLeft: '1px solid #363c4b', padding: '10px 5px' }}>
|
<Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`}>
|
||||||
<span style={{ display: 'block', textTransform: 'uppercase', fontSize: '11px', color: '#616b86' }}><FormattedMessage id='account.follows' defaultMessage='Follows' /></span>
|
<span><FormattedMessage id='account.follows' defaultMessage='Follows' /></span>
|
||||||
<span style={{ display: 'block', fontSize: '15px', fontWeight: '500', color: '#fff' }}><FormattedNumber value={account.get('following_count')} /></span>
|
<strong><FormattedNumber value={account.get('following_count')} /> {extraInfo}</strong>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link to={`/accounts/${account.get('id')}/followers`} style={{ textDecoration: 'none', overflow: 'hidden', width: '80px', padding: '10px 5px', borderLeft: '1px solid #363c4b' }}>
|
<Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}>
|
||||||
<span style={{ display: 'block', textTransform: 'uppercase', fontSize: '11px', color: '#616b86' }}><FormattedMessage id='account.followers' defaultMessage='Followers' /></span>
|
<span><FormattedMessage id='account.followers' defaultMessage='Followers' /></span>
|
||||||
<span style={{ display: 'block', fontSize: '15px', fontWeight: '500', color: '#fff' }}><FormattedNumber value={account.get('followers_count')} /></span>
|
<strong><FormattedNumber value={account.get('followers_count')} /> {extraInfo}</strong>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import emojify from '../../../emoji';
|
import emojify from '../../../emoji';
|
||||||
import escapeTextContentForBrowser from 'react/lib/escapeTextContentForBrowser';
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import IconButton from '../../../components/icon_button';
|
import IconButton from '../../../components/icon_button';
|
||||||
|
import { Motion, spring } from 'react-motion';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||||
@@ -11,12 +12,63 @@ const messages = defineMessages({
|
|||||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }
|
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const Avatar = React.createClass({
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
account: ImmutablePropTypes.map.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState () {
|
||||||
|
return {
|
||||||
|
isHovered: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
|
handleMouseOver () {
|
||||||
|
if (this.state.isHovered) return;
|
||||||
|
this.setState({ isHovered: true });
|
||||||
|
},
|
||||||
|
|
||||||
|
handleMouseOut () {
|
||||||
|
if (!this.state.isHovered) return;
|
||||||
|
this.setState({ isHovered: false });
|
||||||
|
},
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { account } = this.props;
|
||||||
|
const { isHovered } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}>
|
||||||
|
{({ radius }) =>
|
||||||
|
<a
|
||||||
|
href={account.get('url')}
|
||||||
|
className='account__header__avatar'
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener'
|
||||||
|
style={{ display: 'block', width: '90px', height: '90px', margin: '0 auto', marginBottom: '10px', borderRadius: `${radius}px`, overflow: 'hidden' }}
|
||||||
|
onMouseOver={this.handleMouseOver}
|
||||||
|
onMouseOut={this.handleMouseOut}
|
||||||
|
onFocus={this.handleMouseOver}
|
||||||
|
onBlur={this.handleMouseOut}>
|
||||||
|
<img src={account.get('avatar')} alt={account.get('acct')} style={{ display: 'block', width: '90px', height: '90px' }} />
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</Motion>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
const Header = React.createClass({
|
const Header = React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.map,
|
||||||
me: React.PropTypes.number.isRequired,
|
me: React.PropTypes.number.isRequired,
|
||||||
onFollow: React.PropTypes.func.isRequired
|
onFollow: React.PropTypes.func.isRequired,
|
||||||
|
intl: React.PropTypes.object.isRequired
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
@@ -24,6 +76,10 @@ const Header = React.createClass({
|
|||||||
render () {
|
render () {
|
||||||
const { account, me, intl } = this.props;
|
const { account, me, intl } = this.props;
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
let displayName = account.get('display_name');
|
let displayName = account.get('display_name');
|
||||||
let info = '';
|
let info = '';
|
||||||
let actionBtn = '';
|
let actionBtn = '';
|
||||||
@@ -34,7 +90,7 @@ const Header = React.createClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
|
if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
|
||||||
info = <span style={{ position: 'absolute', top: '10px', right: '10px', opacity: '0.7', display: 'inline-block', verticalAlign: 'top', background: 'rgba(0, 0, 0, 0.4)', color: '#fff', textTransform: 'uppercase', fontSize: '11px', fontWeight: '500', padding: '4px', borderRadius: '4px' }}><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>
|
info = <span className='account--follows-info' style={{ position: 'absolute', top: '10px', right: '10px', opacity: '0.7', display: 'inline-block', verticalAlign: 'top', background: 'rgba(0, 0, 0, 0.4)', textTransform: 'uppercase', fontSize: '11px', fontWeight: '500', padding: '4px', borderRadius: '4px' }}><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (me !== account.get('id')) {
|
if (me !== account.get('id')) {
|
||||||
@@ -44,7 +100,7 @@ const Header = React.createClass({
|
|||||||
<IconButton size={26} disabled={true} icon='hourglass' title={intl.formatMessage(messages.requested)} />
|
<IconButton size={26} disabled={true} icon='hourglass' title={intl.formatMessage(messages.requested)} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else if (!account.getIn(['relationship', 'blocking'])) {
|
||||||
actionBtn = (
|
actionBtn = (
|
||||||
<div style={{ position: 'absolute', top: '10px', left: '20px' }}>
|
<div style={{ position: 'absolute', top: '10px', left: '20px' }}>
|
||||||
<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={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} />
|
||||||
@@ -61,18 +117,13 @@ const Header = React.createClass({
|
|||||||
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
|
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='account__header' style={{ flex: '0 0 auto', background: '#2f3441', textAlign: 'center', backgroundImage: `url(${account.get('header')})`, backgroundSize: 'cover', backgroundPosition: 'center', position: 'relative' }}>
|
<div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
|
||||||
<div style={{ background: 'rgba(47, 52, 65, 0.9)', padding: '20px 10px' }}>
|
<div style={{ padding: '20px 10px' }}>
|
||||||
<a href={account.get('url')} target='_blank' rel='noopener' style={{ display: 'block', color: 'inherit', textDecoration: 'none' }}>
|
<Avatar account={account} />
|
||||||
<div className='account__header__avatar' style={{ width: '90px', margin: '0 auto', marginBottom: '10px' }}>
|
|
||||||
<img src={account.get('avatar')} alt='' style={{ display: 'block', width: '90px', height: '90px', borderRadius: '90px' }} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span style={{ display: 'inline-block', color: '#fff', fontSize: '20px', lineHeight: '27px', fontWeight: '500' }} className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} />
|
<span style={{ display: 'inline-block', fontSize: '20px', lineHeight: '27px', fontWeight: '500' }} className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} />
|
||||||
</a>
|
<span className='account__header__username' style={{ fontSize: '14px', fontWeight: '400', display: 'block', marginBottom: '10px' }}>@{account.get('acct')} {lockedIcon}</span>
|
||||||
|
<div style={{ fontSize: '14px' }} className='account__header__content' dangerouslySetInnerHTML={content} />
|
||||||
<span style={{ fontSize: '14px', fontWeight: '400', display: 'block', color: '#489fde', marginBottom: '10px' }}>@{account.get('acct')} {lockedIcon}</span>
|
|
||||||
<div style={{ color: '#d9e1e8', fontSize: '14px' }} className='account__header__content' dangerouslySetInnerHTML={content} />
|
|
||||||
|
|
||||||
{info}
|
{info}
|
||||||
{actionBtn}
|
{actionBtn}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import PureRenderMixin from 'react-addons-pure-render-mixin';
|
|||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import InnerHeader from '../../account/components/header';
|
import InnerHeader from '../../account/components/header';
|
||||||
import ActionBar from '../../account/components/action_bar';
|
import ActionBar from '../../account/components/action_bar';
|
||||||
|
import MissingIndicator from '../../../components/missing_indicator';
|
||||||
|
|
||||||
const Header = React.createClass({
|
const Header = React.createClass({
|
||||||
contextTypes: {
|
contextTypes: {
|
||||||
@@ -9,11 +10,13 @@ const Header = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.map,
|
||||||
me: React.PropTypes.number.isRequired,
|
me: React.PropTypes.number.isRequired,
|
||||||
onFollow: React.PropTypes.func.isRequired,
|
onFollow: React.PropTypes.func.isRequired,
|
||||||
onBlock: React.PropTypes.func.isRequired,
|
onBlock: React.PropTypes.func.isRequired,
|
||||||
onMention: React.PropTypes.func.isRequired
|
onMention: React.PropTypes.func.isRequired,
|
||||||
|
onReport: React.PropTypes.func.isRequired,
|
||||||
|
onMute: React.PropTypes.func.isRequired
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
@@ -30,11 +33,20 @@ const Header = React.createClass({
|
|||||||
this.props.onMention(this.props.account, this.context.router);
|
this.props.onMention(this.props.account, this.context.router);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleReport () {
|
||||||
|
this.props.onReport(this.props.account);
|
||||||
|
this.context.router.push('/report');
|
||||||
|
},
|
||||||
|
|
||||||
|
handleMute() {
|
||||||
|
this.props.onMute(this.props.account);
|
||||||
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, me } = this.props;
|
const { account, me } = this.props;
|
||||||
|
|
||||||
if (!account) {
|
if (account === null) {
|
||||||
return null;
|
return <MissingIndicator />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -50,6 +62,8 @@ const Header = React.createClass({
|
|||||||
me={me}
|
me={me}
|
||||||
onBlock={this.handleBlock}
|
onBlock={this.handleBlock}
|
||||||
onMention={this.handleMention}
|
onMention={this.handleMention}
|
||||||
|
onReport={this.handleReport}
|
||||||
|
onMute={this.handleMute}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ import {
|
|||||||
followAccount,
|
followAccount,
|
||||||
unfollowAccount,
|
unfollowAccount,
|
||||||
blockAccount,
|
blockAccount,
|
||||||
unblockAccount
|
unblockAccount,
|
||||||
|
muteAccount,
|
||||||
|
unmuteAccount
|
||||||
} from '../../../actions/accounts';
|
} from '../../../actions/accounts';
|
||||||
import { mentionCompose } from '../../../actions/compose';
|
import { mentionCompose } from '../../../actions/compose';
|
||||||
|
import { initReport } from '../../../actions/reports';
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
const getAccount = makeGetAccount();
|
const getAccount = makeGetAccount();
|
||||||
@@ -39,6 +42,18 @@ const mapDispatchToProps = dispatch => ({
|
|||||||
|
|
||||||
onMention (account, router) {
|
onMention (account, router) {
|
||||||
dispatch(mentionCompose(account, router));
|
dispatch(mentionCompose(account, router));
|
||||||
|
},
|
||||||
|
|
||||||
|
onReport (account) {
|
||||||
|
dispatch(initReport(account));
|
||||||
|
},
|
||||||
|
|
||||||
|
onMute (account) {
|
||||||
|
if (account.getIn(['relationship', 'muting'])) {
|
||||||
|
dispatch(unmuteAccount(account.get('id')));
|
||||||
|
} else {
|
||||||
|
dispatch(muteAccount(account.get('id')));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import Immutable from 'immutable';
|
|||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
statusIds: state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId), 'items'], Immutable.List()),
|
statusIds: state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId), 'items'], Immutable.List()),
|
||||||
isLoading: state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId), 'isLoading']),
|
isLoading: state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId), 'isLoading']),
|
||||||
|
hasMore: !!state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId), 'next']),
|
||||||
me: state.getIn(['meta', 'me'])
|
me: state.getIn(['meta', 'me'])
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ const AccountTimeline = React.createClass({
|
|||||||
dispatch: React.PropTypes.func.isRequired,
|
dispatch: React.PropTypes.func.isRequired,
|
||||||
statusIds: ImmutablePropTypes.list,
|
statusIds: ImmutablePropTypes.list,
|
||||||
isLoading: React.PropTypes.bool,
|
isLoading: React.PropTypes.bool,
|
||||||
|
hasMore: React.PropTypes.bool,
|
||||||
me: React.PropTypes.number.isRequired
|
me: React.PropTypes.number.isRequired
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -48,7 +50,7 @@ const AccountTimeline = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { statusIds, isLoading, me } = this.props;
|
const { statusIds, isLoading, hasMore, me } = this.props;
|
||||||
|
|
||||||
if (!statusIds && isLoading) {
|
if (!statusIds && isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -66,6 +68,7 @@ const AccountTimeline = React.createClass({
|
|||||||
prepend={<HeaderContainer accountId={this.props.params.accountId} />}
|
prepend={<HeaderContainer accountId={this.props.params.accountId} />}
|
||||||
statusIds={statusIds}
|
statusIds={statusIds}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
hasMore={hasMore}
|
||||||
me={me}
|
me={me}
|
||||||
onScrollToBottom={this.handleScrollToBottom}
|
onScrollToBottom={this.handleScrollToBottom}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
|
import StatusListContainer from '../ui/containers/status_list_container';
|
||||||
|
import Column from '../ui/components/column';
|
||||||
|
import {
|
||||||
|
refreshTimeline,
|
||||||
|
updateTimeline,
|
||||||
|
deleteFromTimelines,
|
||||||
|
connectTimeline,
|
||||||
|
disconnectTimeline
|
||||||
|
} from '../../actions/timelines';
|
||||||
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
|
||||||
|
import createStream from '../../stream';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'column.community', defaultMessage: 'Local timeline' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0,
|
||||||
|
streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
|
||||||
|
accessToken: state.getIn(['meta', 'access_token'])
|
||||||
|
});
|
||||||
|
|
||||||
|
let subscription;
|
||||||
|
|
||||||
|
const CommunityTimeline = React.createClass({
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
dispatch: React.PropTypes.func.isRequired,
|
||||||
|
intl: React.PropTypes.object.isRequired,
|
||||||
|
streamingAPIBaseURL: React.PropTypes.string.isRequired,
|
||||||
|
accessToken: React.PropTypes.string.isRequired,
|
||||||
|
hasUnread: React.PropTypes.bool
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
|
||||||
|
|
||||||
|
dispatch(refreshTimeline('community'));
|
||||||
|
|
||||||
|
if (typeof subscription !== 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription = createStream(streamingAPIBaseURL, accessToken, 'public:local', {
|
||||||
|
|
||||||
|
connected () {
|
||||||
|
dispatch(connectTimeline('community'));
|
||||||
|
},
|
||||||
|
|
||||||
|
reconnected () {
|
||||||
|
dispatch(connectTimeline('community'));
|
||||||
|
},
|
||||||
|
|
||||||
|
disconnected () {
|
||||||
|
dispatch(disconnectTimeline('community'));
|
||||||
|
},
|
||||||
|
|
||||||
|
received (data) {
|
||||||
|
switch(data.event) {
|
||||||
|
case 'update':
|
||||||
|
dispatch(updateTimeline('community', JSON.parse(data.payload)));
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
dispatch(deleteFromTimelines(data.payload));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
// if (typeof subscription !== 'undefined') {
|
||||||
|
// subscription.close();
|
||||||
|
// subscription = null;
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { intl, hasUnread } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column icon='users' active={hasUnread} heading={intl.formatMessage(messages.title)}>
|
||||||
|
<ColumnBackButtonSlim />
|
||||||
|
<StatusListContainer type='community' emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />} />
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(injectIntl(CommunityTimeline));
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
import Avatar from '../../../components/avatar';
|
import Avatar from '../../../components/avatar';
|
||||||
import DisplayName from '../../../components/display_name';
|
import DisplayName from '../../../components/display_name';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
|
||||||
const AutosuggestAccount = ({ account }) => (
|
const AutosuggestAccount = ({ account }) => (
|
||||||
<div style={{ overflow: 'hidden' }}>
|
<div style={{ overflow: 'hidden' }} className='autosuggest-account'>
|
||||||
<div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} size={18} /></div>
|
<div style={{ float: 'left', marginRight: '5px' }}><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={18} /></div>
|
||||||
<DisplayName account={account} />
|
<DisplayName account={account} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
AutosuggestAccount.propTypes = {
|
||||||
|
account: ImmutablePropTypes.map.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
export default AutosuggestAccount;
|
export default AutosuggestAccount;
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import DisplayName from '../../../components/display_name';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
|
||||||
|
const AutosuggestStatus = ({ status }) => (
|
||||||
|
<div style={{ overflow: 'hidden' }} className='autosuggest-status'>
|
||||||
|
<FormattedMessage id='search.status_by' defaultMessage='Status by {name}' values={{ name: <strong>@{status.getIn(['account', 'acct'])}</strong> }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
AutosuggestStatus.propTypes = {
|
||||||
|
status: ImmutablePropTypes.map.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AutosuggestStatus;
|
||||||
@@ -10,7 +10,7 @@ const CharacterCounter = React.createClass({
|
|||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const diff = this.props.max - this.props.text.length;
|
const diff = this.props.max - this.props.text.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, "_").length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span style={{ fontSize: '16px', cursor: 'default' }}>
|
<span style={{ fontSize: '16px', cursor: 'default' }}>
|
||||||
|
|||||||
@@ -2,20 +2,24 @@ import CharacterCounter from './character_counter';
|
|||||||
import Button from '../../../components/button';
|
import Button from '../../../components/button';
|
||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ReplyIndicator from './reply_indicator';
|
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
|
||||||
import UploadButton from './upload_button';
|
|
||||||
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
|
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
|
||||||
import AutosuggestAccountContainer from '../../compose/containers/autosuggest_account_container';
|
|
||||||
import { debounce } from 'react-decoration';
|
import { debounce } from 'react-decoration';
|
||||||
import UploadButtonContainer from '../containers/upload_button_container';
|
import UploadButtonContainer from '../containers/upload_button_container';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import Toggle from 'react-toggle';
|
import Toggle from 'react-toggle';
|
||||||
import { Motion, spring } from 'react-motion';
|
import Collapsable from '../../../components/collapsable';
|
||||||
|
import SpoilerButtonContainer from '../containers/spoiler_button_container';
|
||||||
|
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
||||||
|
import SensitiveButtonContainer from '../containers/sensitive_button_container';
|
||||||
|
import EmojiPickerDropdown from './emoji_picker_dropdown';
|
||||||
|
import UploadFormContainer from '../containers/upload_form_container';
|
||||||
|
import TextIconButton from './text_icon_button';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
|
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
|
||||||
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Content warning' },
|
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Content warning' },
|
||||||
publish: { id: 'compose_form.publish', defaultMessage: 'Publish' }
|
publish: { id: 'compose_form.publish', defaultMessage: 'Toot' }
|
||||||
});
|
});
|
||||||
|
|
||||||
const ComposeForm = React.createClass({
|
const ComposeForm = React.createClass({
|
||||||
@@ -25,28 +29,24 @@ const ComposeForm = React.createClass({
|
|||||||
text: React.PropTypes.string.isRequired,
|
text: React.PropTypes.string.isRequired,
|
||||||
suggestion_token: React.PropTypes.string,
|
suggestion_token: React.PropTypes.string,
|
||||||
suggestions: ImmutablePropTypes.list,
|
suggestions: ImmutablePropTypes.list,
|
||||||
sensitive: React.PropTypes.bool,
|
|
||||||
spoiler: React.PropTypes.bool,
|
spoiler: React.PropTypes.bool,
|
||||||
|
privacy: React.PropTypes.string,
|
||||||
spoiler_text: React.PropTypes.string,
|
spoiler_text: React.PropTypes.string,
|
||||||
unlisted: React.PropTypes.bool,
|
focusDate: React.PropTypes.instanceOf(Date),
|
||||||
private: React.PropTypes.bool,
|
preselectDate: React.PropTypes.instanceOf(Date),
|
||||||
fileDropDate: React.PropTypes.instanceOf(Date),
|
|
||||||
is_submitting: React.PropTypes.bool,
|
is_submitting: React.PropTypes.bool,
|
||||||
is_uploading: React.PropTypes.bool,
|
is_uploading: React.PropTypes.bool,
|
||||||
in_reply_to: ImmutablePropTypes.map,
|
|
||||||
media_count: React.PropTypes.number,
|
|
||||||
me: React.PropTypes.number,
|
me: React.PropTypes.number,
|
||||||
|
needsPrivacyWarning: React.PropTypes.bool,
|
||||||
|
mentionedDomains: React.PropTypes.array.isRequired,
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: React.PropTypes.func.isRequired,
|
||||||
onSubmit: React.PropTypes.func.isRequired,
|
onSubmit: React.PropTypes.func.isRequired,
|
||||||
onCancelReply: React.PropTypes.func.isRequired,
|
|
||||||
onClearSuggestions: React.PropTypes.func.isRequired,
|
onClearSuggestions: React.PropTypes.func.isRequired,
|
||||||
onFetchSuggestions: React.PropTypes.func.isRequired,
|
onFetchSuggestions: React.PropTypes.func.isRequired,
|
||||||
onSuggestionSelected: React.PropTypes.func.isRequired,
|
onSuggestionSelected: React.PropTypes.func.isRequired,
|
||||||
onChangeSensitivity: React.PropTypes.func.isRequired,
|
|
||||||
onChangeSpoilerness: React.PropTypes.func.isRequired,
|
|
||||||
onChangeSpoilerText: React.PropTypes.func.isRequired,
|
onChangeSpoilerText: React.PropTypes.func.isRequired,
|
||||||
onChangeVisibility: React.PropTypes.func.isRequired,
|
onPaste: React.PropTypes.func.isRequired,
|
||||||
onChangeListability: React.PropTypes.func.isRequired,
|
onPickEmoji: React.PropTypes.func.isRequired
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
@@ -75,37 +75,43 @@ const ComposeForm = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onSuggestionSelected (tokenStart, token, value) {
|
onSuggestionSelected (tokenStart, token, value) {
|
||||||
|
this._restoreCaret = null;
|
||||||
this.props.onSuggestionSelected(tokenStart, token, value);
|
this.props.onSuggestionSelected(tokenStart, token, value);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleChangeSensitivity (e) {
|
|
||||||
this.props.onChangeSensitivity(e.target.checked);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleChangeSpoilerness (e) {
|
|
||||||
this.props.onChangeSpoilerness(e.target.checked);
|
|
||||||
this.props.onChangeSpoilerText('');
|
|
||||||
},
|
|
||||||
|
|
||||||
handleChangeSpoilerText (e) {
|
handleChangeSpoilerText (e) {
|
||||||
this.props.onChangeSpoilerText(e.target.value);
|
this.props.onChangeSpoilerText(e.target.value);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleChangeVisibility (e) {
|
componentWillReceiveProps (nextProps) {
|
||||||
this.props.onChangeVisibility(e.target.checked);
|
// If this is the update where we've finished uploading,
|
||||||
},
|
// save the last caret position so we can restore it below!
|
||||||
|
if (!nextProps.is_uploading && this.props.is_uploading) {
|
||||||
handleChangeListability (e) {
|
this._restoreCaret = this.autosuggestTextarea.textarea.selectionStart;
|
||||||
this.props.onChangeListability(e.target.checked);
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
if ((prevProps.in_reply_to === null && this.props.in_reply_to !== null) || (prevProps.in_reply_to !== null && this.props.in_reply_to !== null && prevProps.in_reply_to.get('id') !== this.props.in_reply_to.get('id'))) {
|
// This statement does several things:
|
||||||
// If replying to zero or one users, places the cursor at the end of the textbox.
|
// - If we're beginning a reply, and,
|
||||||
// If replying to more than one user, selects any usernames past the first;
|
// - Replying to zero or one users, places the cursor at the end of the textbox.
|
||||||
// this provides a convenient shortcut to drop everyone else from the conversation.
|
// - Replying to more than one user, selects any usernames past the first;
|
||||||
const selectionStart = this.props.text.search(/\s/) + 1;
|
// this provides a convenient shortcut to drop everyone else from the conversation.
|
||||||
const selectionEnd = this.props.text.length;
|
// - If we've just finished uploading an image, and have a saved caret position,
|
||||||
|
// restores the cursor to that position after the text changes!
|
||||||
|
if (this.props.focusDate !== prevProps.focusDate || (prevProps.is_uploading && !this.props.is_uploading && typeof this._restoreCaret === 'number')) {
|
||||||
|
let selectionEnd, selectionStart;
|
||||||
|
|
||||||
|
if (this.props.preselectDate !== prevProps.preselectDate) {
|
||||||
|
selectionEnd = this.props.text.length;
|
||||||
|
selectionStart = this.props.text.search(/\s/) + 1;
|
||||||
|
} else if (typeof this._restoreCaret === 'number') {
|
||||||
|
selectionStart = this._restoreCaret;
|
||||||
|
selectionEnd = this._restoreCaret;
|
||||||
|
} else {
|
||||||
|
selectionEnd = this.props.text.length;
|
||||||
|
selectionStart = selectionEnd;
|
||||||
|
}
|
||||||
|
|
||||||
this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
|
this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
|
||||||
this.autosuggestTextarea.textarea.focus();
|
this.autosuggestTextarea.textarea.focus();
|
||||||
@@ -116,76 +122,85 @@ const ComposeForm = React.createClass({
|
|||||||
this.autosuggestTextarea = c;
|
this.autosuggestTextarea = c;
|
||||||
},
|
},
|
||||||
|
|
||||||
render () {
|
handleEmojiPick (data) {
|
||||||
const { intl } = this.props;
|
const position = this.autosuggestTextarea.textarea.selectionStart;
|
||||||
let replyArea = '';
|
this._restoreCaret = position + data.shortname.length + 1;
|
||||||
const disabled = this.props.is_submitting || this.props.is_uploading;
|
this.props.onPickEmoji(position, data);
|
||||||
|
},
|
||||||
|
|
||||||
if (this.props.in_reply_to) {
|
render () {
|
||||||
replyArea = <ReplyIndicator status={this.props.in_reply_to} onCancel={this.props.onCancelReply} />;
|
const { intl, needsPrivacyWarning, mentionedDomains, onPaste } = this.props;
|
||||||
|
const disabled = this.props.is_submitting;
|
||||||
|
|
||||||
|
let publishText = '';
|
||||||
|
let privacyWarning = '';
|
||||||
|
let reply_to_other = false;
|
||||||
|
|
||||||
|
if (needsPrivacyWarning) {
|
||||||
|
privacyWarning = (
|
||||||
|
<div className='compose-form__warning'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='compose_form.privacy_disclaimer'
|
||||||
|
defaultMessage='Your private status will be delivered to mentioned users on {domains}. Do you trust {domainsCount, plural, one {that server} other {those servers}} to not leak your status?'
|
||||||
|
values={{ domains: <strong>{mentionedDomains.join(', ')}</strong>, domainsCount: mentionedDomains.length }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let reply_to_other = !!this.props.in_reply_to && (this.props.in_reply_to.getIn(['account', 'id']) !== this.props.me);
|
if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
|
||||||
|
publishText = <span><i className='fa fa-lock' /> {intl.formatMessage(messages.publish)}</span>;
|
||||||
|
} else {
|
||||||
|
publishText = intl.formatMessage(messages.publish) + (this.props.privacy !== 'unlisted' ? '!' : '');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '10px' }}>
|
<div style={{ padding: '10px' }}>
|
||||||
<Motion defaultStyle={{ opacity: !this.props.spoiler ? 0 : 100, height: !this.props.spoiler ? 50 : 0 }} style={{ opacity: spring(!this.props.spoiler ? 0 : 100), height: spring(!this.props.spoiler ? 0 : 50) }}>
|
<Collapsable isVisible={this.props.spoiler} fullHeight={50}>
|
||||||
{({ opacity, height }) =>
|
<div className="spoiler-input">
|
||||||
<div className="spoiler-input" style={{ height: `${height}px`, overflow: 'hidden', opacity: opacity / 100 }}>
|
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} onKeyDown={this.handleKeyDown} type="text" className="spoiler-input__input" />
|
||||||
<input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} type="text" className="spoiler-input__input" />
|
</div>
|
||||||
</div>
|
</Collapsable>
|
||||||
}
|
|
||||||
</Motion>
|
|
||||||
|
|
||||||
{replyArea}
|
{privacyWarning}
|
||||||
|
|
||||||
<AutosuggestTextarea
|
<ReplyIndicatorContainer />
|
||||||
ref={this.setAutosuggestTextarea}
|
|
||||||
placeholder={intl.formatMessage(messages.placeholder)}
|
|
||||||
disabled={disabled}
|
|
||||||
fileDropDate={this.props.fileDropDate}
|
|
||||||
value={this.props.text}
|
|
||||||
onChange={this.handleChange}
|
|
||||||
suggestions={this.props.suggestions}
|
|
||||||
onKeyDown={this.handleKeyDown}
|
|
||||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
|
||||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
|
||||||
onSuggestionSelected={this.onSuggestionSelected}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div style={{ marginTop: '10px', overflow: 'hidden' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
<div style={{ float: 'right' }}><Button text={intl.formatMessage(messages.publish)} onClick={this.handleSubmit} disabled={disabled} /></div>
|
<AutosuggestTextarea
|
||||||
<div style={{ float: 'right', marginRight: '16px', lineHeight: '36px' }}><CharacterCounter max={500} text={[this.props.spoiler_text, this.props.text].join('')} /></div>
|
ref={this.setAutosuggestTextarea}
|
||||||
<UploadButtonContainer style={{ paddingTop: '4px' }} />
|
placeholder={intl.formatMessage(messages.placeholder)}
|
||||||
|
disabled={disabled}
|
||||||
|
value={this.props.text}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
suggestions={this.props.suggestions}
|
||||||
|
onKeyDown={this.handleKeyDown}
|
||||||
|
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||||
|
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||||
|
onSuggestionSelected={this.onSuggestionSelected}
|
||||||
|
onPaste={onPaste}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label style={{ display: 'block', lineHeight: '24px', verticalAlign: 'middle', marginTop: '10px', borderTop: '1px solid #282c37', paddingTop: '10px' }}>
|
<div className='compose-form__modifiers'>
|
||||||
<Toggle checked={this.props.spoiler} onChange={this.handleChangeSpoilerness} />
|
<UploadFormContainer />
|
||||||
<span style={{ display: 'inline-block', verticalAlign: 'top', marginLeft: '8px', color: '#9baec8' }}><FormattedMessage id='compose_form.spoiler' defaultMessage='Hide text behind warning' /></span>
|
</div>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label style={{ display: 'block', lineHeight: '24px', verticalAlign: 'middle', borderTop: '1px solid #282c37', paddingTop: '10px' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<Toggle checked={this.props.private} onChange={this.handleChangeVisibility} />
|
<div className='compose-form__buttons'>
|
||||||
<span style={{ display: 'inline-block', verticalAlign: 'middle', marginBottom: '14px', marginLeft: '8px', color: '#9baec8' }}><FormattedMessage id='compose_form.private' defaultMessage='Mark as private' /></span>
|
<UploadButtonContainer />
|
||||||
</label>
|
<PrivacyDropdownContainer />
|
||||||
|
<SensitiveButtonContainer />
|
||||||
|
<SpoilerButtonContainer />
|
||||||
|
</div>
|
||||||
|
|
||||||
<Motion defaultStyle={{ opacity: (this.props.private || reply_to_other) ? 0 : 100, height: (this.props.private || reply_to_other) ? 39.5 : 0 }} style={{ opacity: spring((this.props.private || reply_to_other) ? 0 : 100), height: spring((this.props.private || reply_to_other) ? 0 : 39.5) }}>
|
<div style={{ display: 'flex' }}>
|
||||||
{({ opacity, height }) =>
|
<div style={{ paddingTop: '10px', marginRight: '16px', lineHeight: '36px' }}><CharacterCounter max={500} text={[this.props.spoiler_text, this.props.text].join('')} /></div>
|
||||||
<label style={{ display: 'block', lineHeight: '24px', verticalAlign: 'middle', height: `${height}px`, overflow: 'hidden', opacity: opacity / 100 }}>
|
<div style={{ paddingTop: '10px' }}><Button text={publishText} onClick={this.handleSubmit} disabled={disabled} /></div>
|
||||||
<Toggle checked={this.props.unlisted} onChange={this.handleChangeListability} />
|
</div>
|
||||||
<span style={{ display: 'inline-block', verticalAlign: 'middle', marginBottom: '14px', marginLeft: '8px', color: '#9baec8' }}><FormattedMessage id='compose_form.unlisted' defaultMessage='Do not display in public timeline' /></span>
|
</div>
|
||||||
</label>
|
|
||||||
}
|
|
||||||
</Motion>
|
|
||||||
|
|
||||||
<Motion defaultStyle={{ opacity: 0, height: 0 }} style={{ opacity: spring(this.props.media_count === 0 ? 0 : 100), height: spring(this.props.media_count === 0 ? 0 : 39.5) }}>
|
|
||||||
{({ opacity, height }) =>
|
|
||||||
<label style={{ display: 'block', lineHeight: '24px', verticalAlign: 'middle', height: `${height}px`, overflow: 'hidden', opacity: opacity / 100 }}>
|
|
||||||
<Toggle checked={this.props.sensitive} onChange={this.handleChangeSensitivity} />
|
|
||||||
<span style={{ display: 'inline-block', verticalAlign: 'middle', marginBottom: '14px', marginLeft: '8px', color: '#9baec8' }}><FormattedMessage id='compose_form.sensitive' defaultMessage='Mark media as sensitive' /></span>
|
|
||||||
</label>
|
|
||||||
}
|
|
||||||
</Motion>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
import { Link } from 'react-router';
|
|
||||||
import { injectIntl, defineMessages } from 'react-intl';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
|
||||||
public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Public timeline' },
|
|
||||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
|
||||||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }
|
|
||||||
});
|
|
||||||
|
|
||||||
const outerStyle = {
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
overflowY: 'hidden'
|
|
||||||
};
|
|
||||||
|
|
||||||
const innerStyle = {
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
padding: '0',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
overflowY: 'auto',
|
|
||||||
flexGrow: '1'
|
|
||||||
};
|
|
||||||
|
|
||||||
const tabStyle = {
|
|
||||||
display: 'block',
|
|
||||||
flex: '1 1 auto',
|
|
||||||
padding: '15px',
|
|
||||||
paddingBottom: '13px',
|
|
||||||
color: '#9baec8',
|
|
||||||
textDecoration: 'none',
|
|
||||||
textAlign: 'center',
|
|
||||||
fontSize: '16px',
|
|
||||||
borderBottom: '2px solid transparent'
|
|
||||||
};
|
|
||||||
|
|
||||||
const tabActiveStyle = {
|
|
||||||
color: '#2b90d9',
|
|
||||||
borderBottom: '2px solid #2b90d9'
|
|
||||||
};
|
|
||||||
|
|
||||||
const Drawer = ({ children, withHeader, intl }) => {
|
|
||||||
let header = '';
|
|
||||||
|
|
||||||
if (withHeader) {
|
|
||||||
header = (
|
|
||||||
<div className='drawer__header'>
|
|
||||||
<Link title={intl.formatMessage(messages.start)} style={tabStyle} to='/getting-started'><i className='fa fa-fw fa-asterisk' /></Link>
|
|
||||||
<Link title={intl.formatMessage(messages.public)} style={tabStyle} to='/timelines/public'><i className='fa fa-fw fa-globe' /></Link>
|
|
||||||
<a title={intl.formatMessage(messages.preferences)} style={tabStyle} href='/settings/preferences'><i className='fa fa-fw fa-cog' /></a>
|
|
||||||
<a title={intl.formatMessage(messages.logout)} style={tabStyle} href='/auth/sign_out' data-method='delete'><i className='fa fa-fw fa-sign-out' /></a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='drawer' style={outerStyle}>
|
|
||||||
{header}
|
|
||||||
|
|
||||||
<div className='drawer__inner' style={innerStyle}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Drawer.propTypes = {
|
|
||||||
withHeader: React.PropTypes.bool,
|
|
||||||
children: React.PropTypes.node,
|
|
||||||
intl: React.PropTypes.object
|
|
||||||
};
|
|
||||||
|
|
||||||
export default injectIntl(Drawer);
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown';
|
||||||
|
import EmojiPicker from 'emojione-picker';
|
||||||
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const settings = {
|
||||||
|
imageType: 'png',
|
||||||
|
sprites: false,
|
||||||
|
imagePathPNG: '/emoji/'
|
||||||
|
};
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
position: 'absolute',
|
||||||
|
right: '5px',
|
||||||
|
top: '5px'
|
||||||
|
};
|
||||||
|
|
||||||
|
const EmojiPickerDropdown = React.createClass({
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
intl: React.PropTypes.object.isRequired,
|
||||||
|
onPickEmoji: React.PropTypes.func.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
|
setRef (c) {
|
||||||
|
this.dropdown = c;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleChange (data) {
|
||||||
|
this.dropdown.hide();
|
||||||
|
this.props.onPickEmoji(data);
|
||||||
|
},
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { intl } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown ref={this.setRef} style={style}>
|
||||||
|
<DropdownTrigger className='emoji-button' title={intl.formatMessage(messages.emoji)} style={{ fontSize: `24px`, width: `24px`, lineHeight: `24px`, display: 'block', marginLeft: '2px' }}>
|
||||||
|
<img draggable="false" className="emojione" alt="🙂" src="/emoji/1f602.svg" />
|
||||||
|
</DropdownTrigger>
|
||||||
|
|
||||||
|
<DropdownContent className='dropdown__left light'>
|
||||||
|
<EmojiPicker emojione={settings} onChange={this.handleChange} search={true} />
|
||||||
|
</DropdownContent>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default injectIntl(EmojiPickerDropdown);
|
||||||
@@ -16,11 +16,11 @@ const NavigationBar = React.createClass({
|
|||||||
|
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '10px', display: 'flex', flexShrink: '0', cursor: 'default' }}>
|
<div className='navigation-bar'>
|
||||||
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} size={40} /></Permalink>
|
<Permalink href={this.props.account.get('url')} to={`/accounts/${this.props.account.get('id')}`} style={{ textDecoration: 'none' }}><Avatar src={this.props.account.get('avatar')} animate size={40} /></Permalink>
|
||||||
|
|
||||||
<div style={{ flex: '1 1 auto', marginLeft: '8px', color: '#9baec8' }}>
|
<div style={{ flex: '1 1 auto', marginLeft: '8px' }}>
|
||||||
<strong style={{ fontWeight: '500', display: 'block', color: '#fff' }}>{this.props.account.get('acct')}</strong>
|
<strong style={{ fontWeight: '500', display: 'block' }}>{this.props.account.get('acct')}</strong>
|
||||||
<a href='/settings/profile' style={{ color: 'inherit', textDecoration: 'none' }}><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
|
<a href='/settings/profile' style={{ color: 'inherit', textDecoration: 'none' }}><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
|
import IconButton from '../../../components/icon_button';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||||
|
public_long: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' },
|
||||||
|
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
||||||
|
unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Do not show in public timelines' },
|
||||||
|
private_short: { id: 'privacy.private.short', defaultMessage: 'Private' },
|
||||||
|
private_long: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' },
|
||||||
|
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
|
||||||
|
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Post to mentioned users only' },
|
||||||
|
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const iconStyle = {
|
||||||
|
lineHeight: '27px',
|
||||||
|
height: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const PrivacyDropdown = React.createClass({
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
value: React.PropTypes.string.isRequired,
|
||||||
|
onChange: React.PropTypes.func.isRequired,
|
||||||
|
intl: React.PropTypes.object.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState () {
|
||||||
|
return {
|
||||||
|
open: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
|
handleToggle () {
|
||||||
|
this.setState({ open: !this.state.open });
|
||||||
|
},
|
||||||
|
|
||||||
|
handleClick (value, e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.setState({ open: false });
|
||||||
|
this.props.onChange(value);
|
||||||
|
},
|
||||||
|
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
|
||||||
|
setRef (c) {
|
||||||
|
this.node = c;
|
||||||
|
},
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { value, onChange, intl } = this.props;
|
||||||
|
const { open } = this.state;
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{ icon: 'globe', value: 'public', shortText: intl.formatMessage(messages.public_short), longText: intl.formatMessage(messages.public_long) },
|
||||||
|
{ icon: 'unlock-alt', value: 'unlisted', shortText: intl.formatMessage(messages.unlisted_short), longText: intl.formatMessage(messages.unlisted_long) },
|
||||||
|
{ icon: 'lock', value: 'private', shortText: intl.formatMessage(messages.private_short), longText: intl.formatMessage(messages.private_long) },
|
||||||
|
{ icon: 'envelope', value: 'direct', shortText: intl.formatMessage(messages.direct_short), longText: intl.formatMessage(messages.direct_long) }
|
||||||
|
];
|
||||||
|
|
||||||
|
const valueOption = options.find(item => item.value === value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={this.setRef} className={`privacy-dropdown ${open ? 'active' : ''}`}>
|
||||||
|
<div className='privacy-dropdown__value'><IconButton icon={valueOption.icon} title={intl.formatMessage(messages.change_privacy)} size={18} active={open} inverted onClick={this.handleToggle} style={iconStyle} /></div>
|
||||||
|
<div className='privacy-dropdown__dropdown'>
|
||||||
|
{options.map(item =>
|
||||||
|
<div role='button' tabIndex='0' key={item.value} onClick={this.handleClick.bind(this, item.value)} className={`privacy-dropdown__option ${item.value === value ? 'active' : ''}`}>
|
||||||
|
<div className='privacy-dropdown__option__icon'><i className={`fa fa-fw fa-${item.icon}`} /></div>
|
||||||
|
<div className='privacy-dropdown__option__content'>
|
||||||
|
<strong>{item.shortText}</strong>
|
||||||
|
{item.longText}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default injectIntl(PrivacyDropdown);
|
||||||
@@ -17,8 +17,9 @@ const ReplyIndicator = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
status: ImmutablePropTypes.map.isRequired,
|
status: ImmutablePropTypes.map,
|
||||||
onCancel: React.PropTypes.func.isRequired
|
onCancel: React.PropTypes.func.isRequired,
|
||||||
|
intl: React.PropTypes.object.isRequired
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
@@ -35,17 +36,22 @@ const ReplyIndicator = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl } = this.props;
|
const { status, intl } = this.props;
|
||||||
const content = { __html: emojify(this.props.status.get('content')) };
|
|
||||||
|
if (!status) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = { __html: emojify(status.get('content')) };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ background: '#9baec8', padding: '10px' }}>
|
<div className='reply-indicator'>
|
||||||
<div style={{ overflow: 'hidden', marginBottom: '5px' }}>
|
<div style={{ overflow: 'hidden', marginBottom: '5px' }}>
|
||||||
<div style={{ float: 'right', lineHeight: '24px' }}><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div>
|
<div style={{ float: 'right', lineHeight: '24px' }}><IconButton title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} /></div>
|
||||||
|
|
||||||
<a href={this.props.status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', color: '#282c37', textDecoration: 'none', overflow: 'hidden', lineHeight: '24px' }}>
|
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name' style={{ display: 'block', maxWidth: '100%', paddingRight: '25px', textDecoration: 'none', overflow: 'hidden', lineHeight: '24px' }}>
|
||||||
<div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={this.props.status.getIn(['account', 'avatar'])} /></div>
|
<div style={{ float: 'left', marginRight: '5px' }}><Avatar size={24} src={status.getIn(['account', 'avatar'])} staticSrc={status.getIn(['account', 'avatar_static'])} /></div>
|
||||||
<DisplayName account={this.props.status.get('account')} />
|
<DisplayName account={status.get('account')} />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,132 +1,72 @@
|
|||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import Autosuggest from 'react-autosuggest';
|
|
||||||
import AutosuggestAccountContainer from '../containers/autosuggest_account_container';
|
|
||||||
import { debounce } from 'react-decoration';
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }
|
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }
|
||||||
});
|
});
|
||||||
|
|
||||||
const getSuggestionValue = suggestion => suggestion.value;
|
|
||||||
|
|
||||||
const renderSuggestion = suggestion => {
|
|
||||||
if (suggestion.type === 'account') {
|
|
||||||
return <AutosuggestAccountContainer id={suggestion.id} />;
|
|
||||||
} else {
|
|
||||||
return <span>#{suggestion.id}</span>
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderSectionTitle = section => (
|
|
||||||
<strong><FormattedMessage id={`search.${section.title}`} defaultMessage={section.title} /></strong>
|
|
||||||
);
|
|
||||||
|
|
||||||
const getSectionSuggestions = section => section.items;
|
|
||||||
|
|
||||||
const outerStyle = {
|
|
||||||
padding: '10px',
|
|
||||||
lineHeight: '20px',
|
|
||||||
position: 'relative'
|
|
||||||
};
|
|
||||||
|
|
||||||
const inputStyle = {
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
display: 'block',
|
|
||||||
width: '100%',
|
|
||||||
border: 'none',
|
|
||||||
padding: '10px',
|
|
||||||
paddingRight: '30px',
|
|
||||||
fontFamily: 'inherit',
|
|
||||||
background: '#282c37',
|
|
||||||
color: '#9baec8',
|
|
||||||
fontSize: '14px',
|
|
||||||
margin: '0'
|
|
||||||
};
|
|
||||||
|
|
||||||
const iconStyle = {
|
|
||||||
position: 'absolute',
|
|
||||||
top: '18px',
|
|
||||||
right: '20px',
|
|
||||||
color: '#9baec8',
|
|
||||||
fontSize: '18px',
|
|
||||||
pointerEvents: 'none'
|
|
||||||
};
|
|
||||||
|
|
||||||
const Search = React.createClass({
|
const Search = React.createClass({
|
||||||
|
|
||||||
contextTypes: {
|
|
||||||
router: React.PropTypes.object
|
|
||||||
},
|
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
suggestions: React.PropTypes.array.isRequired,
|
|
||||||
value: React.PropTypes.string.isRequired,
|
value: React.PropTypes.string.isRequired,
|
||||||
|
submitted: React.PropTypes.bool,
|
||||||
onChange: React.PropTypes.func.isRequired,
|
onChange: React.PropTypes.func.isRequired,
|
||||||
|
onSubmit: React.PropTypes.func.isRequired,
|
||||||
onClear: React.PropTypes.func.isRequired,
|
onClear: React.PropTypes.func.isRequired,
|
||||||
onFetch: React.PropTypes.func.isRequired,
|
onShow: React.PropTypes.func.isRequired,
|
||||||
onReset: React.PropTypes.func.isRequired
|
intl: React.PropTypes.object.isRequired
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
onChange (_, { newValue }) {
|
handleChange (e) {
|
||||||
if (typeof newValue !== 'string') {
|
this.props.onChange(e.target.value);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.onChange(newValue);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onSuggestionsClearRequested () {
|
handleClear (e) {
|
||||||
|
e.preventDefault();
|
||||||
this.props.onClear();
|
this.props.onClear();
|
||||||
},
|
},
|
||||||
|
|
||||||
@debounce(500)
|
handleKeyDown (e) {
|
||||||
onSuggestionsFetchRequested ({ value }) {
|
if (e.key === 'Enter') {
|
||||||
value = value.replace('#', '');
|
e.preventDefault();
|
||||||
this.props.onFetch(value.trim());
|
this.props.onSubmit();
|
||||||
},
|
|
||||||
|
|
||||||
onSuggestionSelected (_, { suggestion }) {
|
|
||||||
if (suggestion.type === 'account') {
|
|
||||||
this.context.router.push(`/accounts/${suggestion.id}`);
|
|
||||||
} else {
|
|
||||||
this.context.router.push(`/timelines/tag/${suggestion.id}`);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
noop () {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
handleFocus () {
|
||||||
|
this.props.onShow();
|
||||||
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const inputProps = {
|
const { intl, value, submitted } = this.props;
|
||||||
placeholder: this.props.intl.formatMessage(messages.placeholder),
|
const hasValue = value.length > 0 || submitted;
|
||||||
value: this.props.value,
|
|
||||||
onChange: this.onChange,
|
|
||||||
style: inputStyle
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={outerStyle}>
|
<div className='search'>
|
||||||
<Autosuggest
|
<input
|
||||||
multiSection={true}
|
className='search__input'
|
||||||
suggestions={this.props.suggestions}
|
type='text'
|
||||||
focusFirstSuggestion={true}
|
placeholder={intl.formatMessage(messages.placeholder)}
|
||||||
focusInputOnSuggestionClick={false}
|
value={value}
|
||||||
alwaysRenderSuggestions={false}
|
onChange={this.handleChange}
|
||||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
onKeyUp={this.handleKeyDown}
|
||||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
onFocus={this.handleFocus}
|
||||||
onSuggestionSelected={this.onSuggestionSelected}
|
|
||||||
getSuggestionValue={getSuggestionValue}
|
|
||||||
renderSuggestion={renderSuggestion}
|
|
||||||
renderSectionTitle={renderSectionTitle}
|
|
||||||
getSectionSuggestions={getSectionSuggestions}
|
|
||||||
inputProps={inputProps}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div style={iconStyle}><i className='fa fa-search' /></div>
|
<div role='button' tabIndex='0' className='search__icon' onClick={hasValue ? this.handleClear : this.noop}>
|
||||||
|
<i className={`fa fa-search ${hasValue ? '' : 'active'}`} />
|
||||||
|
<i aria-label="Clear search" className={`fa fa-times-circle ${hasValue ? 'active' : ''}`} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import AccountContainer from '../../../containers/account_container';
|
||||||
|
import StatusContainer from '../../../containers/status_container';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
|
const SearchResults = React.createClass({
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
results: ImmutablePropTypes.map.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { results } = this.props;
|
||||||
|
|
||||||
|
let accounts, statuses, hashtags;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
if (results.get('accounts') && results.get('accounts').size > 0) {
|
||||||
|
count += results.get('accounts').size;
|
||||||
|
accounts = (
|
||||||
|
<div className='search-results__section'>
|
||||||
|
{results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.get('statuses') && results.get('statuses').size > 0) {
|
||||||
|
count += results.get('statuses').size;
|
||||||
|
statuses = (
|
||||||
|
<div className='search-results__section'>
|
||||||
|
{results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.get('hashtags') && results.get('hashtags').size > 0) {
|
||||||
|
count += results.get('hashtags').size;
|
||||||
|
hashtags = (
|
||||||
|
<div className='search-results__section'>
|
||||||
|
{results.get('hashtags').map(hashtag =>
|
||||||
|
<Link className='search-results__hashtag' to={`/timelines/tag/${hashtag}`}>
|
||||||
|
#{hashtag}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='search-results'>
|
||||||
|
<div className='search-results__header'>
|
||||||
|
<FormattedMessage id='search_results.total' defaultMessage='{count} {count, plural, one {result} other {results}}' values={{ count }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{accounts}
|
||||||
|
{statuses}
|
||||||
|
{hashtags}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default SearchResults;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
|
|
||||||
|
const TextIconButton = React.createClass({
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
label: React.PropTypes.string.isRequired,
|
||||||
|
title: React.PropTypes.string,
|
||||||
|
active: React.PropTypes.bool,
|
||||||
|
onClick: React.PropTypes.func.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
|
handleClick (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onClick();
|
||||||
|
},
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { label, title, active } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button title={title} aria-label={title} className={`text-icon-button ${active ? 'active' : ''}`} onClick={this.handleClick}>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default TextIconButton;
|
||||||
@@ -6,6 +6,11 @@ const messages = defineMessages({
|
|||||||
upload: { id: 'upload_button.label', defaultMessage: 'Add media' }
|
upload: { id: 'upload_button.label', defaultMessage: 'Add media' }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const iconStyle = {
|
||||||
|
lineHeight: '27px',
|
||||||
|
height: null
|
||||||
|
};
|
||||||
|
|
||||||
const UploadButton = React.createClass({
|
const UploadButton = React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
@@ -37,7 +42,7 @@ const UploadButton = React.createClass({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={this.props.style}>
|
<div style={this.props.style}>
|
||||||
<IconButton icon='photo' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} size={24} />
|
<IconButton icon='camera' title={intl.formatMessage(messages.upload)} disabled={disabled} onClick={this.handleClick} style={iconStyle} size={18} inverted />
|
||||||
<input key={resetFileKey} ref={this.setRef} type='file' multiple={false} onChange={this.handleChange} disabled={disabled} style={{ display: 'none' }} />
|
<input key={resetFileKey} ref={this.setRef} type='file' multiple={false} onChange={this.handleChange} disabled={disabled} style={{ display: 'none' }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import PureRenderMixin from 'react-addons-pure-render-mixin';
|
|||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import IconButton from '../../../components/icon_button';
|
import IconButton from '../../../components/icon_button';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
import UploadProgressContainer from '../containers/upload_progress_container';
|
||||||
|
import { Motion, spring } from 'react-motion';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
undo: { id: 'upload_form.undo', defaultMessage: 'Undo' }
|
undo: { id: 'upload_form.undo', defaultMessage: 'Undo' }
|
||||||
@@ -11,7 +13,6 @@ const UploadForm = React.createClass({
|
|||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
media: ImmutablePropTypes.list.isRequired,
|
media: ImmutablePropTypes.list.isRequired,
|
||||||
is_uploading: React.PropTypes.bool,
|
|
||||||
onRemoveFile: React.PropTypes.func.isRequired,
|
onRemoveFile: React.PropTypes.func.isRequired,
|
||||||
intl: React.PropTypes.object.isRequired
|
intl: React.PropTypes.object.isRequired
|
||||||
},
|
},
|
||||||
@@ -21,21 +22,22 @@ const UploadForm = React.createClass({
|
|||||||
render () {
|
render () {
|
||||||
const { intl, media } = this.props;
|
const { intl, media } = this.props;
|
||||||
|
|
||||||
if (!media.size) {
|
const uploads = media.map(attachment =>
|
||||||
return null;
|
<div key={attachment.get('id')} style={{ margin: '5px', flex: '1 1 0' }}>
|
||||||
}
|
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
|
||||||
|
{({ scale }) =>
|
||||||
const uploads = media.map(attachment => (
|
<div style={{ transform: `translateZ(0) scale(${scale})`, width: '100%', height: '100px', borderRadius: '4px', background: `url(${attachment.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }}>
|
||||||
<div key={attachment.get('id')} style={{ borderRadius: '4px', marginBottom: '10px' }} className='transparent-background'>
|
<IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.props.onRemoveFile.bind(this, attachment.get('id'))} />
|
||||||
<div style={{ width: '100%', height: '100px', borderRadius: '4px', background: `url(${attachment.get('preview_url')}) no-repeat center`, backgroundSize: 'cover' }}>
|
</div>
|
||||||
<IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.props.onRemoveFile.bind(this, attachment.get('id'))} />
|
}
|
||||||
</div>
|
</Motion>
|
||||||
</div>
|
</div>
|
||||||
));
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBottom: '20px', padding: '10px', overflow: 'hidden', flexShrink: '0' }}>
|
<div style={{ overflow: 'hidden' }}>
|
||||||
{uploads}
|
<UploadProgressContainer />
|
||||||
|
<div style={{ display: 'flex', padding: '5px' }}>{uploads}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
|
import { Motion, spring } from 'react-motion';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
const UploadProgress = React.createClass({
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
active: React.PropTypes.bool,
|
||||||
|
progress: React.PropTypes.number
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [PureRenderMixin],
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { active, progress } = this.props;
|
||||||
|
|
||||||
|
if (!active) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='upload-progress'>
|
||||||
|
<div>
|
||||||
|
<i className='fa fa-upload' />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ flex: '1 1 auto' }}>
|
||||||
|
<FormattedMessage id='upload_progress.label' defaultMessage='Uploading...' />
|
||||||
|
|
||||||
|
<div className='upload-progress__backdrop'>
|
||||||
|
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}>
|
||||||
|
{({ width }) =>
|
||||||
|
<div className='upload-progress__tracker' style={{ width: `${width}%` }} />
|
||||||
|
}
|
||||||
|
</Motion>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default UploadProgress;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import AutosuggestStatus from '../components/autosuggest_status';
|
||||||
|
import { makeGetStatus } from '../../../selectors';
|
||||||
|
|
||||||
|
const makeMapStateToProps = () => {
|
||||||
|
const getStatus = makeGetStatus();
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { id }) => ({
|
||||||
|
status: getStatus(state, id)
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapStateToProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(makeMapStateToProps)(AutosuggestStatus);
|
||||||
@@ -1,91 +1,78 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import ComposeForm from '../components/compose_form';
|
import ComposeForm from '../components/compose_form';
|
||||||
|
import { uploadCompose } from '../../../actions/compose';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
import {
|
import {
|
||||||
changeCompose,
|
changeCompose,
|
||||||
submitCompose,
|
submitCompose,
|
||||||
cancelReplyCompose,
|
|
||||||
clearComposeSuggestions,
|
clearComposeSuggestions,
|
||||||
fetchComposeSuggestions,
|
fetchComposeSuggestions,
|
||||||
selectComposeSuggestion,
|
selectComposeSuggestion,
|
||||||
changeComposeSensitivity,
|
|
||||||
changeComposeSpoilerness,
|
|
||||||
changeComposeSpoilerText,
|
changeComposeSpoilerText,
|
||||||
changeComposeVisibility,
|
insertEmojiCompose
|
||||||
changeComposeListability
|
|
||||||
} from '../../../actions/compose';
|
} from '../../../actions/compose';
|
||||||
import { makeGetStatus } from '../../../selectors';
|
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const getMentionedUsernames = createSelector(state => state.getIn(['compose', 'text']), text => text.match(/(?:^|[^\/\w])@([a-z0-9_]+@[a-z0-9\.\-]+)/ig));
|
||||||
const getStatus = makeGetStatus();
|
|
||||||
|
|
||||||
const mapStateToProps = function (state, props) {
|
const getMentionedDomains = createSelector(getMentionedUsernames, mentionedUsernamesWithDomains => {
|
||||||
return {
|
return mentionedUsernamesWithDomains !== null ? [...new Set(mentionedUsernamesWithDomains.map(item => item.split('@')[2]))] : [];
|
||||||
text: state.getIn(['compose', 'text']),
|
});
|
||||||
suggestion_token: state.getIn(['compose', 'suggestion_token']),
|
|
||||||
suggestions: state.getIn(['compose', 'suggestions']),
|
|
||||||
sensitive: state.getIn(['compose', 'sensitive']),
|
|
||||||
spoiler: state.getIn(['compose', 'spoiler']),
|
|
||||||
spoiler_text: state.getIn(['compose', 'spoiler_text']),
|
|
||||||
unlisted: state.getIn(['compose', 'unlisted']),
|
|
||||||
private: state.getIn(['compose', 'private']),
|
|
||||||
fileDropDate: state.getIn(['compose', 'fileDropDate']),
|
|
||||||
is_submitting: state.getIn(['compose', 'is_submitting']),
|
|
||||||
is_uploading: state.getIn(['compose', 'is_uploading']),
|
|
||||||
in_reply_to: getStatus(state, state.getIn(['compose', 'in_reply_to'])),
|
|
||||||
media_count: state.getIn(['compose', 'media_attachments']).size,
|
|
||||||
me: state.getIn(['compose', 'me'])
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return mapStateToProps;
|
const mapStateToProps = (state, props) => {
|
||||||
};
|
const mentionedUsernames = getMentionedUsernames(state);
|
||||||
|
const mentionedUsernamesWithDomains = getMentionedDomains(state);
|
||||||
|
|
||||||
const mapDispatchToProps = function (dispatch) {
|
|
||||||
return {
|
return {
|
||||||
onChange (text) {
|
text: state.getIn(['compose', 'text']),
|
||||||
dispatch(changeCompose(text));
|
suggestion_token: state.getIn(['compose', 'suggestion_token']),
|
||||||
},
|
suggestions: state.getIn(['compose', 'suggestions']),
|
||||||
|
spoiler: state.getIn(['compose', 'spoiler']),
|
||||||
onSubmit () {
|
spoiler_text: state.getIn(['compose', 'spoiler_text']),
|
||||||
dispatch(submitCompose());
|
privacy: state.getIn(['compose', 'privacy']),
|
||||||
},
|
focusDate: state.getIn(['compose', 'focusDate']),
|
||||||
|
preselectDate: state.getIn(['compose', 'preselectDate']),
|
||||||
onCancelReply () {
|
is_submitting: state.getIn(['compose', 'is_submitting']),
|
||||||
dispatch(cancelReplyCompose());
|
is_uploading: state.getIn(['compose', 'is_uploading']),
|
||||||
},
|
me: state.getIn(['compose', 'me']),
|
||||||
|
needsPrivacyWarning: (state.getIn(['compose', 'privacy']) === 'private' || state.getIn(['compose', 'privacy']) === 'direct') && mentionedUsernames !== null,
|
||||||
onClearSuggestions () {
|
mentionedDomains: mentionedUsernamesWithDomains
|
||||||
dispatch(clearComposeSuggestions());
|
};
|
||||||
},
|
|
||||||
|
|
||||||
onFetchSuggestions (token) {
|
|
||||||
dispatch(fetchComposeSuggestions(token));
|
|
||||||
},
|
|
||||||
|
|
||||||
onSuggestionSelected (position, token, accountId) {
|
|
||||||
dispatch(selectComposeSuggestion(position, token, accountId));
|
|
||||||
},
|
|
||||||
|
|
||||||
onChangeSensitivity (checked) {
|
|
||||||
dispatch(changeComposeSensitivity(checked));
|
|
||||||
},
|
|
||||||
|
|
||||||
onChangeSpoilerness (checked) {
|
|
||||||
dispatch(changeComposeSpoilerness(checked));
|
|
||||||
},
|
|
||||||
|
|
||||||
onChangeSpoilerText (checked) {
|
|
||||||
dispatch(changeComposeSpoilerText(checked));
|
|
||||||
},
|
|
||||||
|
|
||||||
onChangeVisibility (checked) {
|
|
||||||
dispatch(changeComposeVisibility(checked));
|
|
||||||
},
|
|
||||||
|
|
||||||
onChangeListability (checked) {
|
|
||||||
dispatch(changeComposeListability(checked));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(makeMapStateToProps, mapDispatchToProps)(ComposeForm);
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
|
||||||
|
onChange (text) {
|
||||||
|
dispatch(changeCompose(text));
|
||||||
|
},
|
||||||
|
|
||||||
|
onSubmit () {
|
||||||
|
dispatch(submitCompose());
|
||||||
|
},
|
||||||
|
|
||||||
|
onClearSuggestions () {
|
||||||
|
dispatch(clearComposeSuggestions());
|
||||||
|
},
|
||||||
|
|
||||||
|
onFetchSuggestions (token) {
|
||||||
|
dispatch(fetchComposeSuggestions(token));
|
||||||
|
},
|
||||||
|
|
||||||
|
onSuggestionSelected (position, token, accountId) {
|
||||||
|
dispatch(selectComposeSuggestion(position, token, accountId));
|
||||||
|
},
|
||||||
|
|
||||||
|
onChangeSpoilerText (checked) {
|
||||||
|
dispatch(changeComposeSpoilerText(checked));
|
||||||
|
},
|
||||||
|
|
||||||
|
onPaste (files) {
|
||||||
|
dispatch(uploadCompose(files));
|
||||||
|
},
|
||||||
|
|
||||||
|
onPickEmoji (position, data) {
|
||||||
|
dispatch(insertEmojiCompose(position, data));
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ComposeForm);
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PrivacyDropdown from '../components/privacy_dropdown';
|
||||||
|
import { changeComposeVisibility } from '../../../actions/compose';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
value: state.getIn(['compose', 'privacy'])
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
||||||
|
onChange (value) {
|
||||||
|
dispatch(changeComposeVisibility(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(PrivacyDropdown);
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { cancelReplyCompose } from '../../../actions/compose';
|
||||||
|
import { makeGetStatus } from '../../../selectors';
|
||||||
|
import ReplyIndicator from '../components/reply_indicator';
|
||||||
|
|
||||||
|
const makeMapStateToProps = () => {
|
||||||
|
const getStatus = makeGetStatus();
|
||||||
|
|
||||||
|
const mapStateToProps = (state, props) => ({
|
||||||
|
status: getStatus(state, state.getIn(['compose', 'in_reply_to'])),
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapStateToProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
||||||
|
onCancel () {
|
||||||
|
dispatch(cancelReplyCompose());
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(makeMapStateToProps, mapDispatchToProps)(ReplyIndicator);
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import {
|
import {
|
||||||
changeSearch,
|
changeSearch,
|
||||||
clearSearchSuggestions,
|
clearSearch,
|
||||||
fetchSearchSuggestions,
|
submitSearch,
|
||||||
resetSearch
|
showSearch
|
||||||
} from '../../../actions/search';
|
} from '../../../actions/search';
|
||||||
import Search from '../components/search';
|
import Search from '../components/search';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
suggestions: state.getIn(['search', 'suggestions']),
|
value: state.getIn(['search', 'value']),
|
||||||
value: state.getIn(['search', 'value'])
|
submitted: state.getIn(['search', 'submitted'])
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
@@ -19,15 +19,15 @@ const mapDispatchToProps = dispatch => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onClear () {
|
onClear () {
|
||||||
dispatch(clearSearchSuggestions());
|
dispatch(clearSearch());
|
||||||
},
|
},
|
||||||
|
|
||||||
onFetch (value) {
|
onSubmit () {
|
||||||
dispatch(fetchSearchSuggestions(value));
|
dispatch(submitSearch());
|
||||||
},
|
},
|
||||||
|
|
||||||
onReset () {
|
onShow () {
|
||||||
dispatch(resetSearch());
|
dispatch(showSearch());
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import SearchResults from '../components/search_results';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
results: state.getIn(['search', 'results'])
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(SearchResults);
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import TextIconButton from '../components/text_icon_button';
|
||||||
|
import { changeComposeSensitivity } from '../../../actions/compose';
|
||||||
|
import { Motion, spring } from 'react-motion';
|
||||||
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'compose_form.sensitive', defaultMessage: 'Mark media as sensitive' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
visible: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||||
|
active: state.getIn(['compose', 'sensitive'])
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
||||||
|
onClick () {
|
||||||
|
dispatch(changeComposeSensitivity());
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
const SensitiveButton = React.createClass({
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
visible: React.PropTypes.bool,
|
||||||
|
active: React.PropTypes.bool,
|
||||||
|
onClick: React.PropTypes.func.isRequired,
|
||||||
|
intl: React.PropTypes.object.isRequired
|
||||||
|
},
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { visible, active, onClick, intl } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Motion defaultStyle={{ scale: 0.87 }} style={{ scale: spring(visible ? 1 : 0.87, { stiffness: 200, damping: 3 }) }}>
|
||||||
|
{({ scale }) =>
|
||||||
|
<div style={{ display: visible ? 'block' : 'none', transform: `translateZ(0) scale(${scale})` }}>
|
||||||
|
<TextIconButton onClick={onClick} label='NSFW' title={intl.formatMessage(messages.title)} active={active} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</Motion>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(SensitiveButton));
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import TextIconButton from '../components/text_icon_button';
|
||||||
|
import { changeComposeSpoilerness } from '../../../actions/compose';
|
||||||
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'compose_form.spoiler', defaultMessage: 'Hide text behind content warning' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { intl }) => ({
|
||||||
|
label: 'CW',
|
||||||
|
title: intl.formatMessage(messages.title),
|
||||||
|
active: state.getIn(['compose', 'spoiler'])
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
||||||
|
onClick () {
|
||||||
|
dispatch(changeComposeSpoilerness());
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(TextIconButton));
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import UploadProgress from '../components/upload_progress';
|
||||||
|
|
||||||
|
const mapStateToProps = (state, props) => ({
|
||||||
|
active: state.getIn(['compose', 'is_uploading']),
|
||||||
|
progress: state.getIn(['compose', 'progress'])
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(UploadProgress);
|
||||||
@@ -1,17 +1,34 @@
|
|||||||
import Drawer from './components/drawer';
|
|
||||||
import ComposeFormContainer from './containers/compose_form_container';
|
import ComposeFormContainer from './containers/compose_form_container';
|
||||||
import UploadFormContainer from './containers/upload_form_container';
|
import UploadFormContainer from './containers/upload_form_container';
|
||||||
import NavigationContainer from './containers/navigation_container';
|
import NavigationContainer from './containers/navigation_container';
|
||||||
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
||||||
import SearchContainer from './containers/search_container';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { mountCompose, unmountCompose } from '../../actions/compose';
|
import { mountCompose, unmountCompose } from '../../actions/compose';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
|
import SearchContainer from './containers/search_container';
|
||||||
|
import { Motion, spring } from 'react-motion';
|
||||||
|
import SearchResultsContainer from './containers/search_results_container';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
||||||
|
public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
|
||||||
|
community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
|
||||||
|
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||||
|
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden'])
|
||||||
|
});
|
||||||
|
|
||||||
const Compose = React.createClass({
|
const Compose = React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
dispatch: React.PropTypes.func.isRequired,
|
dispatch: React.PropTypes.func.isRequired,
|
||||||
withHeader: React.PropTypes.bool
|
withHeader: React.PropTypes.bool,
|
||||||
|
showSearch: React.PropTypes.bool,
|
||||||
|
intl: React.PropTypes.object.isRequired
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [PureRenderMixin],
|
mixins: [PureRenderMixin],
|
||||||
@@ -25,16 +42,46 @@ const Compose = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
const { withHeader, showSearch, intl } = this.props;
|
||||||
|
|
||||||
|
let header = '';
|
||||||
|
|
||||||
|
if (withHeader) {
|
||||||
|
header = (
|
||||||
|
<div className='drawer__header'>
|
||||||
|
<Link title={intl.formatMessage(messages.start)} className='drawer__tab' to='/getting-started'><i className='fa fa-fw fa-asterisk' /></Link>
|
||||||
|
<Link title={intl.formatMessage(messages.community)} className='drawer__tab' to='/timelines/public/local'><i className='fa fa-fw fa-users' /></Link>
|
||||||
|
<Link title={intl.formatMessage(messages.public)} className='drawer__tab' to='/timelines/public'><i className='fa fa-fw fa-globe' /></Link>
|
||||||
|
<a title={intl.formatMessage(messages.preferences)} className='drawer__tab' href='/settings/preferences'><i className='fa fa-fw fa-cog' /></a>
|
||||||
|
<a title={intl.formatMessage(messages.logout)} className='drawer__tab' href='/auth/sign_out' data-method='delete'><i className='fa fa-fw fa-sign-out' /></a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer withHeader={this.props.withHeader}>
|
<div className='drawer'>
|
||||||
|
{header}
|
||||||
|
|
||||||
<SearchContainer />
|
<SearchContainer />
|
||||||
<NavigationContainer />
|
|
||||||
<ComposeFormContainer />
|
<div className='drawer__pager'>
|
||||||
<UploadFormContainer />
|
<div className='drawer__inner'>
|
||||||
</Drawer>
|
<NavigationContainer />
|
||||||
|
<ComposeFormContainer />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
|
||||||
|
{({ x }) =>
|
||||||
|
<div className='drawer__inner darker' style={{ transform: `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
|
||||||
|
<SearchResultsContainer />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</Motion>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect()(Compose);
|
export default connect(mapStateToProps)(injectIntl(Compose));
|
||||||
|
|||||||
@@ -16,11 +16,8 @@ const outerStyle = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const panelStyle = {
|
const panelStyle = {
|
||||||
background: '#2f3441',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
borderTop: '1px solid #363c4b',
|
|
||||||
borderBottom: '1px solid #363c4b',
|
|
||||||
padding: '10px 0'
|
padding: '10px 0'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,14 +33,14 @@ const AccountAuthorize = ({ intl, account, onAuthorize, onReject }) => {
|
|||||||
<div>
|
<div>
|
||||||
<div style={outerStyle}>
|
<div style={outerStyle}>
|
||||||
<Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}>
|
<Permalink href={account.get('url')} to={`/accounts/${account.get('id')}`} className='detailed-status__display-name' style={{ display: 'block', overflow: 'hidden', marginBottom: '15px' }}>
|
||||||
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} size={48} /></div>
|
<div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} staticSrc={account.get('avatar_static')} size={48} /></div>
|
||||||
<DisplayName account={account} />
|
<DisplayName account={account} />
|
||||||
</Permalink>
|
</Permalink>
|
||||||
|
|
||||||
<div style={{ color: '#616b86', fontSize: '14px' }} className='account__header__content' dangerouslySetInnerHTML={content} />
|
<div style={{ fontSize: '14px' }} className='account__header__content' dangerouslySetInnerHTML={content} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={panelStyle}>
|
<div className='account--panel' style={panelStyle}>
|
||||||
<div style={btnStyle}><IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} /></div>
|
<div style={btnStyle}><IconButton title={intl.formatMessage(messages.authorize)} icon='check' onClick={onAuthorize} /></div>
|
||||||
<div style={btnStyle}><IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} /></div>
|
<div style={btnStyle}><IconButton title={intl.formatMessage(messages.reject)} icon='times' onClick={onReject} /></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const Followers = React.createClass({
|
|||||||
|
|
||||||
handleLoadMore (e) {
|
handleLoadMore (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.dispatch(expandFollowing(Number(this.props.params.accountId)));
|
this.props.dispatch(expandFollowers(Number(this.props.params.accountId)));
|
||||||
},
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user