mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6c40a5839 | ||
|
|
681e07306b | ||
|
|
794af5a670 | ||
|
|
81e8a593b3 | ||
|
|
89cb4c4502 | ||
|
|
e9c8876a8f | ||
|
|
e68124a426 | ||
|
|
8713a2ae87 | ||
|
|
54e7956af5 | ||
|
|
ee2068f5f1 | ||
|
|
52b3dcab43 | ||
|
|
a7cdcb0bf7 | ||
|
|
99a9154d40 | ||
|
|
3b96d08b58 | ||
|
|
82c5d140be | ||
|
|
7b6b46f3e5 | ||
|
|
d4569b5f51 | ||
|
|
6a9f216c0f | ||
|
|
6a1befc3f0 | ||
|
|
bab47c9d44 | ||
|
|
276e58060a | ||
|
|
8edc16ed85 | ||
|
|
8171cb53e1 | ||
|
|
cbf7cc67f4 | ||
|
|
d742d713cf | ||
|
|
178cecad05 | ||
|
|
460f3f292b | ||
|
|
5285c84de3 | ||
|
|
a8c361e2f7 | ||
|
|
21644bda8a | ||
|
|
75080cfe30 | ||
|
|
e510553d9a | ||
|
|
bb6be9a734 | ||
|
|
bd21719ea5 | ||
|
|
4ad2cd76c0 | ||
|
|
3c4b510829 | ||
|
|
add3cbf7b3 | ||
|
|
8266c65043 | ||
|
|
317f7134f2 | ||
|
|
9936b6ec47 | ||
|
|
1d96f67db6 | ||
|
|
9d00a27f1d | ||
|
|
f364a1e0a1 | ||
|
|
1aacb565d4 | ||
|
|
801a6f8a53 | ||
|
|
dbfd2aff7c | ||
|
|
3af49de0ce | ||
|
|
904aef0afc | ||
|
|
87ada30eff | ||
|
|
f32208b35b | ||
|
|
2c620c868d | ||
|
|
04fbeededa | ||
|
|
8cb9945e25 | ||
|
|
17ceadd8c9 | ||
|
|
3688d3f970 | ||
|
|
1d6455058e | ||
|
|
1374217680 | ||
|
|
f35a9ec8cd | ||
|
|
25230f2e9d | ||
|
|
f29df23c07 | ||
|
|
b3e494d279 | ||
|
|
13376504b5 | ||
|
|
12ec058588 | ||
|
|
5cbe9029aa | ||
|
|
6deee7cb80 | ||
|
|
c772cffc92 | ||
|
|
89b7918e41 | ||
|
|
b521934ee2 | ||
|
|
061c18670b | ||
|
|
0040bc4c01 | ||
|
|
0588a8ab66 | ||
|
|
859a0b1fbe | ||
|
|
d995cdde07 | ||
|
|
f641a3ec40 | ||
|
|
e80e79f267 | ||
|
|
ff75b9474a | ||
|
|
056ba9a04c | ||
|
|
d98ce38bf5 | ||
|
|
1d322d8c70 | ||
|
|
aaa2da6c23 | ||
|
|
5a272413e6 | ||
|
|
4dc83aaf63 | ||
|
|
12713dae86 | ||
|
|
555c593059 | ||
|
|
fb33eefe8e | ||
|
|
cfc6d4bfea | ||
|
|
6bbeee3a6b | ||
|
|
340d3cea62 | ||
|
|
e8e9dbe3ca | ||
|
|
cb45f0a8b3 | ||
|
|
cb1a4ee1cc | ||
|
|
d93ad38dd1 | ||
|
|
c4210db307 | ||
|
|
3fc1029a93 | ||
|
|
54abaeb587 | ||
|
|
4a97db3357 | ||
|
|
88377b3180 | ||
|
|
0dfec6e619 | ||
|
|
e343897526 | ||
|
|
294eb6c81d | ||
|
|
413eef8834 | ||
|
|
9eac05c8fb | ||
|
|
492b973206 | ||
|
|
2bdf413b88 | ||
|
|
76e87f4c34 | ||
|
|
c66e3141e0 | ||
|
|
a0f41bb19d | ||
|
|
056ff427a5 | ||
|
|
71c743dda7 | ||
|
|
fde4dee52f | ||
|
|
b49ace99eb | ||
|
|
d081c0c5b5 | ||
|
|
03ce044c53 | ||
|
|
31647fb2f0 | ||
|
|
f4b9191476 | ||
|
|
4ca5c4bc09 | ||
|
|
c21e500e58 | ||
|
|
7d858c7b3f | ||
|
|
c7e9fe3f5f | ||
|
|
e90ce7f4c3 | ||
|
|
62e8e4c5c5 | ||
|
|
3e2ed222ed | ||
|
|
7896b3f33b | ||
|
|
97121382c7 | ||
|
|
e561819c8d | ||
|
|
c7e0577708 | ||
|
|
2ab8cc9481 | ||
|
|
acdc00af18 | ||
|
|
f9cf5fb744 | ||
|
|
e1b80c3317 | ||
|
|
f37c1c3b5d | ||
|
|
ec19618052 | ||
|
|
81f51b69ec | ||
|
|
084bd6710f | ||
|
|
df843a23dd | ||
|
|
95e770aff8 | ||
|
|
d3252b8c87 | ||
|
|
1eada688fe | ||
|
|
4b6e562492 | ||
|
|
4d3788e4c9 | ||
|
|
50db2d1dda | ||
|
|
297dee6da8 | ||
|
|
2b7f545c2d | ||
|
|
b789a8e107 | ||
|
|
b2e5c0452a | ||
|
|
afa3123829 | ||
|
|
5aca83993b | ||
|
|
8ff0c3db51 | ||
|
|
74e1436ae4 | ||
|
|
def35f0a93 | ||
|
|
06001763ff | ||
|
|
75aa8d79df | ||
|
|
843c3a16c8 | ||
|
|
963827204e | ||
|
|
4e86e4197c | ||
|
|
5dae7bf4ac | ||
|
|
48370db09a | ||
|
|
a9207428fe | ||
|
|
9bee31198e | ||
|
|
2a95c1eee2 | ||
|
|
57ced201eb | ||
|
|
370cdf8c09 | ||
|
|
cb64d599fe | ||
|
|
63fa3c4de7 | ||
|
|
292f758ac9 | ||
|
|
76b96a66a2 | ||
|
|
8754ade5a4 | ||
|
|
e218de811b | ||
|
|
9403280c56 | ||
|
|
964cc362e7 | ||
|
|
a5c9a8e4b0 | ||
|
|
7c5e04e820 | ||
|
|
7fd5509490 | ||
|
|
5e8a02c461 | ||
|
|
ffd96f57e9 | ||
|
|
e3017efebf | ||
|
|
ed27209de9 | ||
|
|
e32ab99edf | ||
|
|
072437b0fd | ||
|
|
674ac50035 | ||
|
|
0eb88b30eb | ||
|
|
f4dfb21e15 | ||
|
|
e5a4fbc1cd | ||
|
|
ce583142ef | ||
|
|
95a7a1ca47 | ||
|
|
7857cadef5 | ||
|
|
47186c4eac | ||
|
|
ae5da93730 | ||
|
|
a65d045c65 | ||
|
|
08e7a90d69 | ||
|
|
547ba126be | ||
|
|
d497ad3369 | ||
|
|
14b483d089 | ||
|
|
5c5d014e23 | ||
|
|
923e20f867 | ||
|
|
cd7b1f2b2b | ||
|
|
c44eb373c3 | ||
|
|
49d0c9bd10 | ||
|
|
cb90802896 | ||
|
|
30bb954e44 | ||
|
|
4cd2a15f35 | ||
|
|
a1f3f550db | ||
|
|
604894e43c | ||
|
|
ebf4f7ecc9 | ||
|
|
964b934efb | ||
|
|
db94158426 | ||
|
|
ef502bf7a8 | ||
|
|
06b90041af | ||
|
|
c3048e599d | ||
|
|
1314f1972a | ||
|
|
0c07e39cc0 | ||
|
|
57d2b81b05 | ||
|
|
a75070cf2e | ||
|
|
2d6c26887a | ||
|
|
7626330ee1 | ||
|
|
2fc54ac529 | ||
|
|
10e8a32c21 | ||
|
|
0a2b26b599 | ||
|
|
4f74d28602 | ||
|
|
c3dff79e6e | ||
|
|
602c3d0be2 | ||
|
|
a3a8d23797 | ||
|
|
7313aaa473 | ||
|
|
74ffc00c20 | ||
|
|
2673de5059 | ||
|
|
644d00d5d8 | ||
|
|
75fb7b6dc2 | ||
|
|
07778844dc | ||
|
|
cb45e33b41 | ||
|
|
26e5631f52 | ||
|
|
3e1b7591e0 | ||
|
|
40ff265ab1 | ||
|
|
19bf7f6cf8 | ||
|
|
67c202d89f | ||
|
|
ad24ff78d2 | ||
|
|
f8b207e563 | ||
|
|
fe279587b5 | ||
|
|
ca51821d7b | ||
|
|
250a578dd7 | ||
|
|
17b9e43c14 | ||
|
|
785e5a4ac8 | ||
|
|
650e8e205e | ||
|
|
f37a210479 | ||
|
|
76d0fdfd67 | ||
|
|
3649605629 | ||
|
|
af89ef45f2 | ||
|
|
8c219c1c4d | ||
|
|
b81b789afa | ||
|
|
fa8d96b6be | ||
|
|
d8e7b25d0e | ||
|
|
52b0819e78 | ||
|
|
994b5006b6 | ||
|
|
a187472d5c | ||
|
|
a20bacd78d | ||
|
|
084f6b28ed | ||
|
|
87bc3e0a72 | ||
|
|
3c06ee9360 | ||
|
|
5b22bfbac2 | ||
|
|
c020008fad | ||
|
|
4eb20108b1 | ||
|
|
42db896d33 | ||
|
|
c0b526e98d | ||
|
|
743580fd08 | ||
|
|
788610a5f6 | ||
|
|
3129828f83 | ||
|
|
58ce5d5055 | ||
|
|
f9259ead50 | ||
|
|
6fb227eb72 | ||
|
|
8af7168f4f | ||
|
|
d77fcfe9de | ||
|
|
2490e65981 | ||
|
|
242f3ea5ea | ||
|
|
e30bdd1cdc | ||
|
|
5625f5b4b8 | ||
|
|
b2c8e3b254 | ||
|
|
3dff07de40 | ||
|
|
19d7a3323e | ||
|
|
dc86b87a62 | ||
|
|
6dd01b4334 | ||
|
|
b6e3796afe | ||
|
|
34ee1c56ea | ||
|
|
85d634c221 | ||
|
|
513a841aac | ||
|
|
88861754f1 | ||
|
|
fe58a5076e | ||
|
|
3e258b1787 | ||
|
|
b82cab0655 | ||
|
|
b547ad44f1 | ||
|
|
e04bf7f9bc | ||
|
|
50d832c2f8 | ||
|
|
0b06c233a8 | ||
|
|
7a3b36433a | ||
|
|
d478910a81 | ||
|
|
028e13761f | ||
|
|
57e7396997 | ||
|
|
80d08bbe2e | ||
|
|
5c44922d2b | ||
|
|
7077300dbc | ||
|
|
c00e6c1ece | ||
|
|
bc3c73a992 | ||
|
|
774a6f68ac | ||
|
|
b87c36078a | ||
|
|
6934263f71 | ||
|
|
cd18a7e76f | ||
|
|
b7de01b0ad | ||
|
|
011e5da26f | ||
|
|
0e931d023d | ||
|
|
295de61f36 | ||
|
|
096025b72a | ||
|
|
e2ed21e113 | ||
|
|
83ef41000c | ||
|
|
10796ed4b3 | ||
|
|
7f0b3c2ddd | ||
|
|
b019c5e180 | ||
|
|
a5f6e1f343 | ||
|
|
4c17cec4fd | ||
|
|
ce38b9da64 | ||
|
|
c920c7e9bc | ||
|
|
81879c9bb9 | ||
|
|
9fb76d35c8 | ||
|
|
a7099295d6 | ||
|
|
3d8c521928 | ||
|
|
95f9bac254 | ||
|
|
70bb6f6632 | ||
|
|
19d62a8c63 | ||
|
|
b15a2af38c | ||
|
|
4ff42385e3 | ||
|
|
dc81a59908 | ||
|
|
fe5ddbe323 | ||
|
|
f8e8eef989 | ||
|
|
9c4b636645 | ||
|
|
8212a7e11b | ||
|
|
3fc19df240 | ||
|
|
13e2382cd7 | ||
|
|
d0cca476e6 | ||
|
|
7ccf9552c2 | ||
|
|
519ca7beb5 | ||
|
|
5db48e8c0f | ||
|
|
1b7eedfd8d | ||
|
|
8b3f69d964 | ||
|
|
5f9240f201 | ||
|
|
f3f0b9c858 | ||
|
|
4507059a04 | ||
|
|
67b17160cb | ||
|
|
57b0b5ec67 | ||
|
|
d255ea84b1 | ||
|
|
04812797a0 | ||
|
|
636d29af9f | ||
|
|
9b94406763 | ||
|
|
9b659b024e | ||
|
|
b97fa228b1 | ||
|
|
0ba973fb44 | ||
|
|
3ac8f09619 | ||
|
|
ab9c2a89b7 | ||
|
|
e31961716e | ||
|
|
b83381719c | ||
|
|
5168f563ef | ||
|
|
7d0d4aa397 | ||
|
|
4f2126ef74 | ||
|
|
92fd146a8c | ||
|
|
c3f3ed8a50 | ||
|
|
3cb84222b2 | ||
|
|
818c09a9b8 | ||
|
|
c8e7ec6cbf | ||
|
|
d02c82d0cd | ||
|
|
26ebdde244 | ||
|
|
1cecad4d32 | ||
|
|
64c79cf5f2 | ||
|
|
6b4505fa3d | ||
|
|
f03c87c26d | ||
|
|
c15c312c2c | ||
|
|
ae3cbe990b | ||
|
|
d9a4653484 | ||
|
|
e993328dc4 | ||
|
|
4a42436b7b | ||
|
|
15afce516c | ||
|
|
3c6391e741 | ||
|
|
39bd0eca6f | ||
|
|
99b3d0021a | ||
|
|
6716362b08 | ||
|
|
68ea670ecd | ||
|
|
13a6ee67e9 | ||
|
|
32fb0714ce | ||
|
|
7cc8176cc4 | ||
|
|
354b51b4fe | ||
|
|
ae52ec983e | ||
|
|
c78e00711a | ||
|
|
b8ddade518 | ||
|
|
324e82c070 | ||
|
|
e6651086d5 | ||
|
|
28ea498df2 | ||
|
|
9e1b470cb5 | ||
|
|
b9a3466e2e | ||
|
|
e60bcb9caf | ||
|
|
aa18a433f8 | ||
|
|
b7d3113677 | ||
|
|
756b5f8b45 | ||
|
|
860030a57c | ||
|
|
a46773887f | ||
|
|
ea7888809e | ||
|
|
cc0a23c295 | ||
|
|
b444316e1c | ||
|
|
17128e9a47 | ||
|
|
f0c846493a | ||
|
|
f6e2caf65f | ||
|
|
cdc1835f72 | ||
|
|
9d2b47f02b | ||
|
|
00a2a65d31 | ||
|
|
da5e8cda99 | ||
|
|
32dd554331 | ||
|
|
8293408c52 | ||
|
|
23a5b236a2 | ||
|
|
b02088e329 | ||
|
|
1a50b6cc85 | ||
|
|
f1bebaa627 | ||
|
|
1250054fb7 | ||
|
|
f3980a03b2 | ||
|
|
a90802a046 | ||
|
|
400615cc58 | ||
|
|
a94f6220f9 | ||
|
|
c65467fedf | ||
|
|
f509d29a94 | ||
|
|
8bed64373e | ||
|
|
322434f751 | ||
|
|
9ee749edb2 | ||
|
|
5c14ac3f67 | ||
|
|
e1929f148a | ||
|
|
717ebf40b7 | ||
|
|
ef40174be9 | ||
|
|
3b819144e0 | ||
|
|
c58813cf48 | ||
|
|
d14b965f54 | ||
|
|
ed88a59ef1 | ||
|
|
ffcb1b464f | ||
|
|
fd11cc5249 | ||
|
|
9b155833a1 | ||
|
|
2484141ecf | ||
|
|
78ea42f34f | ||
|
|
3a1e78579a | ||
|
|
c85596a9bf | ||
|
|
211a6fd868 | ||
|
|
59b048002b | ||
|
|
5506481182 | ||
|
|
12e6501d4d | ||
|
|
7a53cab2f5 | ||
|
|
cfbff62d1c | ||
|
|
5d4c813800 | ||
|
|
e486d48556 | ||
|
|
8663aa0cb5 | ||
|
|
f1e70102d0 | ||
|
|
41d4bd1012 | ||
|
|
ace3e2398b | ||
|
|
521082eb3e | ||
|
|
a6dd03beef | ||
|
|
7246adc467 | ||
|
|
4d0b5b7149 | ||
|
|
bfb2eb30e2 | ||
|
|
5d26f8e3de | ||
|
|
a46c8528e1 | ||
|
|
e3d5850af6 | ||
|
|
cd9648b251 | ||
|
|
59ddde1856 | ||
|
|
862355b0cd | ||
|
|
78878b4295 | ||
|
|
b1d4fff929 | ||
|
|
8cf11d83c8 | ||
|
|
a3ec23c273 | ||
|
|
af9f143b31 | ||
|
|
48bffdbb3b | ||
|
|
8f1448823c | ||
|
|
6a46fb822d | ||
|
|
b186a74199 | ||
|
|
e96c5bbc8f | ||
|
|
06378209a2 | ||
|
|
f6cede3fda | ||
|
|
d5b800f590 | ||
|
|
8cade04431 | ||
|
|
efda925efc | ||
|
|
ec915208e5 | ||
|
|
d069cf9d8e | ||
|
|
af80c4d8ad | ||
|
|
1de3614fe8 | ||
|
|
663bce2875 | ||
|
|
d0090283c0 | ||
|
|
8c53bc2d22 | ||
|
|
f47bd269e5 | ||
|
|
7a99c9358a | ||
|
|
6330d69649 | ||
|
|
6e98d176e7 | ||
|
|
7b84b3daf1 | ||
|
|
40b7669ed8 | ||
|
|
3bb994c562 | ||
|
|
fab14d896b | ||
|
|
9c97d85859 | ||
|
|
ea8e469c1c | ||
|
|
6e3159c7e1 | ||
|
|
5559940967 | ||
|
|
64eb16c3d6 | ||
|
|
f779c5fe01 | ||
|
|
6972b87b0b | ||
|
|
8c320d09da | ||
|
|
279ff0655f | ||
|
|
794cc0fc6c | ||
|
|
7b16a71e9b | ||
|
|
20b4c4e2d0 | ||
|
|
37e093f60a | ||
|
|
de6492a26d | ||
|
|
c60db41047 | ||
|
|
00fe4b7c83 | ||
|
|
e0b177d955 | ||
|
|
29e398c51d | ||
|
|
3dd2da96e8 | ||
|
|
1fea1405ef | ||
|
|
9fa3e361b5 | ||
|
|
06ba5a53c1 | ||
|
|
bb532b5eb2 | ||
|
|
5eb26946cb | ||
|
|
81680442f0 | ||
|
|
c86f9877f3 | ||
|
|
1b3868edec | ||
|
|
4936003f5f | ||
|
|
5c4696ed61 | ||
|
|
bf898fbaec | ||
|
|
41065ba24a | ||
|
|
0e77c93ae5 | ||
|
|
2756b794ee | ||
|
|
626f5cd1e1 | ||
|
|
077daeb793 | ||
|
|
fc567e8a92 | ||
|
|
60d2735fa5 | ||
|
|
6f58754617 | ||
|
|
e935fff8ac | ||
|
|
3ad8d9ffec | ||
|
|
0df40c7e35 | ||
|
|
a793d63f65 | ||
|
|
ccc512cddd | ||
|
|
84016e362d | ||
|
|
e20b6e1b25 | ||
|
|
4ff37d9063 | ||
|
|
c02890af00 | ||
|
|
add7bfc170 | ||
|
|
cea0b4942a | ||
|
|
4ddaca7655 | ||
|
|
03e0c62b72 | ||
|
|
a21d73dcad | ||
|
|
b20d8c18a3 | ||
|
|
85674b3ef5 | ||
|
|
80f695e23d | ||
|
|
8b4c2876c7 | ||
|
|
d56c35ea64 | ||
|
|
9ff2ccf1ed | ||
|
|
0807663965 | ||
|
|
f296fe2abd | ||
|
|
2654e3d452 | ||
|
|
2c2c2dc604 | ||
|
|
aad66ec7e0 | ||
|
|
94e429e431 | ||
|
|
7db5e4d68b | ||
|
|
f3822d09b2 | ||
|
|
9e35b7b39f | ||
|
|
33d71df049 | ||
|
|
7d0aac5d4a | ||
|
|
10cd53b40b | ||
|
|
d4c4b5a3e9 | ||
|
|
b88cbe30de | ||
|
|
9c448d5143 | ||
|
|
cb5228a423 | ||
|
|
bd91f898f5 | ||
|
|
4f70a24a32 | ||
|
|
62835acc76 | ||
|
|
67301e6e85 | ||
|
|
ad4386b235 | ||
|
|
a94e6a0ada | ||
|
|
89f0663723 | ||
|
|
eb9846ba40 | ||
|
|
9d7524644c | ||
|
|
f142f3a27f | ||
|
|
df2c33ac35 | ||
|
|
5666c4ac90 | ||
|
|
37ac5157dd | ||
|
|
5db9ea6520 | ||
|
|
f79af56468 | ||
|
|
a4b9f86e29 | ||
|
|
cee1c5bc1c | ||
|
|
15d410ba12 | ||
|
|
3a7b17750e | ||
|
|
4bacc2b0c2 | ||
|
|
3d10534da1 | ||
|
|
54ea71bedf | ||
|
|
19a83a902a | ||
|
|
7d56404e84 | ||
|
|
b71b7cc883 | ||
|
|
aaa5d17ca3 | ||
|
|
860f434335 | ||
|
|
027358913d | ||
|
|
a91db35f91 | ||
|
|
c261cdc0c6 | ||
|
|
11fb5421b5 | ||
|
|
4dc3b0c2f4 | ||
|
|
f18191793b | ||
|
|
2743f6faf6 | ||
|
|
a41108a53c | ||
|
|
e337a0d18b | ||
|
|
e9c81f0639 | ||
|
|
441ea12a65 | ||
|
|
d1ab279f74 | ||
|
|
387e52b510 | ||
|
|
2a2a069cea | ||
|
|
f20f0c5d10 | ||
|
|
b559b81198 | ||
|
|
1f562c8fad | ||
|
|
3cd9e802dd | ||
|
|
34fee9439f | ||
|
|
0e6b0c01fd | ||
|
|
6fb2a95bf2 | ||
|
|
d5d424bfc5 | ||
|
|
ecbb66e270 | ||
|
|
45094fd176 | ||
|
|
e1c8e75405 | ||
|
|
e9331e90d3 | ||
|
|
d3b81dff35 | ||
|
|
af5be0469f | ||
|
|
b40eed4751 | ||
|
|
673bbb61b1 | ||
|
|
449a6f67f1 | ||
|
|
df859dc782 | ||
|
|
7fac8ef073 | ||
|
|
9f8c9c7eb1 | ||
|
|
ab74ebdc52 | ||
|
|
97e3d7220e | ||
|
|
7acaaadcd6 | ||
|
|
c12660be3b | ||
|
|
ffb0ee20e8 | ||
|
|
e6dba11984 | ||
|
|
b335cb3493 | ||
|
|
6735296c3c | ||
|
|
350d9e74f2 | ||
|
|
b3582025b7 | ||
|
|
3c91b9cda1 | ||
|
|
1fecd4589b | ||
|
|
48b7a67665 | ||
|
|
45c7cbad9e | ||
|
|
75c88df8f1 | ||
|
|
07d1f9d3cf | ||
|
|
dd3d1e9fe7 | ||
|
|
ffbe976760 | ||
|
|
0e0c3f19aa | ||
|
|
63ef49d595 |
@@ -3,5 +3,10 @@ client/lib
|
||||
**/*.html
|
||||
plugins/*
|
||||
!plugins/coral-plugin-facebook-auth
|
||||
!plugins/coral-plugin-auth
|
||||
!plugins/coral-plugin-respect
|
||||
!plugins/coral-plugin-offtopic
|
||||
!plugins/coral-plugin-like
|
||||
!plugins/coral-plugin-mod
|
||||
!plugins/coral-plugin-love
|
||||
node_modules
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
"no-template-curly-in-string": [1],
|
||||
"no-unsafe-negation": [1],
|
||||
"array-callback-return": [1],
|
||||
"arrow-parens": ["warn", "always"],
|
||||
"template-curly-spacing": "warn",
|
||||
"eqeqeq": [2, "smart"],
|
||||
"no-eval": [2],
|
||||
"no-global-assign": [2],
|
||||
@@ -36,6 +38,11 @@
|
||||
"no-unneeded-ternary": [1],
|
||||
"object-curly-spacing": [1],
|
||||
"space-infix-ops": ["error"],
|
||||
"space-in-parens": ["error", "never"],
|
||||
"space-unary-ops": ["error", {
|
||||
"words": true,
|
||||
"nonwords": false
|
||||
}],
|
||||
"no-const-assign": [2],
|
||||
"no-duplicate-imports": [2],
|
||||
"prefer-template": [1],
|
||||
|
||||
@@ -17,7 +17,11 @@ coverage/
|
||||
plugins.json
|
||||
plugins/*
|
||||
!plugins/coral-plugin-facebook-auth
|
||||
!plugins/coral-plugin-auth
|
||||
!plugins/coral-plugin-respect
|
||||
!plugins/coral-plugin-offtopic
|
||||
!plugins/coral-plugin-like
|
||||
!plugins/coral-plugin-mod
|
||||
!plugins/coral-plugin-love
|
||||
|
||||
**/node_modules/*
|
||||
|
||||
+2
-1
@@ -13,7 +13,8 @@ EXPOSE 5000
|
||||
COPY . /usr/src/app
|
||||
|
||||
# Install app dependencies and build static assets.
|
||||
RUN yarn install --frozen-lockfile && \
|
||||
RUN yarn global add node-gyp && \
|
||||
yarn install --frozen-lockfile && \
|
||||
cli plugins reconcile && \
|
||||
yarn build && \
|
||||
yarn install --production && \
|
||||
|
||||
+1
-1
@@ -243,7 +243,7 @@ file under the `scripts` key including:
|
||||
# Setup
|
||||
|
||||
Once you've installed Talk (either via Docker or source), you still need to
|
||||
setup the application. If you are unfamiliar with any terminoligy used in the
|
||||
setup the application. If you are unfamiliar with any terminology used in the
|
||||
setup process, refer to the `TERMINOLOGY.md` document.
|
||||
|
||||
## Via Web
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright 2016 Mozilla Foundation
|
||||
Copyright 2017 Mozilla Foundation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
+32
@@ -145,6 +145,10 @@ type RootMutation {
|
||||
type RootQuery {
|
||||
people: [Person!]
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
leader: Person
|
||||
}
|
||||
```
|
||||
|
||||
Thanks to [gql-merge](https://www.npmjs.com/package/gql-merge) the contents of
|
||||
@@ -259,6 +263,23 @@ If your post function accepts four parameters, then it can modify the field
|
||||
result. It is *required* that the function resolves a promise (or returns) with
|
||||
the modified value or simply the original if you didn't modify it.
|
||||
|
||||
#### Field: `setupFunctions`
|
||||
|
||||
```js
|
||||
setupFunctions: {
|
||||
leader: (options, args) => ({
|
||||
leader: {
|
||||
filter: (person) => person.place === 1
|
||||
},
|
||||
}),
|
||||
}
|
||||
```
|
||||
|
||||
Setup functions allow you to create filters that control which pubsub.publish() events
|
||||
send data to the client. If the type in question contains args, clients may subscribe using those arguments to further filter their subscription.
|
||||
|
||||
For more information, see the [Apollo Docs](https://github.com/apollographql/graphql-subscriptions).
|
||||
|
||||
#### Field: `router`
|
||||
|
||||
```js
|
||||
@@ -375,6 +396,10 @@ module.exports = {
|
||||
type RootQuery {
|
||||
people: [Person!]
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
leader: Person
|
||||
}
|
||||
`,
|
||||
context: {
|
||||
Slack: () => ({
|
||||
@@ -430,6 +455,13 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
setupFunctions: {
|
||||
leader: (options, args) => ({
|
||||
leader: {
|
||||
filter: (person) => person.place === 1
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Talk [](https://circleci.com/gh/coralproject/talk)
|
||||
|
||||
Talk is currently in Beta! [Read more about Talk here.](https://coralproject.net/products/talk.html)
|
||||
Online comments are broken. Our open-source Talk tool rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here.](https://coralproject.net/products/talk.html)
|
||||
|
||||
Third party licenses are available via the `/client/3rdpartylicenses.txt`
|
||||
endpoint when the server is running with built assets.
|
||||
@@ -24,10 +24,13 @@ The Talk application looks for the following configuration values either as envi
|
||||
|
||||
- `TALK_MONGO_URL` (*required*) - the database connection string for the MongoDB database.
|
||||
- `TALK_REDIS_URL` (*required*) - the database connection string for the Redis database.
|
||||
- `TALK_SESSION_SECRET` (*required*) - a random string which will be used to
|
||||
secure cookies.
|
||||
- `TALK_ROOT_URL` (*required*) - root url of the installed application externally
|
||||
available in the format: `<scheme>://<host>` without the path.
|
||||
- `TALK_JWT_SECRET` (*required*) - a long and cryptographical secure random string which will be used to
|
||||
sign and verify tokens via a `HS256` algorithm.
|
||||
- `TALK_JWT_EXPIRY` (_optional_) - the expiry duration (`exp`) for the tokens issued for logged in sessions (Default `1 day`)
|
||||
- `TALK_JWT_ISSUER` (_optional_) - the issuer (`iss`) claim for login JWT tokens (Default `process.env.TALK_ROOT_URL`)
|
||||
- `TALK_JWT_AUDIENCE` (_optional_) - the audience (`aud`) claim for login JWT tokens (Default `talk`)
|
||||
- `TALK_SMTP_EMAIL` (*required for email*) - the address to send emails from using the
|
||||
SMTP provider.
|
||||
- `TALK_SMTP_USERNAME` (*required for email*) - username of the SMTP provider you are using.
|
||||
@@ -42,9 +45,45 @@ available in the format: `<scheme>://<host>` without the path.
|
||||
Refer to the wiki page on [Configuration Loading](https://github.com/coralproject/talk/wiki/Configuration-Loading) for
|
||||
alternative methods of loading configuration during development.
|
||||
|
||||
### License
|
||||
## Supported Browsers
|
||||
|
||||
Copyright 2016 Mozilla Foundation
|
||||
### Web
|
||||
|
||||
- Chrome: latest 2 versions
|
||||
- Firefox: latest 2 versions, and most recent extended support version, if any
|
||||
- Safari: latest 2 versions
|
||||
- Internet Explorer: IE Edge, 11
|
||||
|
||||
### iOS Devices
|
||||
|
||||
- iPad
|
||||
- iPad Pro
|
||||
- iPhone 7 Plus
|
||||
- iPhone 7
|
||||
- iPhone 6 Plus
|
||||
- iPhone 6
|
||||
- iPhone 5
|
||||
|
||||
### iOS Browsers
|
||||
|
||||
- Chrome for iOS: latest version
|
||||
- Firefox for iOS: latest version
|
||||
- Safari for iOS: latest version
|
||||
|
||||
### Android Devices
|
||||
|
||||
- Galaxy S5
|
||||
- Nexus 5X
|
||||
- Nexus 6P
|
||||
|
||||
### Android Browsers
|
||||
|
||||
- Chrome for Android: latest version
|
||||
- Firefox for Android: latest version
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2017 Mozilla Foundation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -3,16 +3,16 @@ const bodyParser = require('body-parser');
|
||||
const morgan = require('morgan');
|
||||
const path = require('path');
|
||||
const helmet = require('helmet');
|
||||
const authentication = require('./middleware/authentication');
|
||||
const {passport} = require('./services/passport');
|
||||
const plugins = require('./services/plugins');
|
||||
const session = require('express-session');
|
||||
const enabled = require('debug').enabled;
|
||||
const RedisStore = require('connect-redis')(session);
|
||||
const redis = require('./services/redis');
|
||||
const csrf = require('csurf');
|
||||
const errors = require('./errors');
|
||||
const graph = require('./graph');
|
||||
const {createGraphOptions} = require('./graph');
|
||||
const apollo = require('graphql-server-express');
|
||||
const accepts = require('accepts');
|
||||
const compression = require('compression');
|
||||
const cookieParser = require('cookie-parser');
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -33,44 +33,44 @@ app.set('trust proxy', 1);
|
||||
app.use(helmet({
|
||||
frameguard: false
|
||||
}));
|
||||
app.use(compression());
|
||||
app.use(cookieParser());
|
||||
app.use(bodyParser.json());
|
||||
app.use('/client', express.static(path.join(__dirname, 'dist')));
|
||||
app.use('/public', express.static(path.join(__dirname, 'public')));
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
//==============================================================================
|
||||
// SESSION MIDDLEWARE
|
||||
// STATIC FILES
|
||||
//==============================================================================
|
||||
|
||||
const session_opts = {
|
||||
secret: process.env.TALK_SESSION_SECRET,
|
||||
httpOnly: true,
|
||||
rolling: true,
|
||||
saveUninitialized: true,
|
||||
resave: true,
|
||||
unset: 'destroy',
|
||||
name: 'talk.sid',
|
||||
cookie: {
|
||||
secure: false,
|
||||
maxAge: 8.64e+7, // 24 hours for session token expiry
|
||||
},
|
||||
store: new RedisStore({
|
||||
client: redis.createClient(),
|
||||
})
|
||||
};
|
||||
// If the application is in production mode, then add gzip rewriting for the
|
||||
// content.
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
app.get('*.js', (req, res, next) => {
|
||||
const accept = accepts(req);
|
||||
if (accept.encoding(['gzip']) === 'gzip') {
|
||||
|
||||
if (app.get('env') === 'production') {
|
||||
// Adjsut the headers on the request by adding a content type header
|
||||
// because express won't be able to detect the mime-type with the .gz
|
||||
// extension and we need to decalre support for the gzip encoding.
|
||||
res.set('Content-Type', 'application/javascript');
|
||||
res.set('Content-Encoding', 'gzip');
|
||||
|
||||
// Enable the secure cookie when we are in production mode.
|
||||
session_opts.cookie.secure = true;
|
||||
} else if (app.get('env') === 'test') {
|
||||
// Rewrite the url so that the gzip version will be served instead.
|
||||
req.url = `${req.url}.gz`;
|
||||
}
|
||||
|
||||
// Add in the secret during tests.
|
||||
session_opts.secret = 'keyboard cat';
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
app.use(session(session_opts));
|
||||
app.use('/client', express.static(path.join(__dirname, 'dist')));
|
||||
app.use('/public', express.static(path.join(__dirname, 'public')));
|
||||
|
||||
//==============================================================================
|
||||
// VIEW CONFIGURATION
|
||||
//==============================================================================
|
||||
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
//==============================================================================
|
||||
// PASSPORT MIDDLEWARE
|
||||
@@ -89,22 +89,27 @@ plugins.get('server', 'passport').forEach((plugin) => {
|
||||
|
||||
// Setup the PassportJS Middleware.
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
// Attach the authentication middleware, this will be responsible for decoding
|
||||
// (if present) the JWT on the request.
|
||||
app.use('/api', authentication);
|
||||
|
||||
//==============================================================================
|
||||
// GraphQL Router
|
||||
//==============================================================================
|
||||
|
||||
// GraphQL endpoint.
|
||||
app.use('/api/v1/graph/ql', apollo.graphqlExpress(graph.createGraphOptions));
|
||||
app.use('/api/v1/graph/ql', apollo.graphqlExpress(createGraphOptions));
|
||||
|
||||
// Only include the graphiql tool if we aren't in production mode.
|
||||
if (app.get('env') !== 'production') {
|
||||
|
||||
// Interactive graphiql interface.
|
||||
app.use('/api/v1/graph/iql', apollo.graphiqlExpress({
|
||||
endpointURL: '/api/v1/graph/ql'
|
||||
}));
|
||||
app.use('/api/v1/graph/iql', (req, res) => {
|
||||
res.render('graphiql', {
|
||||
endpointURL: '/api/v1/graph/ql'
|
||||
});
|
||||
});
|
||||
|
||||
// GraphQL documention.
|
||||
app.get('/admin/docs', (req, res) => {
|
||||
@@ -113,29 +118,6 @@ if (app.get('env') !== 'production') {
|
||||
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// CSRF MIDDLEWARE
|
||||
//==============================================================================
|
||||
|
||||
if (process.env.TEST_MODE === 'unit') {
|
||||
|
||||
// Add this fake test token in the event we are in unit test mode, and don't
|
||||
// include the CSRF protection.
|
||||
app.locals.csrfToken = 'UNIT_TESTS';
|
||||
|
||||
} else {
|
||||
|
||||
// Setup route middlewares for CSRF protection.
|
||||
// Default ignore methods are GET, HEAD, OPTIONS
|
||||
app.use(csrf({}));
|
||||
app.use((req, res, next) => {
|
||||
res.locals.csrfToken = req.csrfToken();
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// ROUTES
|
||||
//==============================================================================
|
||||
|
||||
+24
-11
@@ -1,26 +1,29 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const app = require('../app');
|
||||
const program = require('./commander');
|
||||
const http = require('http');
|
||||
const app = require('../app');
|
||||
const {createServer} = require('http');
|
||||
const scraper = require('../services/scraper');
|
||||
const mailer = require('../services/mailer');
|
||||
const kue = require('../services/kue');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const util = require('./util');
|
||||
const {createSubscriptionManager} = require('../graph/subscriptions');
|
||||
const {
|
||||
PORT
|
||||
} = require('../config');
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
|
||||
const port = normalizePort(process.env.TALK_PORT || '3000');
|
||||
|
||||
const port = normalizePort(PORT);
|
||||
app.set('port', port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
const server = http.createServer(app);
|
||||
const server = createServer(app);
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
@@ -76,20 +79,29 @@ function normalizePort(val) {
|
||||
function onListening() {
|
||||
let addr = server.address();
|
||||
let bind = typeof addr === 'string'
|
||||
? `pipe ${ addr}`
|
||||
: `port ${ addr.port}`;
|
||||
console.log(`Listening on ${ bind}`);
|
||||
? `pipe ${addr}`
|
||||
: `port ${addr.port}`;
|
||||
console.log(`API Server Listening on ${bind}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the app.
|
||||
*/
|
||||
function startApp() {
|
||||
function startApp(program) {
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
server.listen(port);
|
||||
server.listen(port, () => {
|
||||
|
||||
// Mount the websocket server if requested.
|
||||
if (program.websockets) {
|
||||
console.log(`Websocket Server Listening on ${port}`);
|
||||
|
||||
// Mount the subscriptions server on the application server.
|
||||
createSubscriptionManager(server);
|
||||
}
|
||||
});
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
}
|
||||
@@ -100,10 +112,11 @@ function startApp() {
|
||||
|
||||
program
|
||||
.option('-j, --jobs', 'enable job processing on this thread')
|
||||
.option('-w, --websockets', 'enable the websocket (subscriptions) handler on this thread')
|
||||
.parse(process.argv);
|
||||
|
||||
// Start the application serving.
|
||||
startApp();
|
||||
startApp(program);
|
||||
|
||||
// Enable job processing on the thread if enabled.
|
||||
if (program.jobs) {
|
||||
|
||||
@@ -3,11 +3,6 @@ const dotenv = require('dotenv');
|
||||
const fs = require('fs');
|
||||
const program = require('commander');
|
||||
|
||||
// Perform rewrites to the runtime environment variables based on the contents
|
||||
// of the process.env.REWRITE_ENV if it exists. This is done here as it is the
|
||||
// entrypoint for the entire application.
|
||||
require('env-rewrite').rewrite();
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import {Router, Route, IndexRoute, IndexRedirect, browserHistory} from 'react-router';
|
||||
import {Router, Route, IndexRedirect, browserHistory} from 'react-router';
|
||||
|
||||
import Stories from 'containers/Stories/Stories';
|
||||
import Configure from 'containers/Configure/Configure';
|
||||
@@ -18,7 +18,7 @@ const routes = (
|
||||
<div>
|
||||
<Route exact path="/admin/install" component={InstallContainer}/>
|
||||
<Route path='/admin' component={LayoutContainer}>
|
||||
<IndexRoute component={Dashboard} />
|
||||
<IndexRedirect to='/admin/moderate/all' />
|
||||
<Route path='community' component={CommunityContainer} />
|
||||
<Route path='configure' component={Configure} />
|
||||
<Route path='stories' component={Stories} />
|
||||
@@ -39,6 +39,12 @@ const routes = (
|
||||
{/* Moderation Routes */}
|
||||
|
||||
<Route path='moderate' component={ModerationLayout}>
|
||||
<Route path='all' components={ModerationContainer}>
|
||||
<Route path=':id' components={ModerationContainer} />
|
||||
</Route>
|
||||
<Route path='accepted' components={ModerationContainer}>
|
||||
<Route path=':id' components={ModerationContainer} />
|
||||
</Route>
|
||||
<Route path='premod' components={ModerationContainer}>
|
||||
<Route path=':id' components={ModerationContainer} />
|
||||
</Route>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
UPDATE_ASSETS
|
||||
} from '../constants/assets';
|
||||
|
||||
import coralApi from '../../../coral-framework/helpers/response';
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
|
||||
/**
|
||||
* Action disptacher related to assets
|
||||
@@ -24,7 +24,7 @@ export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filte
|
||||
assets: result,
|
||||
count
|
||||
}))
|
||||
.catch(error => dispatch({type: FETCH_ASSETS_FAILURE, error}));
|
||||
.catch((error) => dispatch({type: FETCH_ASSETS_FAILURE, error}));
|
||||
};
|
||||
|
||||
// Update an asset state
|
||||
@@ -34,9 +34,9 @@ export const updateAssetState = (id, closedAt) => (dispatch) => {
|
||||
return coralApi(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}})
|
||||
.then(() =>
|
||||
dispatch({type: UPDATE_ASSET_STATE_SUCCESS}))
|
||||
.catch(error => dispatch({type: UPDATE_ASSET_STATE_FAILURE, error}));
|
||||
.catch((error) => dispatch({type: UPDATE_ASSET_STATE_FAILURE, error}));
|
||||
};
|
||||
|
||||
export const updateAssets = assets => dispatch => {
|
||||
export const updateAssets = (assets) => (dispatch) => {
|
||||
dispatch({type: UPDATE_ASSETS, assets});
|
||||
};
|
||||
|
||||
@@ -1,75 +1,114 @@
|
||||
import bowser from 'bowser';
|
||||
import * as actions from '../constants/auth';
|
||||
import coralApi from 'coral-framework/helpers/response';
|
||||
import coralApi from 'coral-framework/helpers/request';
|
||||
import * as Storage from 'coral-framework/helpers/storage';
|
||||
import {handleAuthToken} from 'coral-framework/actions/auth';
|
||||
|
||||
// Log In.
|
||||
export const handleLogin = (email, password, recaptchaResponse) => dispatch => {
|
||||
//==============================================================================
|
||||
// SIGN IN
|
||||
//==============================================================================
|
||||
|
||||
export const handleLogin = (email, password, recaptchaResponse) => (dispatch) => {
|
||||
dispatch({type: actions.LOGIN_REQUEST});
|
||||
const params = {method: 'POST', body: {email, password}};
|
||||
|
||||
const params = {
|
||||
method: 'POST',
|
||||
body: {
|
||||
email,
|
||||
password
|
||||
}
|
||||
};
|
||||
|
||||
if (recaptchaResponse) {
|
||||
params.headers = {'X-Recaptcha-Response': recaptchaResponse};
|
||||
params.headers = {
|
||||
'X-Recaptcha-Response': recaptchaResponse
|
||||
};
|
||||
}
|
||||
|
||||
return coralApi('/auth/local', params)
|
||||
.then(({user}) => {
|
||||
.then(({user, token}) => {
|
||||
|
||||
if (!user) {
|
||||
if (!bowser.safari && !bowser.ios) {
|
||||
Storage.removeItem('token');
|
||||
}
|
||||
return dispatch(checkLoginFailure('not logged in'));
|
||||
}
|
||||
|
||||
const isAdmin = !!user.roles.filter(i => i === 'ADMIN').length;
|
||||
dispatch(checkLoginSuccess(user, isAdmin));
|
||||
dispatch(handleAuthToken(token));
|
||||
dispatch(checkLoginSuccess(user));
|
||||
})
|
||||
.catch(error => {
|
||||
|
||||
.catch((error) => {
|
||||
if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
|
||||
dispatch({type: actions.LOGIN_MAXIMUM_EXCEEDED, message: error.translation_key});
|
||||
dispatch({
|
||||
type: actions.LOGIN_MAXIMUM_EXCEEDED,
|
||||
message: error.translation_key
|
||||
});
|
||||
} else {
|
||||
dispatch({type: actions.LOGIN_FAILURE, message: error.translation_key});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const forgotPassowordRequest = () => ({type: actions.FETCH_FORGOT_PASSWORD_REQUEST});
|
||||
const forgotPassowordSuccess = () => ({type: actions.FETCH_FORGOT_PASSWORD_SUCCESS});
|
||||
const forgotPassowordFailure = () => ({type: actions.FETCH_FORGOT_PASSWORD_FAILURE});
|
||||
//==============================================================================
|
||||
// FORGOT PASSWORD
|
||||
//==============================================================================
|
||||
|
||||
export const requestPasswordReset = email => dispatch => {
|
||||
const forgotPassowordRequest = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_REQUEST
|
||||
});
|
||||
|
||||
const forgotPassowordSuccess = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS
|
||||
});
|
||||
|
||||
const forgotPassowordFailure = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_FAILURE
|
||||
});
|
||||
|
||||
export const requestPasswordReset = (email) => (dispatch) => {
|
||||
dispatch(forgotPassowordRequest(email));
|
||||
return coralApi('/account/password/reset', {method: 'POST', body: {email}})
|
||||
const redirectUri = location.href;
|
||||
|
||||
return coralApi('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}})
|
||||
.then(() => dispatch(forgotPassowordSuccess()))
|
||||
.catch(error => dispatch(forgotPassowordFailure(error)));
|
||||
.catch((error) => dispatch(forgotPassowordFailure(error)));
|
||||
};
|
||||
|
||||
// Check Login
|
||||
//==============================================================================
|
||||
// CHECK LOGIN
|
||||
//==============================================================================
|
||||
|
||||
const checkLoginRequest = () => ({type: actions.CHECK_LOGIN_REQUEST});
|
||||
const checkLoginSuccess = (user, isAdmin) => ({type: actions.CHECK_LOGIN_SUCCESS, user, isAdmin});
|
||||
const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error});
|
||||
const checkLoginRequest = () => ({
|
||||
type: actions.CHECK_LOGIN_REQUEST
|
||||
});
|
||||
|
||||
export const checkLogin = () => dispatch => {
|
||||
const checkLoginSuccess = (user, isAdmin) => ({
|
||||
type: actions.CHECK_LOGIN_SUCCESS,
|
||||
user,
|
||||
isAdmin
|
||||
});
|
||||
|
||||
const checkLoginFailure = (error) => ({
|
||||
type: actions.CHECK_LOGIN_FAILURE,
|
||||
error
|
||||
});
|
||||
|
||||
export const checkLogin = () => (dispatch) => {
|
||||
dispatch(checkLoginRequest());
|
||||
return coralApi('/auth')
|
||||
.then(({user}) => {
|
||||
if (!user) {
|
||||
if (!bowser.safari && !bowser.ios) {
|
||||
Storage.removeItem('token');
|
||||
}
|
||||
return dispatch(checkLoginFailure('not logged in'));
|
||||
}
|
||||
|
||||
const isAdmin = !!user.roles.filter(i => i === 'ADMIN').length;
|
||||
dispatch(checkLoginSuccess(user, isAdmin));
|
||||
dispatch(checkLoginSuccess(user));
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
dispatch(checkLoginFailure(`${error.translation_key}`));
|
||||
});
|
||||
};
|
||||
|
||||
// LogOut Actions
|
||||
|
||||
const logOutRequest = () => ({type: actions.LOGOUT_REQUEST});
|
||||
const logOutSuccess = () => ({type: actions.LOGOUT_SUCCESS});
|
||||
const logOutFailure = () => ({type: actions.LOGOUT_FAILURE});
|
||||
|
||||
export const logout = () => dispatch => {
|
||||
dispatch(logOutRequest());
|
||||
return coralApi('/auth', {method: 'DELETE'})
|
||||
.then(() => dispatch(logOutSuccess()))
|
||||
.catch(error => dispatch(logOutFailure(error)));
|
||||
};
|
||||
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
HIDE_SUSPENDUSER_DIALOG
|
||||
} from '../constants/community';
|
||||
|
||||
import coralApi from '../../../coral-framework/helpers/response';
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
|
||||
export const fetchAccounts = (query = {}) => dispatch => {
|
||||
export const fetchAccounts = (query = {}) => (dispatch) => {
|
||||
|
||||
dispatch(requestFetchAccounts());
|
||||
coralApi(`/users?${qs.stringify(query)}`)
|
||||
@@ -30,14 +30,14 @@ export const fetchAccounts = (query = {}) => dispatch => {
|
||||
totalPages
|
||||
});
|
||||
})
|
||||
.catch(error => dispatch({type: FETCH_COMMENTERS_FAILURE, error}));
|
||||
.catch((error) => dispatch({type: FETCH_COMMENTERS_FAILURE, error}));
|
||||
};
|
||||
|
||||
const requestFetchAccounts = () => ({
|
||||
type: FETCH_COMMENTERS_REQUEST
|
||||
});
|
||||
|
||||
export const updateSorting = sort => ({
|
||||
export const updateSorting = (sort) => ({
|
||||
type: SORT_UPDATE,
|
||||
sort
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const CONFIG_UPDATED = 'CONFIG_UPDATED';
|
||||
|
||||
export const fetchConfig = () => dispatch => {
|
||||
export const fetchConfig = () => (dispatch) => {
|
||||
let json = document.getElementById('data');
|
||||
let data = JSON.parse(json.textContent);
|
||||
dispatch({type: CONFIG_UPDATED, data});
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import coralApi from 'coral-framework/helpers/response';
|
||||
import coralApi from 'coral-framework/helpers/request';
|
||||
import * as actions from '../constants/install';
|
||||
import validate from 'coral-framework/helpers/validate';
|
||||
import errorMsj from 'coral-framework/helpers/error';
|
||||
|
||||
export const nextStep = () => ({type: actions.NEXT_STEP});
|
||||
export const previousStep = () => ({type: actions.PREVIOUS_STEP});
|
||||
export const goToStep = step => ({type: actions.GO_TO_STEP, step});
|
||||
export const goToStep = (step) => ({type: actions.GO_TO_STEP, step});
|
||||
|
||||
const installRequest = () => ({type: actions.INSTALL_REQUEST});
|
||||
const installSuccess = () => ({type: actions.INSTALL_SUCCESS});
|
||||
const installFailure = error => ({type: actions.INSTALL_FAILURE, error});
|
||||
const installFailure = (error) => ({type: actions.INSTALL_FAILURE, error});
|
||||
|
||||
const addError = (name, error) => ({type: actions.ADD_ERROR, name, error});
|
||||
const hasError = error => ({type: actions.HAS_ERROR, error});
|
||||
const hasError = (error) => ({type: actions.HAS_ERROR, error});
|
||||
const clearErrors = () => ({type: actions.CLEAR_ERRORS});
|
||||
|
||||
const validation = (formData, dispatch, next) => {
|
||||
@@ -21,11 +21,11 @@ const validation = (formData, dispatch, next) => {
|
||||
}
|
||||
|
||||
const validKeys = Object.keys(formData)
|
||||
.filter(name => name !== 'domains');
|
||||
.filter((name) => name !== 'domains');
|
||||
|
||||
// Required Validation
|
||||
const empty = validKeys
|
||||
.filter(name => {
|
||||
.filter((name) => {
|
||||
const cond = !formData[name].length;
|
||||
|
||||
if (cond) {
|
||||
@@ -45,7 +45,7 @@ const validation = (formData, dispatch, next) => {
|
||||
|
||||
// RegExp Validation
|
||||
const validation = validKeys
|
||||
.filter(name => {
|
||||
.filter((name) => {
|
||||
const cond = !validate[name](formData[name]);
|
||||
if (cond) {
|
||||
|
||||
@@ -88,7 +88,7 @@ export const finishInstall = () => (dispatch, getState) => {
|
||||
dispatch(installSuccess());
|
||||
dispatch(nextStep());
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
dispatch(installFailure(`${error.translation_key}`));
|
||||
});
|
||||
@@ -99,10 +99,10 @@ export const updateUserFormData = (name, value) => ({type: actions.UPDATE_FORMDA
|
||||
export const updatePermittedDomains = (value) => ({type: actions.UPDATE_PERMITTED_DOMAINS_SETTINGS, value});
|
||||
|
||||
const checkInstallRequest = () => ({type: actions.CHECK_INSTALL_REQUEST});
|
||||
const checkInstallSuccess = installed => ({type: actions.CHECK_INSTALL_SUCCESS, installed});
|
||||
const checkInstallFailure = error => ({type: actions.CHECK_INSTALL_FAILURE, error});
|
||||
const checkInstallSuccess = (installed) => ({type: actions.CHECK_INSTALL_SUCCESS, installed});
|
||||
const checkInstallFailure = (error) => ({type: actions.CHECK_INSTALL_FAILURE, error});
|
||||
|
||||
export const checkInstall = next => dispatch => {
|
||||
export const checkInstall = (next) => (dispatch) => {
|
||||
dispatch(checkInstallRequest());
|
||||
coralApi('/setup')
|
||||
.then(({installed}) => {
|
||||
@@ -111,7 +111,7 @@ export const checkInstall = next => dispatch => {
|
||||
next();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
dispatch(checkInstallFailure(`${error.translation_key}`));
|
||||
});
|
||||
|
||||
@@ -1,8 +1,29 @@
|
||||
import * as actions from 'constants/moderation';
|
||||
|
||||
export const toggleModal = open => ({type: actions.TOGGLE_MODAL, open});
|
||||
export const toggleModal = (open) => ({type: actions.TOGGLE_MODAL, open});
|
||||
export const singleView = () => ({type: actions.SINGLE_VIEW});
|
||||
|
||||
// Ban User Dialog
|
||||
export const showBanUserDialog = (user, commentId, showRejectedNote) => ({type: actions.SHOW_BANUSER_DIALOG, user, commentId, showRejectedNote});
|
||||
export const showBanUserDialog = (user, commentId, commentStatus, showRejectedNote) => ({type: actions.SHOW_BANUSER_DIALOG, user, commentId, commentStatus, showRejectedNote});
|
||||
export const hideBanUserDialog = (showDialog) => ({type: actions.HIDE_BANUSER_DIALOG, showDialog});
|
||||
|
||||
// Suspend User Dialog
|
||||
export const showSuspendUserDialog = (userId, username, commentId, commentStatus) =>
|
||||
({type: actions.SHOW_SUSPEND_USER_DIALOG, userId, username, commentId, commentStatus});
|
||||
|
||||
export const hideSuspendUserDialog = () => ({type: actions.HIDE_SUSPEND_USER_DIALOG});
|
||||
|
||||
// hide shortcuts note
|
||||
export const hideShortcutsNote = () => {
|
||||
try {
|
||||
window.localStorage.setItem('coral:shortcutsNote', 'hide');
|
||||
} catch (e) {
|
||||
|
||||
// above will fail in Safari private mode
|
||||
}
|
||||
|
||||
return {type: actions.HIDE_SHORTCUTS_NOTE};
|
||||
};
|
||||
|
||||
export const viewUserDetail = (userId) => ({type: actions.VIEW_USER_DETAIL, userId});
|
||||
export const hideUserDetail = () => ({type: actions.HIDE_USER_DETAIL});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import coralApi from '../../../coral-framework/helpers/response';
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
|
||||
export const SETTINGS_LOADING = 'SETTINGS_LOADING';
|
||||
export const SETTINGS_RECEIVED = 'SETTINGS_RECEIVED';
|
||||
@@ -13,19 +13,19 @@ export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED';
|
||||
export const WORDLIST_UPDATED = 'WORDLIST_UPDATED';
|
||||
export const DOMAINLIST_UPDATED = 'DOMAINLIST_UPDATED';
|
||||
|
||||
export const fetchSettings = () => dispatch => {
|
||||
export const fetchSettings = () => (dispatch) => {
|
||||
dispatch({type: SETTINGS_LOADING});
|
||||
coralApi('/settings')
|
||||
.then(settings => {
|
||||
.then((settings) => {
|
||||
dispatch({type: SETTINGS_RECEIVED, settings});
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
dispatch({type: SETTINGS_FETCH_ERROR, error});
|
||||
});
|
||||
};
|
||||
|
||||
// for updating top-level settings
|
||||
export const updateSettings = settings => {
|
||||
export const updateSettings = (settings) => {
|
||||
return {type: SETTINGS_UPDATED, settings};
|
||||
};
|
||||
|
||||
@@ -48,7 +48,7 @@ export const saveSettingsToServer = () => (dispatch, getState) => {
|
||||
.then(() => {
|
||||
dispatch({type: SAVE_SETTINGS_SUCCESS, settings});
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error) => {
|
||||
dispatch({type: SAVE_SETTINGS_FAILED, error});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import coralApi from '../../../coral-framework/helpers/response';
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
import * as userTypes from '../constants/users';
|
||||
|
||||
/**
|
||||
@@ -9,8 +9,8 @@ export const userStatusUpdate = (status, userId, commentId) => {
|
||||
return (dispatch) => {
|
||||
dispatch({type: userTypes.UPDATE_STATUS_REQUEST});
|
||||
return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
|
||||
.then(res => dispatch({type: userTypes.UPDATE_STATUS_SUCCESS, res}))
|
||||
.catch(error => dispatch({type: userTypes.UPDATE_STATUS_FAILURE, error}));
|
||||
.then((res) => dispatch({type: userTypes.UPDATE_STATUS_SUCCESS, res}))
|
||||
.catch((error) => dispatch({type: userTypes.UPDATE_STATUS_FAILURE, error}));
|
||||
};
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ export const userStatusUpdate = (status, userId, commentId) => {
|
||||
export const sendNotificationEmail = (userId, subject, body) => {
|
||||
return (dispatch) => {
|
||||
return coralApi(`/users/${userId}/email`, {method: 'POST', body: {subject, body}})
|
||||
.catch(error => dispatch({type: userTypes.USER_EMAIL_FAILURE, error}));
|
||||
.catch((error) => dispatch({type: userTypes.USER_EMAIL_FAILURE, error}));
|
||||
};
|
||||
};
|
||||
|
||||
@@ -26,6 +26,6 @@ export const sendNotificationEmail = (userId, subject, body) => {
|
||||
export const enableUsernameEdit = (userId) => {
|
||||
return (dispatch) => {
|
||||
return coralApi(`/users/${userId}/username-enable`, {method: 'POST'})
|
||||
.catch(error => dispatch({type: userTypes.USERNAME_ENABLE_FAILURE, error}));
|
||||
.catch((error) => dispatch({type: userTypes.USERNAME_ENABLE_FAILURE, error}));
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
import React from 'react';
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from './ModerationList.css';
|
||||
import {Button} from 'coral-ui';
|
||||
import {menuActionsMap} from '../containers/ModerationQueue/helpers/moderationQueueActionsMap';
|
||||
|
||||
const ActionButton = ({type = '', ...props}) => {
|
||||
const ActionButton = ({type = '', active, ...props}) => {
|
||||
const typeName = type.toLowerCase();
|
||||
let text = menuActionsMap[type].text;
|
||||
|
||||
if (text === 'Approve' && active) {
|
||||
text = 'Approved';
|
||||
} else if (text === 'Reject' && active) {
|
||||
text = 'Rejected';
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={`${type.toLowerCase()} ${styles.actionButton}`}
|
||||
cStyle={type.toLowerCase()}
|
||||
className={`${typeName} ${styles.actionButton} ${active ? styles[`${typeName}__active`] : ''}`}
|
||||
cStyle={typeName}
|
||||
icon={menuActionsMap[type].icon}
|
||||
onClick={type === 'APPROVE' ? props.acceptComment : props.rejectComment}
|
||||
>{menuActionsMap[type].text}</Button>
|
||||
>{text}</Button>
|
||||
);
|
||||
};
|
||||
|
||||
ActionButton.propTypes = {
|
||||
active: PropTypes.bool
|
||||
};
|
||||
|
||||
export default ActionButton;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
.button {
|
||||
-webkit-transform: scale(.8);
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.root {
|
||||
color: black;
|
||||
> :global(.mdl-menu__container) {
|
||||
margin-left: 10px;
|
||||
> :global(.mdl-menu__outline) {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttonOpen {
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
background-color: #616161;
|
||||
}
|
||||
|
||||
.arrowIcon {
|
||||
margin-left: 6px;
|
||||
margin-right: 0;
|
||||
vertical-align: middle;
|
||||
margin-right: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
background-color: #2a2a2a;
|
||||
color: white;
|
||||
&:first-child {
|
||||
margin-bottom: 1px;
|
||||
border-radius: 2px 2px 0px 0px;
|
||||
}
|
||||
&:last-child {
|
||||
border-radius: 0px 0px 2px 2px;
|
||||
}
|
||||
&:hover, &:active, &:focus {
|
||||
background-color: #767676;
|
||||
}
|
||||
&[disabled], &[disabled]:hover, &[disabled]:focus, &[disabled]:active {
|
||||
background-color: #262626;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Button, Icon} from 'coral-ui';
|
||||
import {Menu} from 'react-mdl';
|
||||
import cn from 'classnames';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
import styles from './ActionsMenu.css';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from 'coral-admin/src/translations.json';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
let count = 0;
|
||||
|
||||
class ActionsMenu extends React.Component {
|
||||
id = `actions-dropdown-${count++}`;
|
||||
menu = null;
|
||||
state = {open: false};
|
||||
timeout = null;
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
|
||||
handleRef = (ref) => {
|
||||
this.menu = ref ? findDOMNode(ref).parentNode : null;
|
||||
}
|
||||
|
||||
syncOpenState = () => {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = setTimeout(() => {
|
||||
this.setState({open: this.menu.className.indexOf('is-visible') >= 0});
|
||||
}, 150);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.root} onBlur={this.syncOpenState} >
|
||||
<Button
|
||||
cStyle='actions'
|
||||
className={cn(styles.button, {[styles.buttonOpen]: this.state.open})}
|
||||
disabled={false}
|
||||
id={this.id}
|
||||
onClick={this.syncOpenState}
|
||||
icon={this.props.icon}
|
||||
raised>
|
||||
{lang.t('modqueue.actions')}
|
||||
<Icon
|
||||
name={this.state.open ? 'keyboard_arrow_up' : 'keyboard_arrow_down'}
|
||||
className={styles.arrowIcon}
|
||||
/>
|
||||
</Button>
|
||||
<Menu target={this.id} className={styles.menu} ref={this.handleRef}>
|
||||
{this.props.children}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ActionsMenu.propTypes = {
|
||||
icon: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ActionsMenu;
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import {MenuItem} from 'react-mdl';
|
||||
import styles from './ActionsMenu.css';
|
||||
|
||||
const ActionsMenuItem = (props) =>
|
||||
<MenuItem className={cn(styles.menuItem, props.className)} {...props} />;
|
||||
|
||||
export default ActionsMenuItem;
|
||||
@@ -14,7 +14,7 @@ class AdminLogin extends React.Component {
|
||||
this.state = {email: '', password: '', requestPassword: false};
|
||||
}
|
||||
|
||||
handleSignIn = e => {
|
||||
handleSignIn = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.handleLogin(this.state.email, this.state.password);
|
||||
}
|
||||
@@ -28,7 +28,7 @@ class AdminLogin extends React.Component {
|
||||
this.props.handleLogin(this.state.email, this.state.password, recaptchaResponse);
|
||||
}
|
||||
|
||||
handleRequestPassword = e => {
|
||||
handleRequestPassword = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.requestPasswordReset(this.state.email);
|
||||
}
|
||||
@@ -41,11 +41,11 @@ class AdminLogin extends React.Component {
|
||||
<TextField
|
||||
label='Email Address'
|
||||
value={this.state.email}
|
||||
onChange={e => this.setState({email: e.target.value})} />
|
||||
onChange={(e) => this.setState({email: e.target.value})} />
|
||||
<TextField
|
||||
label='Password'
|
||||
value={this.state.password}
|
||||
onChange={e => this.setState({password: e.target.value})}
|
||||
onChange={(e) => this.setState({password: e.target.value})}
|
||||
type='password' />
|
||||
<div style={{height: 10}}></div>
|
||||
<Button
|
||||
@@ -54,7 +54,7 @@ class AdminLogin extends React.Component {
|
||||
full
|
||||
onClick={this.handleSignIn}>Sign In</Button>
|
||||
<p className={styles.forgotPasswordCTA}>
|
||||
Forgot your password? <a href="#" className={styles.forgotPasswordLink} onClick={e => {
|
||||
Forgot your password? <a href="#" className={styles.forgotPasswordLink} onClick={(e) => {
|
||||
e.preventDefault();
|
||||
this.setState({requestPassword: true});
|
||||
}}>Request a new one.</a>
|
||||
@@ -82,7 +82,7 @@ class AdminLogin extends React.Component {
|
||||
<TextField
|
||||
label='Email Address'
|
||||
value={this.state.email}
|
||||
onChange={e => this.setState({email: e.target.value})} />
|
||||
onChange={(e) => this.setState({email: e.target.value})} />
|
||||
<Button
|
||||
type='submit'
|
||||
cStyle='black'
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React from 'react';
|
||||
import {Provider} from 'react-redux';
|
||||
import ToastContainer from './ToastContainer';
|
||||
import 'material-design-lite';
|
||||
import store from 'services/store';
|
||||
|
||||
import AppRouter from '../AppRouter';
|
||||
|
||||
export default class App extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<AppRouter store={store} />
|
||||
</Provider>
|
||||
<div>
|
||||
<ToastContainer />
|
||||
<AppRouter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Card} from 'coral-ui';
|
||||
|
||||
const EmptyCard = props => (
|
||||
const EmptyCard = (props) => (
|
||||
<Card style={{textAlign: 'center', maxWidth: 400, margin: '0 auto'}}>
|
||||
{props.children}
|
||||
</Card>
|
||||
|
||||
@@ -18,3 +18,47 @@
|
||||
border-radius: 4px;
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
.callToAction {
|
||||
position: fixed;
|
||||
left: 10px;
|
||||
bottom: 10px;
|
||||
width: 280px;
|
||||
height: 200px;
|
||||
background: white;
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.15);
|
||||
|
||||
.ctaHeader {
|
||||
font-size: 16px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
p, li {
|
||||
font-size: 12px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
li span:first-child, p:last-child span:first-child {
|
||||
display: inline-block;
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
float: right;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.smallKey {
|
||||
padding: 5px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
import React from 'react';
|
||||
import React, {PropTypes} from 'react';
|
||||
import Modal from 'components/Modal';
|
||||
import styles from './ModerationKeysModal.css';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const shortcuts = [
|
||||
{
|
||||
title: 'modqueue.navigation',
|
||||
@@ -23,29 +25,52 @@ const shortcuts = [
|
||||
}
|
||||
];
|
||||
|
||||
export default ({open, onClose}) => (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<h3>{lang.t('modqueue.shortcuts')}</h3>
|
||||
<div className={styles.container}>
|
||||
{shortcuts.map((shortcut, i) => (
|
||||
<table className={styles.table} key={i}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{lang.t(shortcut.title)}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.keys(shortcut.shortcuts).map(key => (
|
||||
<tr key={`${key }tr`}>
|
||||
<td className={styles.shortcut}><span className={styles.key}>{key}</span></td>
|
||||
<td>{lang.t(shortcut.shortcuts[key])}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
export default class ModerationKeysModal extends React.Component {
|
||||
|
||||
const lang = new I18n(translations);
|
||||
static propTypes = {
|
||||
open: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
hideShortcutsNote: PropTypes.func.isRequired,
|
||||
shortcutsNoteVisible: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
const {open, onClose, hideShortcutsNote, shortcutsNoteVisible} = this.props;
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.callToAction} style={{display: shortcutsNoteVisible === 'show' ? 'block' : 'none'}}>
|
||||
<div onClick={hideShortcutsNote} className={styles.closeButton}>×</div>
|
||||
<p className={styles.ctaHeader}>{lang.t('modqueue.mod-faster')}</p>
|
||||
<p><strong>{lang.t('modqueue.try-these')}:</strong></p>
|
||||
<ul>
|
||||
<li><span>{lang.t('modqueue.approve')}</span> <span className={styles.smallKey}>t</span></li>
|
||||
<li><span>{lang.t('modqueue.reject')}</span> <span className={styles.smallKey}>r</span></li>
|
||||
</ul>
|
||||
<p><span>{lang.t('modqueue.view-more-shortcuts')}</span> <span className={styles.smallKey}>{lang.t('modqueue.shift-key')}</span> + <span className={styles.smallKey}>/</span></p>
|
||||
</div>
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<h3>{lang.t('modqueue.shortcuts')}</h3>
|
||||
<div className={styles.container}>
|
||||
{shortcuts.map((shortcut, i) => (
|
||||
<table className={styles.table} key={i}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{lang.t(shortcut.title)}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.keys(shortcut.shortcuts).map((key) => (
|
||||
<tr key={`${key}tr`}>
|
||||
<td className={styles.shortcut}><span className={styles.key}>{key}</span></td>
|
||||
<td>{lang.t(shortcut.shortcuts[key])}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,3 +188,17 @@
|
||||
margin: 0;
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.approve__active {
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
background-color: #519954;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.reject__active, .rejected__active {
|
||||
color: white;
|
||||
background-color: #D03235;
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ export default class ModerationList extends React.Component {
|
||||
// Add key handlers. Each action has one and added j/k for moving around
|
||||
bindKeyHandlers () {
|
||||
const {modActions, isActive} = this.props;
|
||||
modActions.filter(action => menuOptionsMap[action].key).forEach(action => {
|
||||
modActions.filter((action) => menuOptionsMap[action].key).forEach((action) => {
|
||||
key(menuOptionsMap[action].key, 'moderationList', () => isActive && this.actionKeyHandler(menuOptionsMap[action].status));
|
||||
});
|
||||
key('j', 'moderationList', () => isActive && this.moveKeyHandler('down'));
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
@keyframes :global(bounceInRight) {
|
||||
from, 60%, 75%, 90%, to {
|
||||
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate3d(3000px, 0, 0); }
|
||||
60% {
|
||||
opacity: 1;
|
||||
transform: translate3d(-25px, 0, 0); }
|
||||
75% {
|
||||
transform: translate3d(10px, 0, 0); }
|
||||
90% {
|
||||
transform: translate3d(-5px, 0, 0); }
|
||||
to {
|
||||
transform: none; } }
|
||||
|
||||
@keyframes :global(bounceOutRight) {
|
||||
20% {
|
||||
opacity: 1;
|
||||
transform: translate3d(-20px, 0, 0); }
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translate3d(2000px, 0, 0); } }
|
||||
|
||||
@keyframes :global(bounceInLeft) {
|
||||
from, 60%, 75%, 90%, to {
|
||||
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); }
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(-3000px, 0, 0); }
|
||||
60% {
|
||||
opacity: 1;
|
||||
transform: translate3d(25px, 0, 0); }
|
||||
75% {
|
||||
transform: translate3d(-10px, 0, 0); }
|
||||
90% {
|
||||
transform: translate3d(5px, 0, 0); }
|
||||
to {
|
||||
transform: none; } }
|
||||
|
||||
@keyframes :global(bounceOutLeft) {
|
||||
20% {
|
||||
opacity: 1;
|
||||
transform: translate3d(20px, 0, 0); }
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translate3d(-2000px, 0, 0); } }
|
||||
|
||||
@keyframes :global(bounceInUp) {
|
||||
from, 60%, 75%, 90%, to {
|
||||
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 3000px, 0); }
|
||||
60% {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, -20px, 0); }
|
||||
75% {
|
||||
transform: translate3d(0, 10px, 0); }
|
||||
90% {
|
||||
transform: translate3d(0, -5px, 0); }
|
||||
to {
|
||||
transform: translate3d(0, 0, 0); } }
|
||||
|
||||
@keyframes :global(bounceOutUp) {
|
||||
20% {
|
||||
transform: translate3d(0, -10px, 0); }
|
||||
40%, 45% {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 20px, 0); }
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, -2000px, 0); } }
|
||||
|
||||
@keyframes :global(bounceInDown) {
|
||||
from, 60%, 75%, 90%, to {
|
||||
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); }
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, -3000px, 0); }
|
||||
60% {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 25px, 0); }
|
||||
75% {
|
||||
transform: translate3d(0, -10px, 0); }
|
||||
90% {
|
||||
transform: translate3d(0, 5px, 0); }
|
||||
to {
|
||||
transform: none; } }
|
||||
|
||||
@keyframes :global(bounceOutDown) {
|
||||
20% {
|
||||
transform: translate3d(0, 10px, 0); }
|
||||
40%, 45% {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, -20px, 0); }
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 2000px, 0); } }
|
||||
|
||||
@keyframes :global(track-progress) {
|
||||
0% {
|
||||
width: 100%; }
|
||||
100% {
|
||||
width: 0; } }
|
||||
|
||||
:global {
|
||||
.bounceOutRight, .toast-exit--top-right, .toast-exit--bottom-right {
|
||||
animation-name: bounceOutRight; }
|
||||
|
||||
.bounceInRight, .toast-enter--top-right, .toast-enter--bottom-right {
|
||||
animation-name: bounceInRight; }
|
||||
|
||||
.bounceInLeft, .toast-enter--top-left, .toast-enter--bottom-left {
|
||||
animation-name: bounceInLeft; }
|
||||
|
||||
.bounceOutLeft, .toast-exit--top-left, .toast-exit--bottom-left {
|
||||
animation-name: bounceOutLeft; }
|
||||
|
||||
.bounceInUp, .toast-enter--bottom-center {
|
||||
animation-name: bounceInUp; }
|
||||
.bounceOutUp, .toast-exit--top-center {
|
||||
animation-name: bounceOutUp; }
|
||||
|
||||
.bounceInDown, .toast-enter--top-center {
|
||||
animation-name: bounceInDown; }
|
||||
|
||||
.bounceOutDown, .toast-exit--bottom-center {
|
||||
animation-name: bounceOutDown; }
|
||||
|
||||
.animated {
|
||||
animation-duration: 0.75s;
|
||||
animation-fill-mode: both; }
|
||||
|
||||
.toastify {
|
||||
z-index: 999;
|
||||
position: fixed;
|
||||
padding: 4px;
|
||||
width: 350px;
|
||||
max-width: 98%;
|
||||
color: #999;
|
||||
box-sizing: border-box; }
|
||||
.toastify--top-left {
|
||||
top: 1em;
|
||||
left: 1em; }
|
||||
.toastify--top-center {
|
||||
top: 1em;
|
||||
left: 50%;
|
||||
margin-left: -175px; }
|
||||
.toastify--top-right {
|
||||
top: 1em;
|
||||
right: 2em; }
|
||||
.toastify--bottom-left {
|
||||
bottom: 1em;
|
||||
left: 1em; }
|
||||
.toastify--bottom-center {
|
||||
bottom: 1em;
|
||||
left: 50%;
|
||||
margin-left: -175px; }
|
||||
.toastify--bottom-right {
|
||||
bottom: 1em;
|
||||
right: 2em; }
|
||||
.toastify__img {
|
||||
float: left;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle; }
|
||||
|
||||
.toastify__close {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
left: 12px;
|
||||
width: 20px;
|
||||
height: 16px;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
transition: .3s ease; }
|
||||
.toastify__close:hover, .toastify__close:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.toastify-content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
padding: 18px 24px 20px 48px;
|
||||
box-sizing: border-box;
|
||||
background: #404040;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1), 0 3px 20px 0 rgba(0, 0, 0, 0.05); }
|
||||
.toastify-content--info {
|
||||
background: #2488cb; }
|
||||
.toastify-content--success {
|
||||
background: #008577; }
|
||||
.toastify-content--warning {
|
||||
background: #ef6c2b; }
|
||||
.toastify-content--error {
|
||||
background: #ef342b; }
|
||||
|
||||
.toastify__body {
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.toastify__progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 4px;
|
||||
z-index: 999;
|
||||
opacity: 0.8;
|
||||
border-radius: 2px;
|
||||
animation: track-progress linear 1;
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import './ToastContainer.css';
|
||||
import {defaultProps} from 'recompose';
|
||||
import {ToastContainer} from 'react-toastify';
|
||||
|
||||
export default defaultProps({
|
||||
autoClose: 5000,
|
||||
})(ToastContainer);
|
||||
@@ -4,10 +4,11 @@ import {IndexLink, Link} from 'react-router';
|
||||
import styles from './Drawer.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
|
||||
const CoralDrawer = ({handleLogout, restricted = false}) => (
|
||||
const CoralDrawer = ({handleLogout, auth}) => (
|
||||
<Drawer className={styles.header}>
|
||||
{ !restricted ?
|
||||
{ auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
|
||||
<div>
|
||||
<Navigation className={styles.nav}>
|
||||
<IndexLink
|
||||
@@ -16,28 +17,37 @@ const CoralDrawer = ({handleLogout, restricted = false}) => (
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.dashboard')}
|
||||
</IndexLink>
|
||||
<Link
|
||||
className={styles.navLink}
|
||||
to="/admin/moderate"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.moderate')}
|
||||
</Link>
|
||||
<Link className={styles.navLink}
|
||||
to="/admin/stories"
|
||||
{
|
||||
can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
<Link
|
||||
className={styles.navLink}
|
||||
to="/admin/moderate"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.moderate')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
<Link className={styles.navLink}
|
||||
to="/admin/stories"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.stories')}
|
||||
</Link>
|
||||
<Link className={styles.navLink}
|
||||
to="/admin/community"
|
||||
activeClassName={styles.active}>
|
||||
to="/admin/community"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.community')}
|
||||
</Link>
|
||||
<Link
|
||||
className={styles.navLink}
|
||||
to="/admin/configure"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.configure')}
|
||||
</Link>
|
||||
{
|
||||
can(auth.user, 'UPDATE_CONFIG') &&
|
||||
(
|
||||
<Link
|
||||
className={styles.navLink}
|
||||
to="/admin/configure"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.configure')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
<a onClick={handleLogout}>Sign Out</a>
|
||||
<span>{`v${process.env.VERSION}`}</span>
|
||||
</Navigation>
|
||||
|
||||
@@ -5,57 +5,74 @@ import styles from './Header.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import {Logo} from './Logo';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
|
||||
const CoralHeader = ({handleLogout, restricted = false}) => (
|
||||
const CoralHeader = ({
|
||||
handleLogout,
|
||||
showShortcuts = () => {},
|
||||
auth
|
||||
}) => (
|
||||
<Header className={styles.header}>
|
||||
<Logo className={styles.logo} />
|
||||
{
|
||||
!restricted ?
|
||||
<div>
|
||||
<Navigation className={styles.nav}>
|
||||
<IndexLink
|
||||
id='dashboardNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/dashboard"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.dashboard')}
|
||||
</IndexLink>
|
||||
<Link
|
||||
id='moderateNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/moderate"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.moderate')}
|
||||
</Link>
|
||||
<Link
|
||||
id='streamsNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/stories"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.stories')}
|
||||
</Link>
|
||||
<Link
|
||||
id='communityNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/community"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.community')}
|
||||
</Link>
|
||||
<Link
|
||||
id='configureNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/configure"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.configure')}
|
||||
</Link>
|
||||
</Navigation>
|
||||
{
|
||||
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
|
||||
<Navigation className={styles.nav}>
|
||||
<IndexLink
|
||||
id='dashboardNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/dashboard"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.dashboard')}
|
||||
</IndexLink>
|
||||
{
|
||||
can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
<Link
|
||||
id='moderateNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/moderate"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.moderate')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
<Link
|
||||
id='streamsNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/stories"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.stories')}
|
||||
</Link>
|
||||
<Link
|
||||
id='communityNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/community"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.community')}
|
||||
</Link>
|
||||
{
|
||||
can(auth.user, 'UPDATE_CONFIG') && (
|
||||
<Link
|
||||
id='configureNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/configure"
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('configure.configure')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
</Navigation>
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className={styles.rightPanel}>
|
||||
<ul>
|
||||
<li className={styles.settings}>
|
||||
<div>
|
||||
<IconButton name="settings" id="menu-settings"/>
|
||||
<Menu target="menu-settings" align="right">
|
||||
<MenuItem onClick={handleLogout}>Sign Out</MenuItem>
|
||||
<MenuItem onClick={() => showShortcuts(true)}>{lang.t('configure.shortcuts')}</MenuItem>
|
||||
<MenuItem onClick={handleLogout}>{lang.t('configure.sign-out')}</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
@@ -65,15 +82,13 @@ const CoralHeader = ({handleLogout, restricted = false}) => (
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
</Header>
|
||||
);
|
||||
|
||||
CoralHeader.propTypes = {
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
restricted: PropTypes.bool // hide elemnts from a user that's logged out
|
||||
auth: PropTypes.object,
|
||||
showShortcuts: PropTypes.func,
|
||||
handleLogout: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
@@ -4,9 +4,17 @@ import Header from './Header';
|
||||
import Drawer from './Drawer';
|
||||
import styles from './Layout.css';
|
||||
|
||||
const Layout = ({children, handleLogout = () => {}, restricted = false, ...props}) => (
|
||||
const Layout = ({
|
||||
children,
|
||||
handleLogout = () => {},
|
||||
toggleShortcutModal,
|
||||
restricted = false,
|
||||
...props}) => (
|
||||
<LayoutMDL fixedDrawer>
|
||||
<Header handleLogout={handleLogout} restricted={restricted} {...props} />
|
||||
<Header
|
||||
handleLogout={handleLogout}
|
||||
showShortcuts={toggleShortcutModal}
|
||||
{...props} />
|
||||
<Drawer handleLogout={handleLogout} restricted={restricted} {...props} />
|
||||
<div className={styles.layout}>
|
||||
{children}
|
||||
@@ -16,6 +24,7 @@ const Layout = ({children, handleLogout = () => {}, restricted = false, ...props
|
||||
|
||||
Layout.propTypes = {
|
||||
handleLogout: PropTypes.func,
|
||||
toggleShortcutModal: PropTypes.func,
|
||||
restricted: PropTypes.bool // hide elements from a user that's logged out
|
||||
};
|
||||
|
||||
|
||||
@@ -2,11 +2,7 @@ export const CHECK_LOGIN_REQUEST = 'CHECK_LOGIN_REQUEST';
|
||||
export const CHECK_LOGIN_SUCCESS = 'CHECK_LOGIN_SUCCESS';
|
||||
export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE';
|
||||
|
||||
export const CHECK_CSRF_TOKEN = 'CHECK_CSRF_TOKEN';
|
||||
|
||||
export const LOGOUT_REQUEST = 'LOGOUT_REQUEST';
|
||||
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
|
||||
export const LOGOUT_FAILURE = 'LOGOUT_FAILURE';
|
||||
export const LOGOUT = 'LOGOUT';
|
||||
|
||||
export const LOGIN_REQUEST = 'LOGIN_REQUEST';
|
||||
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
|
||||
|
||||
@@ -2,3 +2,8 @@ export const TOGGLE_MODAL = 'TOGGLE_MODAL';
|
||||
export const SINGLE_VIEW = 'SINGLE_VIEW';
|
||||
export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG';
|
||||
export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG';
|
||||
export const HIDE_SHORTCUTS_NOTE = 'HIDE_SHORTCUTS_NOTE';
|
||||
export const SHOW_SUSPEND_USER_DIALOG = 'SHOW_SUSPEND_USER_DIALOG';
|
||||
export const HIDE_SUSPEND_USER_DIALOG = 'HIDE_SUSPEND_USER_DIALOG';
|
||||
export const VIEW_USER_DETAIL = 'VIEW_USER_DETAIL';
|
||||
export const HIDE_USER_DETAIL = 'HIDE_USER_DETAIL';
|
||||
|
||||
@@ -3,7 +3,7 @@ import {connect} from 'react-redux';
|
||||
import {compose} from 'react-apollo';
|
||||
|
||||
import {modUserFlaggedQuery} from 'coral-admin/src/graphql/queries';
|
||||
import {banUser, setUserStatus, suspendUser} from 'coral-admin/src/graphql/mutations';
|
||||
import {banUser, setUserStatus, rejectUsername} from 'coral-admin/src/graphql/mutations';
|
||||
|
||||
import {
|
||||
fetchAccounts,
|
||||
@@ -113,7 +113,7 @@ class CommunityContainer extends Component {
|
||||
error={data.error}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
approveUser={props.approveUser}
|
||||
suspendUser={props.suspendUser}
|
||||
rejectUsername={props.rejectUsername}
|
||||
showSuspendUserDialog={props.showSuspendUserDialog}
|
||||
/>
|
||||
<BanUserDialog
|
||||
@@ -126,7 +126,7 @@ class CommunityContainer extends Component {
|
||||
open={community.suspendDialog}
|
||||
handleClose={props.hideSuspendUserDialog}
|
||||
user={community.user}
|
||||
suspendUser={props.suspendUser}
|
||||
rejectUsername={props.rejectUsername}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -148,12 +148,12 @@ class CommunityContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
const mapStateToProps = (state) => ({
|
||||
community: state.community.toJS()
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
fetchAccounts: query => dispatch(fetchAccounts(query)),
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
fetchAccounts: (query) => dispatch(fetchAccounts(query)),
|
||||
showBanUserDialog: (user) => dispatch(showBanUserDialog(user)),
|
||||
hideBanUserDialog: () => dispatch(hideBanUserDialog(false)),
|
||||
showSuspendUserDialog: (user) => dispatch(showSuspendUserDialog(user)),
|
||||
@@ -165,5 +165,5 @@ export default compose(
|
||||
modUserFlaggedQuery,
|
||||
banUser,
|
||||
setUserStatus,
|
||||
suspendUser
|
||||
rejectUsername
|
||||
)(CommunityContainer);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
const CommunityLayout = props => (
|
||||
const CommunityLayout = (props) => (
|
||||
<div>
|
||||
{props.children}
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,6 @@ import translations from 'coral-admin/src/translations.json';
|
||||
|
||||
import styles from './Community.css';
|
||||
import Table from './Table';
|
||||
import Loading from './Loading';
|
||||
import {Pager, Icon} from 'coral-ui';
|
||||
import EmptyCard from '../../components/EmptyCard';
|
||||
|
||||
@@ -29,8 +28,8 @@ const tableHeaders = [
|
||||
}
|
||||
];
|
||||
|
||||
const People = ({isFetching, commenters, searchValue, onSearchChange, ...props}) => {
|
||||
const hasResults = !isFetching && !!commenters.length;
|
||||
const People = ({commenters, searchValue, onSearchChange, ...props}) => {
|
||||
const hasResults = !!commenters.length;
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.leftColumn}>
|
||||
@@ -47,7 +46,6 @@ const People = ({isFetching, commenters, searchValue, onSearchChange, ...props})
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.mainContent}>
|
||||
{ isFetching && <Loading /> }
|
||||
{
|
||||
hasResults
|
||||
? <Table
|
||||
|
||||
@@ -54,7 +54,7 @@ class Table extends Component {
|
||||
<SelectField label={'Select me'} value={row.status || ''}
|
||||
className={styles.selectField}
|
||||
label={lang.t('community.status')}
|
||||
onChange={status => this.onCommenterStatusChange(row.id, status)}>
|
||||
onChange={(status) => this.onCommenterStatusChange(row.id, status)}>
|
||||
<Option value={'ACTIVE'}>{lang.t('community.active')}</Option>
|
||||
<Option value={'BANNED'}>{lang.t('community.banned')}</Option>
|
||||
</SelectField>
|
||||
@@ -63,8 +63,9 @@ class Table extends Component {
|
||||
<SelectField label={'Select me'} value={row.roles[0] || ''}
|
||||
className={styles.selectField}
|
||||
label={lang.t('community.role')}
|
||||
onChange={role => this.onRoleChange(row.id, role)}>
|
||||
onChange={(role) => this.onRoleChange(row.id, role)}>
|
||||
<Option value={''}>.</Option>
|
||||
<Option value={'STAFF'}>{lang.t('community.staff')}</Option>
|
||||
<Option value={'MODERATOR'}>{lang.t('community.moderator')}</Option>
|
||||
<Option value={'ADMIN'}>{lang.t('community.admin')}</Option>
|
||||
</SelectField>
|
||||
@@ -77,4 +78,4 @@ class Table extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(state => ({commenters: state.community.get('accounts')}))(Table);
|
||||
export default connect((state) => ({commenters: state.community.get('accounts')}))(Table);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import styles from '../Community.css';
|
||||
import BanUserButton from '../../../components/BanUserButton';
|
||||
import BanUserButton from './BanUserButton';
|
||||
import {Button} from 'coral-ui';
|
||||
import {menuActionsMap} from '../../../containers/ModerationQueue/helpers/moderationQueueActionsMap';
|
||||
|
||||
|
||||
@@ -10,16 +10,16 @@ const lang = new I18n(translations);
|
||||
|
||||
const stages = [
|
||||
{
|
||||
title: 'suspenduser.title_0',
|
||||
description: 'suspenduser.description_0',
|
||||
title: 'suspenduser.title_reject',
|
||||
description: 'suspenduser.description_reject',
|
||||
options: {
|
||||
'j': 'suspenduser.no_cancel',
|
||||
'k': 'suspenduser.yes_suspend'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'suspenduser.title_1',
|
||||
description: 'suspenduser.description_1',
|
||||
title: 'suspenduser.title_notify',
|
||||
description: 'suspenduser.description_notify',
|
||||
options: {
|
||||
'j': 'bandialog.cancel',
|
||||
'k': 'suspenduser.send'
|
||||
@@ -34,11 +34,11 @@ class SuspendUserDialog extends Component {
|
||||
static propTypes = {
|
||||
stage: PropTypes.number,
|
||||
handleClose: PropTypes.func.isRequired,
|
||||
suspendUser: PropTypes.func.isRequired
|
||||
rejectUsername: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({email: lang.t('suspenduser.email'), about: lang.t('suspenduser.username')});
|
||||
this.setState({email: lang.t('suspenduser.email_message_reject'), about: lang.t('suspenduser.username')});
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -46,13 +46,13 @@ class SuspendUserDialog extends Component {
|
||||
* handles the possible actions for that dialog.
|
||||
*/
|
||||
onActionClick = (stage, menuOption) => () => {
|
||||
const {suspendUser, user} = this.props;
|
||||
const {rejectUsername, user} = this.props;
|
||||
const {stage} = this.state;
|
||||
|
||||
const cancel = this.props.handleClose;
|
||||
const next = () => this.setState({stage: stage + 1});
|
||||
const suspend = () => {
|
||||
suspendUser({userId: user.user.id, message: this.state.email})
|
||||
rejectUsername({id: user.user.id, message: this.state.email})
|
||||
.then(() => {
|
||||
this.props.handleClose();
|
||||
});
|
||||
@@ -79,7 +79,7 @@ class SuspendUserDialog extends Component {
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
onCancel={handleClose}
|
||||
title={lang.t('suspenduser.title')}>
|
||||
title={lang.t('suspenduser.suspend_user')}>
|
||||
<div className={styles.title}>
|
||||
{lang.t(stages[stage].title, lang.t('suspenduser.username'))}
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ import translations from '../../../translations.json';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
// Render a single user for the list
|
||||
const User = props => {
|
||||
const User = (props) => {
|
||||
const {user, modActionButtons} = props;
|
||||
let userStatus = user.status;
|
||||
|
||||
@@ -37,7 +37,7 @@ const User = props => {
|
||||
<div className={styles.flaggedByCount}>
|
||||
<i className="material-icons">flag</i><span className={styles.flaggedByLabel}>{lang.t('community.flags')}({ user.actions.length })</span>:
|
||||
{ user.action_summaries.map(
|
||||
(action, i ) => {
|
||||
(action, i) => {
|
||||
return <span className={styles.flaggedBy} key={i}>
|
||||
{lang.t(`community.${action.reason}`)} ({action.count})
|
||||
</span>;
|
||||
@@ -46,7 +46,7 @@ const User = props => {
|
||||
</div>
|
||||
<div className={styles.flaggedReasons}>
|
||||
{ user.action_summaries.map(
|
||||
(action_sum, i ) => {
|
||||
(action_sum, i) => {
|
||||
return <div key={i}>
|
||||
<span className={styles.flaggedByLabel}>
|
||||
{lang.t(`community.${action_sum.reason}`)} ({action_sum.count})
|
||||
|
||||
@@ -96,24 +96,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
.inlineTextfield {
|
||||
border-color: #ccc;
|
||||
border-style: solid;
|
||||
border-width: 0px 0px 1px 0px;
|
||||
text-align: center;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.inlineTextfield:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.charCountTexfield {
|
||||
width: 4em;
|
||||
padding: 0px;
|
||||
border-color: #ccc;
|
||||
border-style: solid;
|
||||
border-width: 0px 0px 1px 0px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.charCountTexfieldEnabled {
|
||||
border-color: #00796b;
|
||||
}
|
||||
|
||||
.charCountTexfield:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.changedSave {
|
||||
background-color: #00796B;
|
||||
color: white;
|
||||
|
||||
@@ -15,6 +15,7 @@ import translations from 'coral-admin/src/translations.json';
|
||||
import StreamSettings from './StreamSettings';
|
||||
import ModerationSettings from './ModerationSettings';
|
||||
import TechSettings from './TechSettings';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
|
||||
class Configure extends Component {
|
||||
constructor (props) {
|
||||
@@ -118,6 +119,11 @@ class Configure extends Component {
|
||||
render () {
|
||||
const {activeSection} = this.state;
|
||||
const section = this.getSection(activeSection);
|
||||
const {auth: {user}} = this.props;
|
||||
|
||||
if (!can(user, 'UPDATE_CONFIG')) {
|
||||
return <p>You must be an administrator to access config settings. Please find the nearest Admin and ask them to level you up!</p>;
|
||||
}
|
||||
|
||||
const showSave = Object.keys(this.state.errors).reduce(
|
||||
(bool, error) => this.state.errors[error] ? false : bool, this.state.changed);
|
||||
@@ -171,7 +177,8 @@ class Configure extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
const mapStateToProps = (state) => ({
|
||||
auth: state.auth.toJS(),
|
||||
settings: state.settings.toJS()
|
||||
});
|
||||
export default connect(mapStateToProps)(Configure);
|
||||
|
||||
@@ -18,8 +18,8 @@ const Domainlist = ({domains, onChangeDomainlist}) => {
|
||||
value={domains}
|
||||
inputProps={{placeholder: 'URL'}}
|
||||
addOnPaste={true}
|
||||
pasteSplit={data => data.split(',').map(d => d.trim())}
|
||||
onChange={tags => onChangeDomainlist('whitelist', tags)}
|
||||
pasteSplit={(data) => data.split(',').map((d) => d.trim())}
|
||||
onChange={(tags) => onChangeDomainlist('whitelist', tags)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,7 @@ class EmbedLink extends Component {
|
||||
location.protocol,
|
||||
'//',
|
||||
location.hostname,
|
||||
location.port ? (`:${ window.location.port}`) : ''
|
||||
location.port ? (`:${window.location.port}`) : ''
|
||||
].join('');
|
||||
const coralJsUrl = [talkBaseUrl, '/embed.js'].join('');
|
||||
const nonce = String(Math.random()).slice(2);
|
||||
|
||||
@@ -27,6 +27,12 @@ const ModerationSettings = ({settings, updateSettings, onChangeWordlist}) => {
|
||||
const on = styles.enabledSetting;
|
||||
const off = styles.disabledSetting;
|
||||
|
||||
const onChangeEditCommentWindowLength = (e) => {
|
||||
const value = e.target.value;
|
||||
const valueAsNumber = parseFloat(value);
|
||||
const milliseconds = (!isNaN(valueAsNumber)) && (valueAsNumber * 1000);
|
||||
updateSettings({editCommentWindowLength: milliseconds || value});
|
||||
};
|
||||
return (
|
||||
<div className={styles.Configure}>
|
||||
<Card className={`${styles.configSetting} ${settings.requireEmailConfirmation ? on : off}`}>
|
||||
@@ -72,6 +78,27 @@ const ModerationSettings = ({settings, updateSettings, onChangeWordlist}) => {
|
||||
bannedWords={settings.wordlist.banned}
|
||||
suspectWords={settings.wordlist.suspect}
|
||||
onChangeWordlist={onChangeWordlist} />
|
||||
|
||||
{/* Edit Comment Timeframe */}
|
||||
<Card className={styles.configSetting}>
|
||||
<div className={styles.settingsHeader}>{lang.t('configure.edit-comment-timeframe-heading')}</div>
|
||||
<p>
|
||||
{lang.t('configure.edit-comment-timeframe-text-pre')}
|
||||
|
||||
<input
|
||||
style={{width: '3em'}}
|
||||
className={styles.inlineTextfield}
|
||||
type="number"
|
||||
min="0"
|
||||
onChange={onChangeEditCommentWindowLength}
|
||||
placeholder="30"
|
||||
defaultValue={(settings.editCommentWindowLength / 1000) /* saved as ms, rendered as seconds */}
|
||||
pattern='[0-9]+([\.][0-9]*)?'
|
||||
/>
|
||||
|
||||
{lang.t('configure.edit-comment-timeframe-text-post')}
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -81,7 +81,7 @@ const StreamSettings = ({updateSettings, settingsError, settings, errors}) => {
|
||||
<p className={settings.charCountEnable ? '' : styles.disabledSettingText}>
|
||||
<span>{lang.t('configure.comment-count-text-pre')}</span>
|
||||
<input type='text'
|
||||
className={`${styles.charCountTexfield} ${settings.charCountEnable && styles.charCountTexfieldEnabled}`}
|
||||
className={`${styles.inlineTextfield} ${styles.charCountTexfield} ${settings.charCountEnable && styles.charCountTexfieldEnabled}`}
|
||||
htmlFor='charCount'
|
||||
onChange={updateCharCount(updateSettings, settingsError)}
|
||||
value={settings.charCount}
|
||||
@@ -170,7 +170,7 @@ export default StreamSettings;
|
||||
|
||||
// To see if we are talking about weeks, days or hours
|
||||
// We talk the remainder of the division and see if it's 0
|
||||
const getTimeoutMeasure = ts => {
|
||||
const getTimeoutMeasure = (ts) => {
|
||||
if (ts % TIMESTAMPS['weeks'] === 0) {
|
||||
return 'weeks';
|
||||
} else if (ts % TIMESTAMPS['days'] === 0) {
|
||||
@@ -182,6 +182,6 @@ const getTimeoutMeasure = ts => {
|
||||
|
||||
// Dividing the amount by it's measure (hours, days, weeks) we
|
||||
// obtain the amount of time
|
||||
const getTimeoutAmount = ts => ts / TIMESTAMPS[getTimeoutMeasure(ts)];
|
||||
const getTimeoutAmount = (ts) => ts / TIMESTAMPS[getTimeoutMeasure(ts)];
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
@@ -15,8 +15,8 @@ const Wordlist = ({suspectWords, bannedWords, onChangeWordlist}) => (
|
||||
value={bannedWords}
|
||||
inputProps={{placeholder: 'word or phrase'}}
|
||||
addOnPaste={true}
|
||||
pasteSplit={data => data.split(',').map(d => d.trim())}
|
||||
onChange={tags => onChangeWordlist('banned', tags)}
|
||||
pasteSplit={(data) => data.split(',').map((d) => d.trim())}
|
||||
onChange={(tags) => onChangeWordlist('banned', tags)}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -28,8 +28,8 @@ const Wordlist = ({suspectWords, bannedWords, onChangeWordlist}) => (
|
||||
value={suspectWords}
|
||||
inputProps={{placeholder: 'word or phrase'}}
|
||||
addOnPaste={true}
|
||||
pasteSplit={data => data.split(',').map(d => d.trim())}
|
||||
onChange={tags => onChangeWordlist('suspect', tags)} />
|
||||
pasteSplit={(data) => data.split(',').map((d) => d.trim())}
|
||||
onChange={(tags) => onChangeWordlist('suspect', tags)} />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@ const ActivityWidget = ({assets}) => {
|
||||
<div className={styles.widgetTable}>
|
||||
{
|
||||
assets.length
|
||||
? assets.map(asset => {
|
||||
? assets.map((asset) => {
|
||||
return (
|
||||
<div className={styles.rowLinkify} key={asset.id}>
|
||||
<Link className={styles.linkToModerate} to={`/admin/moderate/flagged/${asset.id}`}>Moderate</Link>
|
||||
|
||||
@@ -6,7 +6,6 @@ import {getMetrics} from 'coral-admin/src/graphql/queries';
|
||||
import FlagWidget from './FlagWidget';
|
||||
import ActivityWidget from './ActivityWidget';
|
||||
import CountdownTimer from 'coral-admin/src/components/CountdownTimer';
|
||||
import {showBanUserDialog, hideBanUserDialog} from 'coral-admin/src/actions/moderation';
|
||||
|
||||
import {Spinner} from 'coral-ui';
|
||||
|
||||
@@ -36,19 +35,14 @@ class Dashboard extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
settings: state.settings.toJS(),
|
||||
moderation: state.moderation.toJS()
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
showBanUserDialog: (user, commentId) => dispatch(showBanUserDialog(user, commentId)),
|
||||
hideBanUserDialog: () => dispatch(hideBanUserDialog(false))
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(mapStateToProps),
|
||||
getMetrics
|
||||
)(Dashboard);
|
||||
|
||||
@@ -18,10 +18,10 @@ const FlagWidget = ({assets}) => {
|
||||
<div className={styles.widgetTable}>
|
||||
{
|
||||
assets.length
|
||||
? assets.map(asset => {
|
||||
? assets.map((asset) => {
|
||||
let flagSummary = null;
|
||||
if (asset.action_summaries) {
|
||||
flagSummary = asset.action_summaries.find(s => s.type === 'FlagAssetActionSummary');
|
||||
flagSummary = asset.action_summaries.find((s) => s.__typename === 'FlagAssetActionSummary');
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -18,8 +18,8 @@ const LikeWidget = ({assets}) => {
|
||||
<div className={styles.widgetTable}>
|
||||
{
|
||||
assets.length
|
||||
? assets.map(asset => {
|
||||
const likeSummary = asset.action_summaries.find(s => s.type === 'LikeAssetActionSummary');
|
||||
? assets.map((asset) => {
|
||||
const likeSummary = asset.action_summaries.find((s) => s.type === 'LikeAssetActionSummary');
|
||||
return (
|
||||
<div className={styles.rowLinkify} key={asset.id}>
|
||||
<Link className={styles.linkToModerate} to={`/admin/moderate/flagged/${asset.id}`}>Moderate</Link>
|
||||
|
||||
@@ -7,7 +7,7 @@ import BanUserDialog from 'coral-admin/src/components/BanUserDialog';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const MostLikedCommentsWidget = props => {
|
||||
const MostLikedCommentsWidget = (props) => {
|
||||
const {
|
||||
comments,
|
||||
moderation,
|
||||
|
||||
@@ -64,32 +64,32 @@ InstallContainer.contextTypes = {
|
||||
router: React.PropTypes.object
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
const mapStateToProps = (state) => ({
|
||||
install: state.install.toJS()
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
nextStep: () => dispatch(nextStep()),
|
||||
goToStep: step => dispatch(goToStep(step)),
|
||||
goToStep: (step) => dispatch(goToStep(step)),
|
||||
previousStep: () => dispatch(previousStep()),
|
||||
finishInstall: () => dispatch(finishInstall()),
|
||||
checkInstall: next => dispatch(checkInstall(next)),
|
||||
handleDomainsChange: value => {
|
||||
checkInstall: (next) => dispatch(checkInstall(next)),
|
||||
handleDomainsChange: (value) => {
|
||||
dispatch(updatePermittedDomains(value));
|
||||
},
|
||||
handleSettingsChange: e => {
|
||||
handleSettingsChange: (e) => {
|
||||
const {name, value} = e.currentTarget;
|
||||
dispatch(updateSettingsFormData(name, value));
|
||||
},
|
||||
handleUserChange: e => {
|
||||
handleUserChange: (e) => {
|
||||
const {name, value} = e.currentTarget;
|
||||
dispatch(updateUserFormData(name, value));
|
||||
},
|
||||
handleSettingsSubmit: e => {
|
||||
handleSettingsSubmit: (e) => {
|
||||
e.preventDefault();
|
||||
dispatch(submitSettings());
|
||||
},
|
||||
handleUserSubmit: e => {
|
||||
handleUserSubmit: (e) => {
|
||||
e.preventDefault();
|
||||
dispatch(submitUser());
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ const lang = new I18n(translations);
|
||||
import translations from '../../translations.json';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
|
||||
const AddOrganizationName = props => {
|
||||
const AddOrganizationName = (props) => {
|
||||
const {handleSettingsChange, handleSettingsSubmit, install} = props;
|
||||
return (
|
||||
<div className={styles.step}>
|
||||
|
||||
@@ -6,7 +6,7 @@ const lang = new I18n(translations);
|
||||
import translations from '../../translations.json';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
|
||||
const InitialStep = props => {
|
||||
const InitialStep = (props) => {
|
||||
const {handleUserChange, handleUserSubmit, install} = props;
|
||||
return (
|
||||
<div className={styles.step}>
|
||||
|
||||
@@ -6,7 +6,7 @@ const lang = new I18n(translations);
|
||||
import translations from '../../translations.json';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
|
||||
const InitialStep = props => {
|
||||
const InitialStep = (props) => {
|
||||
const {nextStep} = props;
|
||||
return (
|
||||
<div className={styles.step}>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import styles from './style.css';
|
||||
import {Button, Select, Option, TextField} from 'coral-ui';
|
||||
|
||||
const InviteTeamMembers = props => {
|
||||
const InviteTeamMembers = (props) => {
|
||||
const {nextStep} = props;
|
||||
return (
|
||||
<div className={styles.step}>
|
||||
|
||||
@@ -7,7 +7,7 @@ const lang = new I18n(translations);
|
||||
import translations from '../../translations.json';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
|
||||
const PermittedDomainsStep = props => {
|
||||
const PermittedDomainsStep = (props) => {
|
||||
const {finishInstall, install, handleDomainsChange} = props;
|
||||
const domains = install.data.settings.domains.whitelist;
|
||||
return (
|
||||
@@ -19,8 +19,8 @@ const PermittedDomainsStep = props => {
|
||||
value={domains}
|
||||
inputProps={{placeholder: 'URL'}}
|
||||
addOnPaste={true}
|
||||
pasteSplit={data => data.split(',').map(d => d.trim())}
|
||||
onChange={tags => handleDomainsChange(tags)}
|
||||
pasteSplit={(data) => data.split(',').map((d) => d.trim())}
|
||||
onChange={(tags) => handleDomainsChange(tags)}
|
||||
/>
|
||||
</Card>
|
||||
<Button cStyle='green' onClick={finishInstall} raised>{lang.t('PERMITTED_DOMAINS.SUBMIT')}</Button>
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import Layout from '../components/ui/Layout';
|
||||
import {checkLogin, handleLogin, logout, requestPasswordReset} from '../actions/auth';
|
||||
import {fetchConfig} from '../actions/config';
|
||||
import {FullLoading} from '../components/FullLoading';
|
||||
import AdminLogin from '../components/AdminLogin';
|
||||
import {logout} from 'coral-framework/actions/auth';
|
||||
import {FullLoading} from '../components/FullLoading';
|
||||
import {toggleModal as toggleShortcutModal} from '../actions/moderation';
|
||||
import {checkLogin, handleLogin, requestPasswordReset} from '../actions/auth';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
|
||||
class LayoutContainer extends Component {
|
||||
componentWillMount () {
|
||||
componentWillMount() {
|
||||
const {checkLogin, fetchConfig} = this.props;
|
||||
|
||||
checkLogin();
|
||||
fetchConfig();
|
||||
}
|
||||
render () {
|
||||
render() {
|
||||
const {
|
||||
isAdmin,
|
||||
user,
|
||||
loggedIn,
|
||||
loadingUser,
|
||||
loginError,
|
||||
@@ -23,36 +26,60 @@ class LayoutContainer extends Component {
|
||||
passwordRequestSuccess
|
||||
} = this.props.auth;
|
||||
|
||||
const {handleLogout, TALK_RECAPTCHA_PUBLIC} = this.props;
|
||||
if (loadingUser) { return <FullLoading />; }
|
||||
if (!isAdmin) {
|
||||
return <AdminLogin
|
||||
loginMaxExceeded={loginMaxExceeded}
|
||||
handleLogin={this.props.handleLogin}
|
||||
requestPasswordReset={this.props.requestPasswordReset}
|
||||
passwordRequestSuccess={passwordRequestSuccess}
|
||||
recaptchaPublic={TALK_RECAPTCHA_PUBLIC}
|
||||
errorMessage={loginError} />;
|
||||
const {
|
||||
handleLogout,
|
||||
toggleShortcutModal,
|
||||
TALK_RECAPTCHA_PUBLIC
|
||||
} = this.props;
|
||||
if (loadingUser) {
|
||||
return <FullLoading />;
|
||||
}
|
||||
if (!loggedIn) {
|
||||
return (
|
||||
<AdminLogin
|
||||
loginMaxExceeded={loginMaxExceeded}
|
||||
handleLogin={this.props.handleLogin}
|
||||
requestPasswordReset={this.props.requestPasswordReset}
|
||||
passwordRequestSuccess={passwordRequestSuccess}
|
||||
recaptchaPublic={TALK_RECAPTCHA_PUBLIC}
|
||||
errorMessage={loginError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (can(user, 'ACCESS_ADMIN') && loggedIn) {
|
||||
return (
|
||||
<Layout
|
||||
handleLogout={handleLogout}
|
||||
toggleShortcutModal={toggleShortcutModal}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
} else if (loggedIn) {
|
||||
return (
|
||||
<Layout {...this.props}>
|
||||
<p>This page is for team use only. Please contact an administrator if you want to join this team.</p>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
if (isAdmin && loggedIn) { return <Layout handleLogout={handleLogout} {...this.props} />; }
|
||||
return <FullLoading />;
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
const mapStateToProps = (state) => ({
|
||||
auth: state.auth.toJS(),
|
||||
TALK_RECAPTCHA_PUBLIC: state.config.get('data').get('TALK_RECAPTCHA_PUBLIC', null)
|
||||
TALK_RECAPTCHA_PUBLIC: state.config
|
||||
.get('data')
|
||||
.get('TALK_RECAPTCHA_PUBLIC', null)
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
checkLogin: () => dispatch(checkLogin()),
|
||||
fetchConfig: () => dispatch(fetchConfig()),
|
||||
handleLogin: (username, password, recaptchaResponse) => dispatch(handleLogin(username, password, recaptchaResponse)),
|
||||
requestPasswordReset: email => dispatch(requestPasswordReset(email)),
|
||||
handleLogin: (username, password, recaptchaResponse) =>
|
||||
dispatch(handleLogin(username, password, recaptchaResponse)),
|
||||
requestPasswordReset: (email) => dispatch(requestPasswordReset(email)),
|
||||
toggleShortcutModal: (toggle) => dispatch(toggleShortcutModal(toggle)),
|
||||
handleLogout: () => dispatch(logout())
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(LayoutContainer);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer);
|
||||
|
||||
@@ -1,24 +1,42 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {compose} from 'react-apollo';
|
||||
import * as notification from 'coral-admin/src/services/notification';
|
||||
import key from 'keymaster';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import styles from './components/styles.css';
|
||||
import translations from 'coral-admin/src/translations';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
|
||||
import {modQueueQuery} from '../../graphql/queries';
|
||||
import {banUser, setCommentStatus} from '../../graphql/mutations';
|
||||
import {modQueueQuery, getQueueCounts} from '../../graphql/queries';
|
||||
import {banUser, setCommentStatus, suspendUser} from '../../graphql/mutations';
|
||||
|
||||
import {fetchSettings} from 'actions/settings';
|
||||
import {updateAssets} from 'actions/assets';
|
||||
import {toggleModal, singleView, showBanUserDialog, hideBanUserDialog} from 'actions/moderation';
|
||||
import {
|
||||
toggleModal,
|
||||
singleView,
|
||||
showBanUserDialog,
|
||||
hideBanUserDialog,
|
||||
showSuspendUserDialog,
|
||||
hideSuspendUserDialog,
|
||||
hideShortcutsNote,
|
||||
viewUserDetail,
|
||||
hideUserDetail
|
||||
} from 'actions/moderation';
|
||||
|
||||
import {Spinner} from 'coral-ui';
|
||||
import BanUserDialog from '../../components/BanUserDialog';
|
||||
import BanUserDialog from './components/BanUserDialog';
|
||||
import SuspendUserDialog from './components/SuspendUserDialog';
|
||||
import ModerationQueue from './ModerationQueue';
|
||||
import ModerationMenu from './components/ModerationMenu';
|
||||
import ModerationHeader from './components/ModerationHeader';
|
||||
import NotFoundAsset from './components/NotFoundAsset';
|
||||
import ModerationKeysModal from '../../components/ModerationKeysModal';
|
||||
import UserDetail from './UserDetail';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
class ModerationContainer extends Component {
|
||||
state = {
|
||||
@@ -43,12 +61,13 @@ class ModerationContainer extends Component {
|
||||
const {acceptComment, rejectComment} = this.props;
|
||||
const {selectedIndex} = this.state;
|
||||
const comments = this.getComments();
|
||||
const commentId = {commentId: comments[selectedIndex].id};
|
||||
const comment = comments[selectedIndex];
|
||||
const commentId = {commentId: comment.id};
|
||||
|
||||
if (accept) {
|
||||
acceptComment(commentId);
|
||||
comment.status !== 'ACCEPTED' && acceptComment(commentId);
|
||||
} else {
|
||||
rejectComment(commentId);
|
||||
comment.status !== 'REJECTED' && rejectComment(commentId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,14 +79,14 @@ class ModerationContainer extends Component {
|
||||
|
||||
select = (next) => () => {
|
||||
if (next) {
|
||||
this.setState(prevState =>
|
||||
this.setState((prevState) =>
|
||||
({
|
||||
...prevState,
|
||||
selectedIndex: prevState.selectedIndex < this.getComments().length - 1
|
||||
? prevState.selectedIndex + 1 : prevState.selectedIndex
|
||||
}));
|
||||
} else {
|
||||
this.setState(prevState =>
|
||||
this.setState((prevState) =>
|
||||
({
|
||||
...prevState,
|
||||
selectedIndex: prevState.selectedIndex > 0 ?
|
||||
@@ -81,6 +100,33 @@ class ModerationContainer extends Component {
|
||||
this.props.modQueueResort(sort);
|
||||
}
|
||||
|
||||
suspendUser = async (args) => {
|
||||
this.props.hideSuspendUserDialog();
|
||||
try {
|
||||
const result = await this.props.suspendUser(args);
|
||||
if (result.data.suspendUser.errors) {
|
||||
throw result.data.suspendUser.errors;
|
||||
}
|
||||
notification.success(
|
||||
lang.t('suspenduser.notify_suspend_until',
|
||||
this.props.moderation.suspendUserDialog.username,
|
||||
lang.timeago(args.until)),
|
||||
);
|
||||
const {commentStatus, commentId} = this.props.moderation.suspendUserDialog;
|
||||
if (commentStatus !== 'REJECTED') {
|
||||
return this.props.rejectComment({commentId})
|
||||
.then((result) => {
|
||||
if (result.data.setCommentStatus.errors) {
|
||||
throw result.data.setCommentStatus.errors;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
notification.showMutationErrors(err);
|
||||
}
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
key.unbind('s');
|
||||
key.unbind('shift+/');
|
||||
@@ -110,7 +156,7 @@ class ModerationContainer extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {data, moderation, settings, assets, onClose, ...props} = this.props;
|
||||
const {data, moderation, settings, assets, onClose, viewUserDetail, hideUserDetail, ...props} = this.props;
|
||||
const providedAssetId = this.props.params.id;
|
||||
const activeTab = this.props.route.path === ':id' ? 'premod' : this.props.route.path;
|
||||
|
||||
@@ -125,7 +171,7 @@ class ModerationContainer extends Component {
|
||||
}
|
||||
|
||||
if (providedAssetId) {
|
||||
asset = assets.find(asset => asset.id === this.props.params.id);
|
||||
asset = assets.find((asset) => asset.id === this.props.params.id);
|
||||
|
||||
if (!asset) {
|
||||
return <NotFoundAsset assetId={providedAssetId} />;
|
||||
@@ -135,6 +181,12 @@ class ModerationContainer extends Component {
|
||||
const comments = data[activeTab];
|
||||
let activeTabCount;
|
||||
switch(activeTab) {
|
||||
case 'all':
|
||||
activeTabCount = data.allCount;
|
||||
break;
|
||||
case 'accepted':
|
||||
activeTabCount = data.acceptedCount;
|
||||
break;
|
||||
case 'premod':
|
||||
activeTabCount = data.premodCount;
|
||||
break;
|
||||
@@ -151,6 +203,8 @@ class ModerationContainer extends Component {
|
||||
<ModerationHeader asset={asset} />
|
||||
<ModerationMenu
|
||||
asset={asset}
|
||||
allCount={data.allCount}
|
||||
acceptedCount={data.acceptedCount}
|
||||
premodCount={data.premodCount}
|
||||
rejectedCount={data.rejectedCount}
|
||||
flaggedCount={data.flaggedCount}
|
||||
@@ -166,49 +220,79 @@ class ModerationContainer extends Component {
|
||||
bannedWords={settings.wordlist.banned}
|
||||
suspectWords={settings.wordlist.suspect}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
showSuspendUserDialog={props.showSuspendUserDialog}
|
||||
acceptComment={props.acceptComment}
|
||||
rejectComment={props.rejectComment}
|
||||
loadMore={props.loadMore}
|
||||
assetId={providedAssetId}
|
||||
sort={this.state.sort}
|
||||
commentCount={activeTabCount}
|
||||
currentUserId={this.props.auth.user.id}
|
||||
viewUserDetail={viewUserDetail}
|
||||
hideUserDetail={hideUserDetail}
|
||||
/>
|
||||
<BanUserDialog
|
||||
open={moderation.banDialog}
|
||||
user={moderation.user}
|
||||
commentId={moderation.commentId}
|
||||
commentStatus={moderation.commentStatus}
|
||||
handleClose={props.hideBanUserDialog}
|
||||
handleBanUser={props.banUser}
|
||||
showRejectedNote={moderation.showRejectedNote}
|
||||
rejectComment={props.rejectComment}
|
||||
/>
|
||||
<ModerationKeysModal
|
||||
<SuspendUserDialog
|
||||
open={moderation.suspendUserDialog.show}
|
||||
username={moderation.suspendUserDialog.username}
|
||||
userId={moderation.suspendUserDialog.userId}
|
||||
organizationName={data.settings.organizationName}
|
||||
onCancel={props.hideSuspendUserDialog}
|
||||
onPerform={this.suspendUser}
|
||||
/>
|
||||
<ModerationKeysModal
|
||||
hideShortcutsNote={props.hideShortcutsNote}
|
||||
shortcutsNoteVisible={moderation.shortcutsNoteVisible}
|
||||
open={moderation.modalOpen}
|
||||
onClose={onClose}/>
|
||||
{moderation.userDetailId && (
|
||||
<UserDetail
|
||||
id={moderation.userDetailId}
|
||||
hideUserDetail={hideUserDetail} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
const mapStateToProps = (state) => ({
|
||||
moderation: state.moderation.toJS(),
|
||||
settings: state.settings.toJS(),
|
||||
auth: state.auth.toJS(),
|
||||
assets: state.assets.get('assets')
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
toggleModal: toggle => dispatch(toggleModal(toggle)),
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
onClose: () => dispatch(toggleModal(false)),
|
||||
singleView: () => dispatch(singleView()),
|
||||
updateAssets: assets => dispatch(updateAssets(assets)),
|
||||
fetchSettings: () => dispatch(fetchSettings()),
|
||||
showBanUserDialog: (user, commentId, showRejectedNote) => dispatch(showBanUserDialog(user, commentId, showRejectedNote)),
|
||||
hideBanUserDialog: () => dispatch(hideBanUserDialog(false)),
|
||||
...bindActionCreators({
|
||||
toggleModal,
|
||||
singleView,
|
||||
updateAssets,
|
||||
fetchSettings,
|
||||
showBanUserDialog,
|
||||
hideShortcutsNote,
|
||||
showSuspendUserDialog,
|
||||
hideSuspendUserDialog,
|
||||
viewUserDetail,
|
||||
hideUserDetail,
|
||||
}, dispatch),
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
setCommentStatus,
|
||||
getQueueCounts,
|
||||
banUser,
|
||||
suspendUser,
|
||||
modQueueQuery,
|
||||
banUser
|
||||
)(ModerationContainer);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
const ModerationLayout = props => (
|
||||
const ModerationLayout = (props) => (
|
||||
<div>
|
||||
{props.children}
|
||||
</div>
|
||||
|
||||
@@ -9,52 +9,82 @@ import translations from 'coral-admin/src/translations';
|
||||
import LoadMore from './components/LoadMore';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
const ModerationQueue = ({comments, selectedIndex, commentCount, singleView, loadMore, activeTab, sort, ...props}) => {
|
||||
return (
|
||||
<div id="moderationList" className={`${styles.list} ${singleView ? styles.singleView : ''}`}>
|
||||
<ul style={{paddingLeft: 0}}>
|
||||
{
|
||||
comments.length
|
||||
? comments.map((comment, i) => {
|
||||
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
|
||||
return <Comment
|
||||
key={i}
|
||||
index={i}
|
||||
comment={comment}
|
||||
commentType={activeTab}
|
||||
selected={i === selectedIndex}
|
||||
suspectWords={props.suspectWords}
|
||||
bannedWords={props.bannedWords}
|
||||
actions={actionsMap[status]}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
acceptComment={props.acceptComment}
|
||||
rejectComment={props.rejectComment}
|
||||
currentAsset={props.currentAsset}
|
||||
/>;
|
||||
})
|
||||
: <EmptyCard>{lang.t('modqueue.emptyqueue')}</EmptyCard>
|
||||
}
|
||||
</ul>
|
||||
<LoadMore
|
||||
comments={comments}
|
||||
loadMore={loadMore}
|
||||
sort={sort}
|
||||
tab={activeTab}
|
||||
showLoadMore={comments.length < commentCount}
|
||||
assetId={props.assetId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
class ModerationQueue extends React.Component {
|
||||
|
||||
ModerationQueue.propTypes = {
|
||||
bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
currentAsset: PropTypes.object,
|
||||
showBanUserDialog: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
comments: PropTypes.array.isRequired
|
||||
};
|
||||
static propTypes = {
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
currentAsset: PropTypes.object,
|
||||
showBanUserDialog: PropTypes.func.isRequired,
|
||||
showSuspendUserDialog: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
comments: PropTypes.array.isRequired
|
||||
}
|
||||
|
||||
componentDidUpdate (prev) {
|
||||
const {loadMore, comments, commentCount, sort, activeTab: tab, assetId: asset_id} = this.props;
|
||||
|
||||
// if the user just moderated the last (visible) comment
|
||||
// AND there are more comments available on the server,
|
||||
// go ahead and load more comments
|
||||
if (prev.comments.length > 0 && comments.length === 0 && commentCount > 0) {
|
||||
loadMore({sort, tab, asset_id});
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
comments,
|
||||
selectedIndex,
|
||||
commentCount,
|
||||
singleView,
|
||||
loadMore,
|
||||
activeTab,
|
||||
sort,
|
||||
viewUserDetail,
|
||||
...props
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div id="moderationList" className={`${styles.list} ${singleView ? styles.singleView : ''}`}>
|
||||
<ul style={{paddingLeft: 0}}>
|
||||
{
|
||||
comments.length
|
||||
? comments.map((comment, i) => {
|
||||
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
|
||||
return <Comment
|
||||
key={i}
|
||||
index={i}
|
||||
comment={comment}
|
||||
selected={i === selectedIndex}
|
||||
suspectWords={props.suspectWords}
|
||||
bannedWords={props.bannedWords}
|
||||
viewUserDetail={viewUserDetail}
|
||||
actions={actionsMap[status]}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
showSuspendUserDialog={props.showSuspendUserDialog}
|
||||
acceptComment={props.acceptComment}
|
||||
rejectComment={props.rejectComment}
|
||||
currentAsset={props.currentAsset}
|
||||
currentUserId={this.props.currentUserId}
|
||||
/>;
|
||||
})
|
||||
: <EmptyCard>{lang.t('modqueue.emptyqueue')}</EmptyCard>
|
||||
}
|
||||
</ul>
|
||||
<LoadMore
|
||||
comments={comments}
|
||||
loadMore={loadMore}
|
||||
sort={sort}
|
||||
tab={activeTab}
|
||||
showLoadMore={comments.length < commentCount}
|
||||
assetId={props.assetId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ModerationQueue;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
.copyButton {
|
||||
float: right;
|
||||
top: -10px;
|
||||
}
|
||||
|
||||
.memberSince {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.small {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
|
||||
.stat {
|
||||
margin: 0 4px 12px;
|
||||
}
|
||||
|
||||
.stat:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stat p:first-child {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.profileEmail {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
font-size: 16px;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Button, Drawer} from 'coral-ui';
|
||||
import styles from './UserDetail.css';
|
||||
import {compose} from 'react-apollo';
|
||||
import {getUserDetail} from 'coral-admin/src/graphql/queries';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
|
||||
class UserDetail extends React.Component {
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
hideUserDetail: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
copyPermalink = () => {
|
||||
this.profile.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
} catch (e) {
|
||||
|
||||
/* nothing */
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {data, hideUserDetail} = this.props;
|
||||
|
||||
if (!('user' in data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {user, totalComments, rejectedComments} = data;
|
||||
const localProfile = user.profiles.find((p) => p.provider === 'local');
|
||||
let profile;
|
||||
if (localProfile) {
|
||||
profile = localProfile.id;
|
||||
}
|
||||
|
||||
let rejectedPercent = rejectedComments / totalComments;
|
||||
if (rejectedPercent === Infinity || isNaN(rejectedPercent)) {
|
||||
|
||||
// if totalComments is 0, you're dividing by zero, which is naughty
|
||||
rejectedPercent = 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer handleClickOutside={hideUserDetail}>
|
||||
<h3>{user.username}</h3>
|
||||
<Button className={styles.copyButton} onClick={this.copyPermalink}>Copy</Button>
|
||||
{profile && <input className={styles.profileEmail} readOnly type="text" ref={(ref) => this.profile = ref} value={profile} />}
|
||||
<Slot fill="userProfile" user={user} />
|
||||
<p className={styles.memberSince}><strong>Member since</strong> {new Date(user.created_at).toLocaleString()}</p>
|
||||
<hr/>
|
||||
<p>
|
||||
<strong>Account summary</strong>
|
||||
<br/><small className={styles.small}>Data represents the last six months of activity</small>
|
||||
</p>
|
||||
<div className={styles.stats}>
|
||||
<div className={styles.stat}>
|
||||
<p>Total Comments</p>
|
||||
<p>{totalComments}</p>
|
||||
</div>
|
||||
<div className={styles.stat}>
|
||||
<p>Reject Rate</p>
|
||||
<p>{`${(rejectedPercent).toFixed(1)}%`}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
getUserDetail
|
||||
)(UserDetail);
|
||||
+4
-3
@@ -152,13 +152,14 @@ input.error{
|
||||
|
||||
.cancel {
|
||||
margin-right: 10px;
|
||||
width: 47%;
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.ban {
|
||||
width: 47%;
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin: 20px 0;
|
||||
margin: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
+5
-5
@@ -5,17 +5,17 @@ import styles from './BanUserDialog.css';
|
||||
import Button from 'coral-ui/components/Button';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
import translations from '../../../translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const onBanClick = (userId, commentId, handleBanUser, rejectComment, handleClose) => (e) => {
|
||||
const onBanClick = (userId, commentId, commentStatus, handleBanUser, rejectComment, handleClose) => (e) => {
|
||||
e.preventDefault();
|
||||
handleBanUser({userId})
|
||||
.then(handleClose)
|
||||
.then(() => rejectComment({commentId}));
|
||||
.then(() => commentStatus === 'REJECTED' ? null : rejectComment({commentId}));
|
||||
};
|
||||
|
||||
const BanUserDialog = ({open, handleClose, handleBanUser, rejectComment, user, commentId, showRejectedNote}) => (
|
||||
const BanUserDialog = ({open, handleClose, handleBanUser, rejectComment, user, commentId, commentStatus, showRejectedNote}) => (
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
id="banuserDialog"
|
||||
@@ -36,7 +36,7 @@ const BanUserDialog = ({open, handleClose, handleBanUser, rejectComment, user, c
|
||||
<Button cStyle="cancel" className={styles.cancel} onClick={handleClose} raised>
|
||||
{lang.t('bandialog.cancel')}
|
||||
</Button>
|
||||
<Button cStyle="black" className={styles.ban} onClick={onBanClick(user.id, commentId, handleBanUser, rejectComment, handleClose)} raised>
|
||||
<Button cStyle="black" className={styles.ban} onClick={onBanClick(user.id, commentId, commentStatus, handleBanUser, rejectComment, handleClose)} raised>
|
||||
{lang.t('bandialog.yes_ban_user')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -1,16 +1,18 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import timeago from 'timeago.js';
|
||||
import Linkify from 'react-linkify';
|
||||
import Highlighter from 'react-highlight-words';
|
||||
import {Link} from 'react-router';
|
||||
import Linkify from 'react-linkify';
|
||||
|
||||
import styles from './styles.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
import FlagBox from './FlagBox';
|
||||
import styles from './styles.css';
|
||||
import CommentType from './CommentType';
|
||||
import ActionButton from 'coral-admin/src/components/ActionButton';
|
||||
import BanUserButton from 'coral-admin/src/components/BanUserButton';
|
||||
import Highlighter from 'react-highlight-words';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import {getActionSummary} from 'coral-framework/utils';
|
||||
import ActionButton from 'coral-admin/src/components/ActionButton';
|
||||
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
|
||||
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
|
||||
const linkify = new Linkify();
|
||||
|
||||
@@ -18,85 +20,166 @@ import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from 'coral-admin/src/translations.json';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const Comment = ({actions = [], comment, ...props}) => {
|
||||
const Comment = ({
|
||||
actions = [],
|
||||
comment,
|
||||
viewUserDetail,
|
||||
suspectWords,
|
||||
bannedWords,
|
||||
...props
|
||||
}) => {
|
||||
const links = linkify.getMatches(comment.body);
|
||||
const linkText = links ? links.map(link => link.raw) : [];
|
||||
const linkText = links ? links.map((link) => link.raw) : [];
|
||||
const flagActionSummaries = getActionSummary('FlagActionSummary', comment);
|
||||
const flagActions = comment.actions && comment.actions.filter(a => a.__typename === 'FlagAction');
|
||||
const flagActions =
|
||||
comment.actions &&
|
||||
comment.actions.filter((a) => a.__typename === 'FlagAction');
|
||||
let commentType = '';
|
||||
if (comment.status === 'PREMOD') {
|
||||
commentType = 'premod';
|
||||
} else if (flagActions && flagActions.length) {
|
||||
commentType = 'flagged';
|
||||
}
|
||||
|
||||
// since words are checked against word boundaries on the backend,
|
||||
// this should be the behavior on the front end as well.
|
||||
// currently the highlighter plugin does not support this out of the box.
|
||||
const searchWords = [...suspectWords, ...bannedWords]
|
||||
.filter((w) => {
|
||||
return new RegExp(`(^|\\s)${w}(\\s|$)`).test(comment.body);
|
||||
})
|
||||
.concat(linkText);
|
||||
|
||||
return (
|
||||
<li tabIndex={props.index} className={`mdl-card ${props.selected ? 'mdl-shadow--8dp' : 'mdl-shadow--2dp'} ${styles.Comment} ${styles.listItem}`}>
|
||||
<li
|
||||
tabIndex={props.index}
|
||||
className={`mdl-card ${props.selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp'} ${styles.Comment} ${styles.listItem} ${props.selected ? styles.selected : ''}`}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.itemHeader}>
|
||||
<div className={styles.author}>
|
||||
<span>
|
||||
<span className={styles.username} onClick={() => viewUserDetail(comment.user.id)}>
|
||||
{comment.user.name}
|
||||
</span>
|
||||
<span className={styles.created}>
|
||||
{timeago().format(comment.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}
|
||||
{timeago().format(
|
||||
comment.created_at || Date.now() - props.index * 60 * 1000,
|
||||
lang.getLocale().replace('-', '_')
|
||||
)}
|
||||
</span>
|
||||
<BanUserButton user={comment.user} onClick={() => props.showBanUserDialog(comment.user, comment.id, comment.status !== 'REJECTED')} />
|
||||
<CommentType type={props.commentType} />
|
||||
{props.currentUserId !== comment.user.id &&
|
||||
<ActionsMenu icon="not_interested">
|
||||
<ActionsMenuItem
|
||||
disabled={comment.user.status === 'BANNED'}
|
||||
onClick={() => props.showSuspendUserDialog(comment.user.id, comment.user.name, comment.id, comment.status)}>
|
||||
Suspend User</ActionsMenuItem>
|
||||
<ActionsMenuItem
|
||||
disabled={comment.user.status === 'BANNED'}
|
||||
onClick={() => props.showBanUserDialog(comment.user, comment.id, comment.status, comment.status !== 'REJECTED')}>
|
||||
Ban User
|
||||
</ActionsMenuItem>
|
||||
</ActionsMenu>
|
||||
}
|
||||
<CommentType type={commentType} />
|
||||
</div>
|
||||
{comment.user.status === 'banned' ?
|
||||
<span className={styles.banned}>
|
||||
<Icon name='error_outline'/>
|
||||
{lang.t('comment.banned_user')}
|
||||
</span>
|
||||
{comment.user.status === 'banned'
|
||||
? <span className={styles.banned}>
|
||||
<Icon name="error_outline" />
|
||||
{lang.t('comment.banned_user')}
|
||||
</span>
|
||||
: null}
|
||||
<Slot fill="adminCommentInfoBar" comment={comment} />
|
||||
</div>
|
||||
<div className={styles.moderateArticle}>
|
||||
Story: {comment.asset.title}
|
||||
{!props.currentAsset && (
|
||||
<Link to={`/admin/moderate/${comment.asset.id}`}>Moderate →</Link>
|
||||
)}
|
||||
{!props.currentAsset &&
|
||||
<Link to={`/admin/moderate/${comment.asset.id}`}>Moderate →</Link>}
|
||||
</div>
|
||||
<div className={styles.itemBody}>
|
||||
<p className={styles.body}>
|
||||
<Highlighter
|
||||
searchWords={[...props.suspectWords, ...props.bannedWords, ...linkText]}
|
||||
textToHighlight={comment.body} />
|
||||
searchWords={searchWords}
|
||||
textToHighlight={comment.body}
|
||||
/>
|
||||
{' '}
|
||||
<a
|
||||
className={styles.external}
|
||||
href={`${comment.asset.url}#${comment.id}`}
|
||||
target="_blank"
|
||||
>
|
||||
<Icon name="open_in_new" /> {lang.t('comment.view_context')}
|
||||
</a>
|
||||
</p>
|
||||
<Slot fill="adminCommentContent" comment={comment} />
|
||||
<div className={styles.sideActions}>
|
||||
{links ? <span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
|
||||
{links
|
||||
? <span className={styles.hasLinks}>
|
||||
<Icon name="error_outline" /> Contains Link
|
||||
</span>
|
||||
: null}
|
||||
<div className={`actions ${styles.actions}`}>
|
||||
{actions.map((action, i) =>
|
||||
<ActionButton key={i}
|
||||
type={action}
|
||||
user={comment.user}
|
||||
acceptComment={() => props.acceptComment({commentId: comment.id})}
|
||||
rejectComment={() => props.rejectComment({commentId: comment.id})}
|
||||
/>
|
||||
)}
|
||||
{actions.map((action, i) => {
|
||||
const active =
|
||||
(action === 'REJECT' && comment.status === 'REJECTED') ||
|
||||
(action === 'APPROVE' && comment.status === 'ACCEPTED');
|
||||
return (
|
||||
<ActionButton
|
||||
key={i}
|
||||
type={action}
|
||||
user={comment.user}
|
||||
status={comment.status}
|
||||
active={active}
|
||||
acceptComment={() =>
|
||||
(comment.status === 'ACCEPTED'
|
||||
? null
|
||||
: props.acceptComment({commentId: comment.id}))}
|
||||
rejectComment={() =>
|
||||
(comment.status === 'REJECTED'
|
||||
? null
|
||||
: props.rejectComment({commentId: comment.id}))}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Slot fill="adminSideActions" comment={comment} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
flagActions && flagActions.length
|
||||
? <FlagBox actions={flagActions} actionSummaries={flagActionSummaries} />
|
||||
: null
|
||||
}
|
||||
<div>
|
||||
<Slot fill="adminCommentDetailArea" comment={comment} />
|
||||
</div>
|
||||
{flagActions && flagActions.length
|
||||
? <FlagBox
|
||||
actions={flagActions}
|
||||
actionSummaries={flagActionSummaries}
|
||||
/>
|
||||
: null}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
Comment.propTypes = {
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
currentAsset: PropTypes.object,
|
||||
showBanUserDialog: PropTypes.func.isRequired,
|
||||
showSuspendUserDialog: PropTypes.func.isRequired,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
comment: PropTypes.shape({
|
||||
body: PropTypes.string.isRequired,
|
||||
action_summaries: PropTypes.array,
|
||||
actions: PropTypes.array,
|
||||
created_at: PropTypes.string.isRequired,
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
status: PropTypes.string
|
||||
}),
|
||||
}).isRequired,
|
||||
asset: PropTypes.shape({
|
||||
title: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
id: PropTypes.string
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from './CommentCount.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from 'coral-admin/src/translations.json';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const CommentCount = props => (
|
||||
<span className={styles.count}>{props.count}</span>
|
||||
);
|
||||
const CommentCount = ({count}) => {
|
||||
let number = count;
|
||||
|
||||
// shorten large counts to abbreviations
|
||||
if (number / 1e9 > 1) {
|
||||
number = `${(number / 1e9).toFixed(1)}${lang.t('modqueue.billion')}`;
|
||||
} else if (number / 1e6 > 1) {
|
||||
number = `${(number / 1e6).toFixed(1)}${lang.t('modqueue.million')}`;
|
||||
} else if (number / 1e3 > 1) {
|
||||
number = `${(number / 1e3).toFixed(1)}${lang.t('modqueue.thousand')}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={styles.count}>{number}</span>
|
||||
);
|
||||
};
|
||||
|
||||
CommentCount.propTypes = {
|
||||
count: PropTypes.number.isRequired
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, {PropTypes} from 'react';
|
||||
import styles from './CommentType.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
|
||||
const CommentType = props => {
|
||||
const CommentType = (props) => {
|
||||
const typeData = getTypeData(props.type);
|
||||
|
||||
return (
|
||||
@@ -12,7 +12,7 @@ const CommentType = props => {
|
||||
);
|
||||
};
|
||||
|
||||
const getTypeData = type => {
|
||||
const getTypeData = (type) => {
|
||||
switch (type) {
|
||||
case 'premod':
|
||||
return {icon: 'query_builder', text: 'Pre-Mod', className: 'premod'};
|
||||
|
||||
@@ -55,7 +55,7 @@ class FlagBox extends Component {
|
||||
<ul>
|
||||
{actionSummaries.map((summary, i) => {
|
||||
|
||||
const actionList = actions.filter(a => a.reason === summary.reason);
|
||||
const actionList = actions.filter((a) => a.reason === summary.reason);
|
||||
|
||||
return (
|
||||
<li key={i}>
|
||||
|
||||
@@ -7,13 +7,16 @@ const LoadMore = ({comments, loadMore, sort, tab, assetId, showLoadMore}) =>
|
||||
{
|
||||
showLoadMore && <Button
|
||||
className={styles.loadMore}
|
||||
onClick={() =>
|
||||
loadMore({
|
||||
cursor: comments[comments.length - 1].created_at,
|
||||
onClick={() => {
|
||||
const lastComment = comments[comments.length - 1];
|
||||
const cursor = lastComment ? lastComment.created_at : null;
|
||||
return loadMore({
|
||||
cursor,
|
||||
sort,
|
||||
tab,
|
||||
asset_id: assetId
|
||||
})}>
|
||||
});
|
||||
}}>
|
||||
Load More
|
||||
</Button>
|
||||
}
|
||||
@@ -23,7 +26,7 @@ LoadMore.propTypes = {
|
||||
comments: PropTypes.array.isRequired,
|
||||
loadMore: PropTypes.func.isRequired,
|
||||
sort: PropTypes.oneOf(['CHRONOLOGICAL', 'REVERSE_CHRONOLOGICAL']).isRequired,
|
||||
tab: PropTypes.oneOf(['rejected', 'premod', 'flagged']).isRequired,
|
||||
tab: PropTypes.oneOf(['rejected', 'premod', 'flagged', 'all', 'accepted']).isRequired,
|
||||
assetId: PropTypes.string,
|
||||
showLoadMore: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import {Link} from 'react-router';
|
||||
import {Icon} from 'coral-ui';
|
||||
import styles from './styles.css';
|
||||
|
||||
const ModerationHeader = props => (
|
||||
const ModerationHeader = (props) => (
|
||||
<div className=''>
|
||||
<div className={`mdl-tabs ${styles.header}`}>
|
||||
{
|
||||
|
||||
@@ -4,22 +4,18 @@ import styles from './styles.css';
|
||||
import {SelectField, Option} from 'react-mdl-selectfield';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from 'coral-admin/src/translations.json';
|
||||
import {Icon} from 'coral-ui';
|
||||
import {Link} from 'react-router';
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const ModerationMenu = (
|
||||
{asset, premodCount, rejectedCount, flaggedCount, selectSort, sort}
|
||||
{asset, allCount, acceptedCount, premodCount, rejectedCount, flaggedCount, selectSort, sort}
|
||||
) => {
|
||||
const premodPath = asset
|
||||
? `/admin/moderate/premod/${asset.id}`
|
||||
: '/admin/moderate/premod';
|
||||
const rejectPath = asset
|
||||
? `/admin/moderate/rejected/${asset.id}`
|
||||
: '/admin/moderate/rejected';
|
||||
const flagPath = asset
|
||||
? `/admin/moderate/flagged/${asset.id}`
|
||||
: '/admin/moderate/flagged';
|
||||
|
||||
function getPath (type) {
|
||||
return asset ? `/admin/moderate/${type}/${asset.id}` : `/admin/moderate/${type}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mdl-tabs">
|
||||
@@ -27,29 +23,41 @@ const ModerationMenu = (
|
||||
<div className={styles.tabBarPadding} />
|
||||
<div>
|
||||
<Link
|
||||
to={premodPath}
|
||||
to={getPath('all')}
|
||||
className={`mdl-tabs__tab ${styles.tab}`}
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('modqueue.premod')} <CommentCount count={premodCount} />
|
||||
<Icon name='question_answer' className={styles.tabIcon} /> {lang.t('modqueue.all')} <CommentCount count={allCount} />
|
||||
</Link>
|
||||
<Link
|
||||
to={flagPath}
|
||||
to={getPath('premod')}
|
||||
className={`mdl-tabs__tab ${styles.tab}`}
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('modqueue.flagged')} <CommentCount count={flaggedCount} />
|
||||
<Icon name='access_time' className={styles.tabIcon} /> {lang.t('modqueue.premod')} <CommentCount count={premodCount} />
|
||||
</Link>
|
||||
<Link
|
||||
to={rejectPath}
|
||||
to={getPath('flagged')}
|
||||
className={`mdl-tabs__tab ${styles.tab}`}
|
||||
activeClassName={styles.active}>
|
||||
{lang.t('modqueue.rejected')} <CommentCount count={rejectedCount} />
|
||||
<Icon name='flag' className={styles.tabIcon} /> {lang.t('modqueue.flagged')} <CommentCount count={flaggedCount} />
|
||||
</Link>
|
||||
<Link
|
||||
to={getPath('accepted')}
|
||||
className={`mdl-tabs__tab ${styles.tab}`}
|
||||
activeClassName={styles.active}>
|
||||
<Icon name='check' className={styles.tabIcon} /> {lang.t('modqueue.approved')} <CommentCount count={acceptedCount} />
|
||||
</Link>
|
||||
<Link
|
||||
to={getPath('rejected')}
|
||||
className={`mdl-tabs__tab ${styles.tab}`}
|
||||
activeClassName={styles.active}>
|
||||
<Icon name='close' className={styles.tabIcon} /> {lang.t('modqueue.rejected')} <CommentCount count={rejectedCount} />
|
||||
</Link>
|
||||
</div>
|
||||
<SelectField
|
||||
className={styles.selectField}
|
||||
label="Sort"
|
||||
value={sort}
|
||||
onChange={sort => selectSort(sort)}>
|
||||
onChange={(sort) => selectSort(sort)}>
|
||||
<Option value={'REVERSE_CHRONOLOGICAL'}>Newest First</Option>
|
||||
<Option value={'CHRONOLOGICAL'}>Oldest First</Option>
|
||||
</SelectField>
|
||||
@@ -59,6 +67,7 @@ const ModerationMenu = (
|
||||
};
|
||||
|
||||
ModerationMenu.propTypes = {
|
||||
allCount: PropTypes.number.isRequired,
|
||||
premodCount: PropTypes.number.isRequired,
|
||||
rejectedCount: PropTypes.number.isRequired,
|
||||
flaggedCount: PropTypes.number.isRequired,
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './styles.css';
|
||||
|
||||
const NotFound = props => (
|
||||
const NotFound = (props) => (
|
||||
<div className={`mdl-card mdl-shadow--2dp ${styles.notFound}`}>
|
||||
<p>
|
||||
The provided asset id <Link to={`/admin/moderate/${props.assetId}`}>{props.assetId}</Link> does not exist.
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
.dialog {
|
||||
border: none;
|
||||
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
|
||||
width: 400px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.header {
|
||||
color: black;
|
||||
font-size: 1.5em;
|
||||
font-weight: 500;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.close {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
userSelect: none;
|
||||
outline: none;
|
||||
border: none;
|
||||
touchAction: manipulation;
|
||||
&::-moz-focus-inner: {
|
||||
border: 0;
|
||||
}
|
||||
background: 0;
|
||||
padding: 0;
|
||||
font-size: 24px;
|
||||
line-height: 14px;
|
||||
cursor: pointer;
|
||||
color: #363636;
|
||||
&:hover {
|
||||
color: #6b6b6b;
|
||||
}
|
||||
}
|
||||
|
||||
.legend {
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.radioGroup {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
label.radioGroup {
|
||||
|
||||
&:global(.is-checked) > :global(.mdl-radio__outer-circle),
|
||||
> :global(.mdl-radio__outer-circle) {
|
||||
border-color: #212121;
|
||||
}
|
||||
|
||||
> :global(.mdl-radio__inner-circle) {
|
||||
background: #212121;
|
||||
}
|
||||
|
||||
> :global(.mdl-radio__label) {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.messageInput {
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.perform {
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 6px;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Dialog} from 'coral-ui';
|
||||
import {RadioGroup, Radio} from 'react-mdl';
|
||||
import styles from './SuspendUserDialog.css';
|
||||
|
||||
import Button from 'coral-ui/components/Button';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import {dateAdd} from 'coral-framework/utils';
|
||||
import translations from '../../../translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const initialState = {step: 0, duration: '3'};
|
||||
|
||||
function durationsToDate(hours) {
|
||||
|
||||
// Add 1 minute more to help `timeago.js` to display the correct duration.
|
||||
return dateAdd(new Date(), 'minute', hours * 60 + 1);
|
||||
}
|
||||
|
||||
class SuspendUserDialog extends React.Component {
|
||||
|
||||
state = initialState;
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
if (this.props.open && !next.open) {
|
||||
this.setState(initialState);
|
||||
}
|
||||
}
|
||||
|
||||
handleDurationChange = (event) => {
|
||||
this.setState({duration: event.target.value});
|
||||
}
|
||||
|
||||
handleMessageChange = (event) => {
|
||||
this.setState({message: event.target.value});
|
||||
}
|
||||
|
||||
goToStep1 = () => {
|
||||
this.setState({
|
||||
step: 1,
|
||||
message: lang.t(
|
||||
'suspenduser.email_message_suspend',
|
||||
this.props.username,
|
||||
this.props.organizationName,
|
||||
lang.timeago(durationsToDate(this.state.duration)),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
handlePerform = () => {
|
||||
|
||||
this.props.onPerform({
|
||||
id: this.props.userId,
|
||||
message: this.state.message,
|
||||
|
||||
// Add 1 minute more to help `timeago.js` to display the correct duration.
|
||||
until: durationsToDate(this.state.duration),
|
||||
});
|
||||
};
|
||||
|
||||
renderStep0() {
|
||||
const {onCancel, username} = this.props;
|
||||
const {duration} = this.state;
|
||||
return (
|
||||
<section>
|
||||
<h1 className={styles.header}>
|
||||
{lang.t('suspenduser.title_suspend')}
|
||||
</h1>
|
||||
<p className={styles.description}>
|
||||
{lang.t('suspenduser.description_suspend', username)}
|
||||
</p>
|
||||
<fieldset>
|
||||
<legend className={styles.legend}>{lang.t('suspenduser.select_duration')}</legend>
|
||||
<RadioGroup
|
||||
name='status filter'
|
||||
value={duration}
|
||||
childContainer='div'
|
||||
onChange={this.handleDurationChange}
|
||||
className={styles.radioGroup}
|
||||
>
|
||||
<Radio value='1'>{lang.t('suspenduser.one_hour')}</Radio>
|
||||
<Radio value='3'>{lang.t('suspenduser.hours', 3)}</Radio>
|
||||
<Radio value='24'>{lang.t('suspenduser.hours', 24)}</Radio>
|
||||
<Radio value='168'>{lang.t('suspenduser.days', 7)}</Radio>
|
||||
</RadioGroup>
|
||||
</fieldset>
|
||||
<div className={styles.buttons}>
|
||||
<Button cStyle="white" className={styles.cancel} onClick={onCancel} raised>
|
||||
{lang.t('suspenduser.cancel')}
|
||||
</Button>
|
||||
<Button cStyle="black" className={styles.perform} onClick={this.goToStep1} raised>
|
||||
{lang.t('suspenduser.suspend_user')}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
renderStep1() {
|
||||
const {onCancel, username} = this.props;
|
||||
const {message} = this.state;
|
||||
return (
|
||||
<section>
|
||||
<h1 className={styles.header}>
|
||||
{lang.t('suspenduser.title_notify')}
|
||||
</h1>
|
||||
<p className={styles.description}>
|
||||
{lang.t('suspenduser.description_notify', username)}
|
||||
</p>
|
||||
<fieldset>
|
||||
<legend className={styles.legend}>{lang.t('suspenduser.write_message')}</legend>
|
||||
<textarea
|
||||
rows={5}
|
||||
className={styles.messageInput}
|
||||
value={message}
|
||||
onChange={this.handleMessageChange} />
|
||||
</fieldset>
|
||||
<div className={styles.buttons}>
|
||||
<Button cStyle="white" className={styles.cancel} onClick={onCancel} raised>
|
||||
{lang.t('suspenduser.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
cStyle="black"
|
||||
className={styles.perform}
|
||||
onClick={this.handlePerform}
|
||||
disabled={this.state.message.length === 0}
|
||||
raised
|
||||
>
|
||||
{lang.t('suspenduser.send')}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {open, onCancel} = this.props;
|
||||
const {step} = this.state;
|
||||
return (
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
onCancel={onCancel}
|
||||
open={open}
|
||||
>
|
||||
<div className={styles.close}>
|
||||
<button aria-label="Close" onClick={onCancel} className={styles.closeButton}>×</button>
|
||||
</div>
|
||||
{step === 0 && this.renderStep0()}
|
||||
{step === 1 && this.renderStep1()}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SuspendUserDialog.propTypes = {
|
||||
open: PropTypes.bool.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
onPerform: PropTypes.func.isRequired,
|
||||
username: PropTypes.string,
|
||||
userId: PropTypes.string,
|
||||
organizationName: PropTypes.string,
|
||||
};
|
||||
|
||||
export default SuspendUserDialog;
|
||||
@@ -418,3 +418,40 @@ span {
|
||||
.loadMore:hover {
|
||||
background-color: #4399FF;
|
||||
}
|
||||
|
||||
.tabIcon {
|
||||
position: relative;
|
||||
top: 7px;
|
||||
}
|
||||
|
||||
.username {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 0, 0, .1);
|
||||
}
|
||||
}
|
||||
|
||||
.external {
|
||||
font-size: .7em;
|
||||
text-decoration: none;
|
||||
color: #063b9a;
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
margin-left: 10px;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 12px;
|
||||
top: 2px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ class Stories extends Component {
|
||||
|
||||
onStatusClick = (closeStream, id, statusMenuOpen) => () => {
|
||||
if (statusMenuOpen) {
|
||||
this.setState(prev => {
|
||||
this.setState((prev) => {
|
||||
prev.statusMenus[id] = false;
|
||||
return prev;
|
||||
});
|
||||
@@ -65,7 +65,7 @@ class Stories extends Component {
|
||||
this.props.fetchAssets(page, limit, search, sort, filter);
|
||||
});
|
||||
} else {
|
||||
this.setState(prev => {
|
||||
this.setState((prev) => {
|
||||
prev.statusMenus[id] = true;
|
||||
return prev;
|
||||
});
|
||||
|
||||
@@ -54,7 +54,7 @@ class Stories extends Component {
|
||||
|
||||
onStatusClick = (closeStream, id, statusMenuOpen) => () => {
|
||||
if (statusMenuOpen) {
|
||||
this.setState(prev => {
|
||||
this.setState((prev) => {
|
||||
prev.statusMenus[id] = false;
|
||||
return prev;
|
||||
});
|
||||
@@ -64,7 +64,7 @@ class Stories extends Component {
|
||||
this.props.fetchAssets(page, limit, search, sort, filter);
|
||||
});
|
||||
} else {
|
||||
this.setState(prev => {
|
||||
this.setState((prev) => {
|
||||
prev.statusMenus[id] = true;
|
||||
return prev;
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ fragment metrics on Asset {
|
||||
created_at
|
||||
commentCount
|
||||
action_summaries {
|
||||
type: __typename
|
||||
actionCount
|
||||
actionableItemCount
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ fragment commentView on Comment {
|
||||
asset {
|
||||
id
|
||||
title
|
||||
url
|
||||
}
|
||||
action_summaries {
|
||||
count
|
||||
|
||||
@@ -2,6 +2,7 @@ import {graphql} from 'react-apollo';
|
||||
import SET_USER_STATUS from './setUserStatus.graphql';
|
||||
import SET_COMMENT_STATUS from './setCommentStatus.graphql';
|
||||
import SUSPEND_USER from './suspendUser.graphql';
|
||||
import REJECT_USERNAME from './rejectUsername.graphql';
|
||||
|
||||
export const banUser = graphql(SET_USER_STATUS, {
|
||||
props: ({mutate}) => ({
|
||||
@@ -32,11 +33,22 @@ export const setUserStatus = graphql(SET_USER_STATUS, {
|
||||
|
||||
export const suspendUser = graphql(SUSPEND_USER, {
|
||||
props: ({mutate}) => ({
|
||||
suspendUser: ({userId, message}) => {
|
||||
suspendUser: (input) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
userId,
|
||||
message
|
||||
input,
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
export const rejectUsername = graphql(REJECT_USERNAME, {
|
||||
props: ({mutate}) => ({
|
||||
rejectUsername: (input) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
refetchQueries: ['Users']
|
||||
});
|
||||
@@ -44,6 +56,7 @@ export const suspendUser = graphql(SUSPEND_USER, {
|
||||
})
|
||||
});
|
||||
|
||||
const views = ['all', 'premod', 'flagged', 'accepted', 'rejected'];
|
||||
export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
|
||||
props: ({mutate}) => ({
|
||||
acceptComment: ({commentId}) => {
|
||||
@@ -54,20 +67,37 @@ export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
|
||||
},
|
||||
updateQueries: {
|
||||
ModQueue: (oldData) => {
|
||||
const premod = oldData.premod.filter(c => c.id !== commentId);
|
||||
const flagged = oldData.flagged.filter(c => c.id !== commentId);
|
||||
const rejected = oldData.rejected.filter(c => c.id !== commentId);
|
||||
const comment = views.reduce((comment, view) => {
|
||||
return comment ? comment : oldData[view].find((c) => c.id === commentId);
|
||||
}, null);
|
||||
let accepted;
|
||||
let acceptedCount = oldData.acceptedCount;
|
||||
|
||||
// if the comment was already in the Approved queue, don't re-add it
|
||||
if (comment.status === 'ACCEPTED') {
|
||||
accepted = [...oldData.accepted];
|
||||
} else {
|
||||
comment.status = 'ACCEPTED';
|
||||
acceptedCount++;
|
||||
accepted = [comment, ...oldData.accepted];
|
||||
}
|
||||
|
||||
const premod = oldData.premod.filter((c) => c.id !== commentId);
|
||||
const flagged = oldData.flagged.filter((c) => c.id !== commentId);
|
||||
const rejected = oldData.rejected.filter((c) => c.id !== commentId);
|
||||
const premodCount = premod.length < oldData.premod.length ? oldData.premodCount - 1 : oldData.premodCount;
|
||||
const flaggedCount = flagged.length < oldData.flagged.length ? oldData.flaggedCount - 1 : oldData.flaggedCount;
|
||||
const rejectedCount = rejected.length < oldData.rejected.length ? oldData.rejectedCount - 1 : oldData.rejectedCount;
|
||||
|
||||
return {
|
||||
...oldData,
|
||||
premodCount,
|
||||
flaggedCount,
|
||||
rejectedCount,
|
||||
premodCount: Math.max(0, premodCount),
|
||||
flaggedCount: Math.max(0, flaggedCount),
|
||||
acceptedCount: Math.max(0, acceptedCount),
|
||||
rejectedCount: Math.max(0, rejectedCount),
|
||||
premod,
|
||||
flagged,
|
||||
accepted,
|
||||
rejected,
|
||||
};
|
||||
}
|
||||
@@ -82,21 +112,37 @@ export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
|
||||
},
|
||||
updateQueries: {
|
||||
ModQueue: (oldData) => {
|
||||
const comment = oldData.premod.concat(oldData.flagged).filter(c => c.id === commentId)[0];
|
||||
const rejected = [comment].concat(oldData.rejected);
|
||||
const premod = oldData.premod.filter(c => c.id !== commentId);
|
||||
const flagged = oldData.flagged.filter(c => c.id !== commentId);
|
||||
const comment = views.reduce((comment, view) => {
|
||||
return comment ? comment : oldData[view].find((c) => c.id === commentId);
|
||||
}, null);
|
||||
let rejected;
|
||||
let rejectedCount = oldData.rejectedCount;
|
||||
|
||||
// if the item was already in the Rejected queue, don't put it in again
|
||||
if (comment.status === 'REJECTED') {
|
||||
rejected = oldData.rejected;
|
||||
} else {
|
||||
comment.status = 'REJECTED';
|
||||
rejectedCount++;
|
||||
rejected = [comment, ...oldData.rejected];
|
||||
}
|
||||
|
||||
const premod = oldData.premod.filter((c) => c.id !== commentId);
|
||||
const flagged = oldData.flagged.filter((c) => c.id !== commentId);
|
||||
const accepted = oldData.accepted.filter((c) => c.id !== commentId);
|
||||
const premodCount = premod.length < oldData.premod.length ? oldData.premodCount - 1 : oldData.premodCount;
|
||||
const flaggedCount = flagged.length < oldData.flagged.length ? oldData.flaggedCount - 1 : oldData.flaggedCount;
|
||||
const rejectedCount = oldData.rejectedCount + 1;
|
||||
const acceptedCount = accepted.length < oldData.accepted.length ? oldData.acceptedCount - 1 : oldData.acceptedCount;
|
||||
|
||||
return {
|
||||
...oldData,
|
||||
premodCount,
|
||||
flaggedCount,
|
||||
rejectedCount,
|
||||
premodCount: Math.max(0, premodCount),
|
||||
flaggedCount: Math.max(0, flaggedCount),
|
||||
acceptedCount: Math.max(0, acceptedCount),
|
||||
rejectedCount: Math.max(0, rejectedCount),
|
||||
premod,
|
||||
flagged,
|
||||
accepted,
|
||||
rejected
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
mutation rejectUsername($input: RejectUsernameInput!) {
|
||||
rejectUsername(input: $input) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
mutation suspendUser($userId: ID!, $message: String) {
|
||||
suspendUser(id: $userId, message: $message) {
|
||||
mutation suspendUser($input: SuspendUserInput!) {
|
||||
suspendUser(input: $input) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
query Counts ($asset_id: ID) {
|
||||
allCount: commentCount(query: {
|
||||
asset_id: $asset_id
|
||||
})
|
||||
acceptedCount: commentCount(query: {
|
||||
statuses: [ACCEPTED],
|
||||
asset_id: $asset_id
|
||||
})
|
||||
premodCount: commentCount(query: {
|
||||
statuses: [PREMOD],
|
||||
asset_id: $asset_id
|
||||
})
|
||||
rejectedCount: commentCount(query: {
|
||||
statuses: [REJECTED],
|
||||
asset_id: $asset_id
|
||||
})
|
||||
flaggedCount: commentCount(query: {
|
||||
action_type: FLAG,
|
||||
asset_id: $asset_id,
|
||||
statuses: [NONE, PREMOD]
|
||||
})
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import MOD_QUEUE_QUERY from './modQueueQuery.graphql';
|
||||
import MOD_QUEUE_LOAD_MORE from './loadMore.graphql';
|
||||
import MOD_USER_FLAGGED_QUERY from './modUserFlaggedQuery.graphql';
|
||||
import METRICS from './metricsQuery.graphql';
|
||||
import USER_DETAIL from './userDetail.graphql';
|
||||
import GET_QUEUE_COUNTS from './getQueueCounts.graphql';
|
||||
|
||||
export const modQueueQuery = graphql(MOD_QUEUE_QUERY, {
|
||||
options: ({params: {id = null}}) => {
|
||||
@@ -33,28 +35,34 @@ export const getMetrics = graphql(METRICS, {
|
||||
}
|
||||
});
|
||||
|
||||
export const loadMore = (fetchMore) => ({limit, cursor, sort, tab, asset_id}) => {
|
||||
let statuses;
|
||||
export const loadMore = (fetchMore) => ({limit = 10, cursor, sort, tab, asset_id}) => {
|
||||
let variables = {
|
||||
limit,
|
||||
cursor,
|
||||
sort,
|
||||
asset_id
|
||||
};
|
||||
switch(tab) {
|
||||
case 'all':
|
||||
variables.statuses = null;
|
||||
break;
|
||||
case 'accepted':
|
||||
variables.statuses = ['ACCEPTED'];
|
||||
break;
|
||||
case 'premod':
|
||||
statuses = ['PREMOD'];
|
||||
variables.statuses = ['PREMOD'];
|
||||
break;
|
||||
case 'flagged':
|
||||
statuses = ['NONE', 'PREMOD'];
|
||||
variables.statuses = ['NONE', 'PREMOD'];
|
||||
variables.action_type = 'FLAG';
|
||||
break;
|
||||
case 'rejected':
|
||||
statuses = ['REJECTED'];
|
||||
variables.statuses = ['REJECTED'];
|
||||
break;
|
||||
}
|
||||
return fetchMore({
|
||||
query: MOD_QUEUE_LOAD_MORE,
|
||||
variables: {
|
||||
limit,
|
||||
cursor,
|
||||
sort,
|
||||
statuses,
|
||||
asset_id
|
||||
},
|
||||
variables,
|
||||
updateQuery: (oldData, {fetchMoreResult:{comments}}) => {
|
||||
return {
|
||||
...oldData,
|
||||
@@ -87,3 +95,22 @@ export const modQueueResort = (id, fetchMore) => (sort) => {
|
||||
updateQuery: (oldData, {fetchMoreResult:{data}}) => data
|
||||
});
|
||||
};
|
||||
|
||||
export const getUserDetail = graphql(USER_DETAIL, {
|
||||
options: ({id}) => {
|
||||
return {
|
||||
variables: {author_id: id}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const getQueueCounts = graphql(GET_QUEUE_COUNTS, {
|
||||
options: ({params: {id = null}}) => {
|
||||
return {
|
||||
pollInterval: 5000,
|
||||
variables: {
|
||||
asset_id: id
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#import "../fragments/commentView.graphql"
|
||||
|
||||
query LoadMoreModQueue($limit: Int = 10, $cursor: Date, $sort: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!]) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sort: $sort}) {
|
||||
query LoadMoreModQueue($limit: Int = 10, $cursor: Date, $sort: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sort: $sort, action_type: $action_type}) {
|
||||
...commentView
|
||||
action_summaries {
|
||||
count
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
#import "../fragments/commentView.graphql"
|
||||
|
||||
query ModQueue ($asset_id: ID, $sort: SORT_ORDER) {
|
||||
all: comments(query: {
|
||||
statuses: [NONE, PREMOD, ACCEPTED, REJECTED],
|
||||
asset_id: $asset_id,
|
||||
sort: $sort
|
||||
}) {
|
||||
...commentView
|
||||
}
|
||||
accepted: comments(query: {
|
||||
statuses: [ACCEPTED],
|
||||
asset_id: $asset_id,
|
||||
sort: $sort
|
||||
}) {
|
||||
...commentView
|
||||
}
|
||||
premod: comments(query: {
|
||||
statuses: [PREMOD],
|
||||
asset_id: $asset_id,
|
||||
@@ -28,6 +42,13 @@ query ModQueue ($asset_id: ID, $sort: SORT_ORDER) {
|
||||
title
|
||||
url
|
||||
}
|
||||
allCount: commentCount(query: {
|
||||
asset_id: $asset_id
|
||||
})
|
||||
acceptedCount: commentCount(query: {
|
||||
statuses: [ACCEPTED],
|
||||
asset_id: $asset_id
|
||||
})
|
||||
premodCount: commentCount(query: {
|
||||
statuses: [PREMOD],
|
||||
asset_id: $asset_id
|
||||
@@ -41,4 +62,7 @@ query ModQueue ($asset_id: ID, $sort: SORT_ORDER) {
|
||||
asset_id: $asset_id,
|
||||
statuses: [NONE, PREMOD]
|
||||
})
|
||||
settings {
|
||||
organizationName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
query UserDetail ($author_id: ID!) {
|
||||
user(id: $author_id) {
|
||||
id
|
||||
username
|
||||
created_at
|
||||
profiles {
|
||||
id
|
||||
provider
|
||||
}
|
||||
}
|
||||
totalComments: commentCount(query: {author_id: $author_id})
|
||||
rejectedComments: commentCount(query: {author_id: $author_id, statuses: [REJECTED]})
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import * as actions from '../constants/auth';
|
||||
const initialState = Map({
|
||||
loggedIn: false,
|
||||
user: null,
|
||||
isAdmin: false,
|
||||
loginError: null,
|
||||
loginMaxExceeded: false,
|
||||
passwordRequestSuccess: null
|
||||
@@ -24,12 +23,9 @@ export default function auth (state = initialState, action) {
|
||||
return state
|
||||
.set('loggedIn', true)
|
||||
.set('loadingUser', false)
|
||||
.set('isAdmin', action.isAdmin)
|
||||
.set('user', action.user);
|
||||
case actions.LOGOUT_SUCCESS:
|
||||
case actions.LOGOUT:
|
||||
return initialState;
|
||||
case actions.LOGIN_REQUEST:
|
||||
return state.set('loginError', null);
|
||||
case actions.LOGIN_SUCCESS:
|
||||
return state.set('loginMaxExceeded', false).set('loginError', null);
|
||||
case actions.LOGIN_FAILURE:
|
||||
|
||||
@@ -53,17 +53,17 @@ export default function community (state = initialState, action) {
|
||||
}
|
||||
case SET_ROLE : {
|
||||
const commenters = state.get('accounts');
|
||||
const idx = commenters.findIndex(el => el.id === action.id);
|
||||
const idx = commenters.findIndex((el) => el.id === action.id);
|
||||
|
||||
commenters[idx].roles[0] = action.role;
|
||||
return state.set('accounts', commenters.map(id => id));
|
||||
return state.set('accounts', commenters.map((id) => id));
|
||||
}
|
||||
case SET_COMMENTER_STATUS: {
|
||||
const commenters = state.get('accounts');
|
||||
const idx = commenters.findIndex(el => el.id === action.id);
|
||||
const idx = commenters.findIndex((el) => el.id === action.id);
|
||||
|
||||
commenters[idx].status = action.status;
|
||||
return state.set('accounts', commenters.map(id => id));
|
||||
return state.set('accounts', commenters.map((id) => id));
|
||||
|
||||
}
|
||||
case SORT_UPDATE :
|
||||
|
||||
@@ -1,27 +1,53 @@
|
||||
import {Map} from 'immutable';
|
||||
import {fromJS, Map} from 'immutable';
|
||||
import * as actions from '../constants/moderation';
|
||||
|
||||
const initialState = Map({
|
||||
const initialState = fromJS({
|
||||
singleView: false,
|
||||
modalOpen: false,
|
||||
user: Map({}),
|
||||
user: {},
|
||||
commentId: null,
|
||||
banDialog: false
|
||||
commentStatus: null,
|
||||
userDetailId: null,
|
||||
banDialog: false,
|
||||
shortcutsNoteVisible: window.localStorage.getItem('coral:shortcutsNote') || 'show',
|
||||
suspendUserDialog: {
|
||||
show: false,
|
||||
userId: null,
|
||||
username: '',
|
||||
commentId: null,
|
||||
commentStatus: '',
|
||||
},
|
||||
});
|
||||
|
||||
export default function moderation (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.HIDE_BANUSER_DIALOG:
|
||||
return state
|
||||
.set('banDialog', false);
|
||||
.set('banDialog', false)
|
||||
.set('commentStatus', null);
|
||||
case actions.SHOW_BANUSER_DIALOG:
|
||||
return state
|
||||
.merge({
|
||||
user: Map(action.user),
|
||||
commentId: action.commentId,
|
||||
commentStatus: action.commentStatus,
|
||||
showRejectedNote: action.showRejectedNote,
|
||||
banDialog: true
|
||||
});
|
||||
case actions.SHOW_SUSPEND_USER_DIALOG:
|
||||
return state
|
||||
.mergeDeep({
|
||||
suspendUserDialog: {
|
||||
show: true,
|
||||
userId: action.userId,
|
||||
username: action.username,
|
||||
commentId: action.commentId,
|
||||
commentStatus: action.commentStatus,
|
||||
}
|
||||
});
|
||||
case actions.HIDE_SUSPEND_USER_DIALOG:
|
||||
return state
|
||||
.setIn(['suspendUserDialog', 'show'], false);
|
||||
case actions.SET_ACTIVE_TAB:
|
||||
return state
|
||||
.set('activeTab', action.activeTab);
|
||||
@@ -31,6 +57,13 @@ export default function moderation (state = initialState, action) {
|
||||
case actions.SINGLE_VIEW:
|
||||
return state
|
||||
.set('singleView', !state.get('singleView'));
|
||||
case actions.HIDE_SHORTCUTS_NOTE:
|
||||
return state
|
||||
.set('shortcutsNoteVisible', 'hide');
|
||||
case actions.VIEW_USER_DETAIL:
|
||||
return state.set('userDetailId', action.userId);
|
||||
case actions.HIDE_USER_DETAIL:
|
||||
return state.set('userDetailId', null);
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import ApolloClient, {addTypename} from 'apollo-client';
|
||||
import getNetworkInterface from './transport';
|
||||
import {networkInterface} from 'coral-framework/services/transport';
|
||||
import fragmentMatcher from './fragmentMatcher';
|
||||
|
||||
export const client = new ApolloClient({
|
||||
@@ -12,5 +12,5 @@ export const client = new ApolloClient({
|
||||
}
|
||||
return null;
|
||||
},
|
||||
networkInterface: getNetworkInterface()
|
||||
networkInterface
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user