mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8368eb78d | ||
|
|
e88bac6e0f | ||
|
|
583ab0ff74 | ||
|
|
fb7ad76b8b | ||
|
|
f7d1543aa3 | ||
|
|
950a35310b | ||
|
|
9af7ae0357 | ||
|
|
073eea2847 | ||
|
|
c699e044fd | ||
|
|
4ef7abc132 | ||
|
|
9e486f3a9d | ||
|
|
fbcd390220 | ||
|
|
f0330ed21c | ||
|
|
75a944eba8 | ||
|
|
6850dd1a53 | ||
|
|
98ef60a48e | ||
|
|
8323344d7d | ||
|
|
fbecc363cf | ||
|
|
df925f5a1a | ||
|
|
30073b88f1 | ||
|
|
27ecbd1bdd | ||
|
|
4477c0981f | ||
|
|
2c9eae9264 | ||
|
|
812a4ee704 | ||
|
|
d3caf350ac | ||
|
|
864e3f1a1f | ||
|
|
61d16fbe08 | ||
|
|
000e26e3e1 | ||
|
|
71f0200557 | ||
|
|
d79edcd412 | ||
|
|
485b9d036f | ||
|
|
041ba95a5b | ||
|
|
d0d047f641 | ||
|
|
b057131d34 | ||
|
|
ba572fbf48 | ||
|
|
e3c1f8c231 | ||
|
|
07ddb94327 | ||
|
|
0addb938ae | ||
|
|
c0d01e55e1 | ||
|
|
3ac301142e | ||
|
|
7072aa013b | ||
|
|
091be25f36 | ||
|
|
41a3639d31 | ||
|
|
f96146bfd9 | ||
|
|
2eec87aa80 | ||
|
|
aabd40d53a | ||
|
|
9bce69a7cd | ||
|
|
7afa6c018b | ||
|
|
94b56b547f | ||
|
|
6329e9f618 | ||
|
|
ea7266595d | ||
|
|
4f2d97cd82 | ||
|
|
6a00d3d249 | ||
|
|
d3040804a2 | ||
|
|
24596a48bd | ||
|
|
04ae844a1a | ||
|
|
a8a8455f40 | ||
|
|
d7302e4b3c | ||
|
|
f09ca6d060 | ||
|
|
73f24fd75b | ||
|
|
001beb7a5b | ||
|
|
0b04a011c4 | ||
|
|
00fb2e277b | ||
|
|
a79dd76f4b | ||
|
|
0d91396b1c | ||
|
|
e1324a168b | ||
|
|
af9180f114 | ||
|
|
8f5e54520b | ||
|
|
2061a7b97d | ||
|
|
3b4e69f06e | ||
|
|
f71be09bca | ||
|
|
87ef0f1cb9 | ||
|
|
0640be2a77 | ||
|
|
fdf11fa277 | ||
|
|
8b71175c46 | ||
|
|
6da4439b94 | ||
|
|
7731ec42de | ||
|
|
67a4d63a23 | ||
|
|
ce9ee0a5b0 | ||
|
|
2e8f5f2e69 | ||
|
|
099186d030 | ||
|
|
99daa0b2c9 | ||
|
|
c2b488fa98 | ||
|
|
612d2596f9 | ||
|
|
835d30d362 | ||
|
|
015e3d3ed9 | ||
|
|
4c7c852af7 | ||
|
|
72123cabe7 | ||
|
|
f17b296e2e | ||
|
|
2c72975137 | ||
|
|
5c1314165c | ||
|
|
fd83fed7bb | ||
|
|
dff89ec6cd | ||
|
|
ef200a85cb | ||
|
|
9c0e6ccb36 | ||
|
|
64276cd157 | ||
|
|
214de045fb | ||
|
|
92a9a535c6 | ||
|
|
3db7439d76 | ||
|
|
65be2b89a4 | ||
|
|
c9f6ec327a | ||
|
|
06ddb30d77 | ||
|
|
af3542aec5 | ||
|
|
016eb472eb | ||
|
|
ab772839c5 | ||
|
|
1b9c2404db | ||
|
|
cf96d07a7a | ||
|
|
1c01a3320f | ||
|
|
8d018df0d1 | ||
|
|
b2a2b135a4 | ||
|
|
ccd22ea334 | ||
|
|
83910f7fce | ||
|
|
28873ed961 | ||
|
|
e3febf4a0c | ||
|
|
c3b25d2d0a | ||
|
|
96a375e38a | ||
|
|
417a7a3c36 | ||
|
|
c903d8c0f9 | ||
|
|
24e895460b | ||
|
|
d6a83201ad | ||
|
|
bbdde50858 | ||
|
|
8730ada87c | ||
|
|
506b938536 | ||
|
|
043e722d95 | ||
|
|
fb1dc23fa9 | ||
|
|
e882b51ed0 | ||
|
|
e52a6fd9c1 | ||
|
|
24af7b2812 | ||
|
|
24b5a58584 | ||
|
|
a868cc9c67 | ||
|
|
af83d32de1 | ||
|
|
fa82824d03 | ||
|
|
f0033a7912 | ||
|
|
0924531677 | ||
|
|
44b77597ed | ||
|
|
ec2bf3cb7f | ||
|
|
37f3c11104 | ||
|
|
9359bfc322 | ||
|
|
111ebd67ba | ||
|
|
31f87dc192 | ||
|
|
c9dad30c71 | ||
|
|
7f53375c67 | ||
|
|
df0c016e37 | ||
|
|
bf3fe2ab8e | ||
|
|
9629ba2f83 | ||
|
|
ac7b4d56a1 | ||
|
|
c29bb02b1c | ||
|
|
8c72e32a25 | ||
|
|
4a31bb7ba2 | ||
|
|
3be1c94f8f | ||
|
|
7fba0428bb | ||
|
|
c7d433deb5 | ||
|
|
a665dbd88e | ||
|
|
c990a812a4 | ||
|
|
3f26a8d7ad | ||
|
|
4a3e5942b0 | ||
|
|
5360bdc8ec | ||
|
|
548a204665 | ||
|
|
a8e8b78441 | ||
|
|
258788fd88 | ||
|
|
8cc3d19819 | ||
|
|
0de5a328ab | ||
|
|
4830aec138 | ||
|
|
4269fd1eb9 | ||
|
|
12e61bbc66 | ||
|
|
511e71f63a | ||
|
|
e675d716cb | ||
|
|
2d6143af62 | ||
|
|
873d1915c3 | ||
|
|
67f3f8cdef | ||
|
|
8f0eef7cb5 | ||
|
|
4c7c3c9e42 | ||
|
|
8f9724ed90 | ||
|
|
9fb7aa2657 | ||
|
|
d714586279 | ||
|
|
3e4c9dcb48 | ||
|
|
285ec98d54 | ||
|
|
853b0ca6dd | ||
|
|
c58d9068e3 | ||
|
|
78e0fb6cc6 | ||
|
|
dfbb9ad96f | ||
|
|
2d43eef508 | ||
|
|
fa22e45a4d | ||
|
|
abdf5335da | ||
|
|
c24454e234 | ||
|
|
97e2e61a4b | ||
|
|
57ced3c91f | ||
|
|
f7c46b1a42 | ||
|
|
c664438ade | ||
|
|
a5b5043a0d | ||
|
|
b5b6b113d4 | ||
|
|
a13d384cee | ||
|
|
3a0a854317 | ||
|
|
73c3d31fbc | ||
|
|
03a0d904d2 | ||
|
|
315d3babde | ||
|
|
4d6055b9b8 | ||
|
|
d3d1148ee6 | ||
|
|
ecd9219e17 | ||
|
|
ce4ad426a7 | ||
|
|
77c8d966ab | ||
|
|
cabadf467f | ||
|
|
0807acc98f | ||
|
|
3c5b56c599 | ||
|
|
899bba7327 | ||
|
|
41dd3eb180 | ||
|
|
e02537df13 | ||
|
|
81d90cb138 | ||
|
|
e4a359e0b4 | ||
|
|
2882dfbadb | ||
|
|
f2b9db9dbc | ||
|
|
c563a13e7c | ||
|
|
5976595871 | ||
|
|
8a4a7d3589 | ||
|
|
5073e60df5 | ||
|
|
5a71db2568 | ||
|
|
ac0acea22b | ||
|
|
6198c9e730 | ||
|
|
9ac06281e9 | ||
|
|
78e5cb7aa6 | ||
|
|
882bf57bbc | ||
|
|
789fb3dfce | ||
|
|
819b1c597c | ||
|
|
38148ad51c | ||
|
|
8f6ff46244 | ||
|
|
4727c52de4 | ||
|
|
f461d27810 | ||
|
|
fbc5a9d74b | ||
|
|
6ff4994d0d | ||
|
|
c793a840ea | ||
|
|
ebda51dc0f | ||
|
|
b0573749ee | ||
|
|
933903de2f | ||
|
|
4373c382e1 | ||
|
|
af6787bc0f | ||
|
|
dd1fbf30ca | ||
|
|
a5aa59e255 | ||
|
|
19b8d0a6ea | ||
|
|
bcf8370359 | ||
|
|
0da0b1a37e | ||
|
|
31ee8139b6 | ||
|
|
51977dcf54 | ||
|
|
0b18bb06e7 | ||
|
|
9abb3d30da | ||
|
|
f0839c57df | ||
|
|
b2929e9a83 | ||
|
|
ca7b790962 | ||
|
|
42a477ae41 | ||
|
|
53461df287 | ||
|
|
9b18a78ce0 | ||
|
|
da35f39baf | ||
|
|
e885178dfc | ||
|
|
24e8aab0fe | ||
|
|
44d6ad340d | ||
|
|
63b6315435 | ||
|
|
95ca43ff8c | ||
|
|
55e4aed511 | ||
|
|
6d5adc8f54 | ||
|
|
247e05383e | ||
|
|
3988fb9e93 | ||
|
|
7d524efaad | ||
|
|
d7a88cdc2a | ||
|
|
18f5b0cc5e | ||
|
|
2df18f07cb | ||
|
|
786e85686d | ||
|
|
9a651880a2 | ||
|
|
82e01eac00 | ||
|
|
5efdc9865f | ||
|
|
62d01cf2a0 | ||
|
|
81bf63836a | ||
|
|
54ac2985ba | ||
|
|
e9bd5703f1 | ||
|
|
a394ba29b3 | ||
|
|
a2470fa01b | ||
|
|
9b98e3cac9 | ||
|
|
4f89c1e1c3 | ||
|
|
c3e551deb5 | ||
|
|
0678ea0344 | ||
|
|
b09b56bd24 | ||
|
|
2a0f70f084 | ||
|
|
bf2e11ef9a | ||
|
|
0fc810a7b1 | ||
|
|
918ad915b8 | ||
|
|
d51b6b31f6 | ||
|
|
e88445beac | ||
|
|
33be7531a0 | ||
|
|
2131638f48 | ||
|
|
39a980b22f | ||
|
|
ce0b63fe75 | ||
|
|
c0cb641df3 | ||
|
|
cee68ae471 | ||
|
|
33cd783b37 | ||
|
|
82ef16a4ee | ||
|
|
736ab6066f | ||
|
|
3327604a58 | ||
|
|
0c3cfd21a9 | ||
|
|
903e70d641 | ||
|
|
21f6caa942 | ||
|
|
f70ccf641a | ||
|
|
dd4e3d7ec8 | ||
|
|
886a468993 | ||
|
|
de0dad6243 | ||
|
|
7226c475e3 | ||
|
|
173c9262d1 | ||
|
|
5e7ee344ec | ||
|
|
b2d0c58a72 | ||
|
|
b34b033a18 | ||
|
|
4195d8b419 | ||
|
|
bfb2da519f | ||
|
|
8821c0aa5c | ||
|
|
a56031ac47 | ||
|
|
a3eeec34df | ||
|
|
c4ad667788 | ||
|
|
d20666adb9 | ||
|
|
96b44b2b99 | ||
|
|
b92b4027b0 | ||
|
|
f1423abdea | ||
|
|
c31e6347d0 | ||
|
|
48179b413c | ||
|
|
c955fcd29e | ||
|
|
457322e3d0 | ||
|
|
9ba8cbcbe1 | ||
|
|
93363a0650 | ||
|
|
7f442ce91e | ||
|
|
42f0d76d98 | ||
|
|
1578316363 | ||
|
|
5c033dbfdf | ||
|
|
3cdf2a3118 | ||
|
|
4e5e989d22 | ||
|
|
62660ed144 | ||
|
|
2d44f7abf4 | ||
|
|
ea98f0275d | ||
|
|
07e6c15c3a | ||
|
|
05e91129f4 | ||
|
|
350e679abf | ||
|
|
ae07dfb2cd | ||
|
|
88d012755c | ||
|
|
0cdefa0066 | ||
|
|
e4688f9876 | ||
|
|
e091fa552a | ||
|
|
53b1104cf8 | ||
|
|
975fe550e0 | ||
|
|
8adff14263 | ||
|
|
5f07d0256c | ||
|
|
f573504a3c | ||
|
|
8c0118106b | ||
|
|
4ce720e510 | ||
|
|
81c669d63f | ||
|
|
97a8f95b93 | ||
|
|
7bf114c01a | ||
|
|
6460cbd536 | ||
|
|
05b326b91c | ||
|
|
9af206da1a | ||
|
|
0c3a9db443 | ||
|
|
511f4f0975 | ||
|
|
95545a0e7d | ||
|
|
12a71fa117 | ||
|
|
302d30cde1 | ||
|
|
3da4a70442 | ||
|
|
6d7afc7bbc | ||
|
|
b6786f614e | ||
|
|
a6a903c459 | ||
|
|
34b58779e8 | ||
|
|
82a332c787 | ||
|
|
bfe82218ed | ||
|
|
5f5c64870a | ||
|
|
ad4a5595f9 | ||
|
|
b7137fbcd4 | ||
|
|
b84daf0422 | ||
|
|
4089d09681 | ||
|
|
a7eb588f7f | ||
|
|
cd92dbf682 | ||
|
|
89ab71eb1d | ||
|
|
c1cfe3b200 | ||
|
|
d7ed679384 | ||
|
|
7446712109 | ||
|
|
6647e22d1c | ||
|
|
59acdc5c02 | ||
|
|
b06ae9001e | ||
|
|
1e718532f3 | ||
|
|
6d797b44c8 | ||
|
|
dbd7f26311 | ||
|
|
11f774ecab | ||
|
|
d472223ee3 | ||
|
|
b4daf8fc98 | ||
|
|
e41c224e3d | ||
|
|
20b5525c48 | ||
|
|
23da613f2b | ||
|
|
df079d29bd | ||
|
|
e782df815b | ||
|
|
9bfc17d332 | ||
|
|
3ff47dc610 | ||
|
|
ed929fa2c6 | ||
|
|
369e974a0c | ||
|
|
2a90956a1d | ||
|
|
833922f0aa | ||
|
|
f6f9d380f1 | ||
|
|
823bf18335 | ||
|
|
16bf887161 | ||
|
|
27eb159a75 | ||
|
|
04540c658e | ||
|
|
327c87cda6 | ||
|
|
b43b84acff | ||
|
|
1dec169520 | ||
|
|
edce550587 | ||
|
|
934b630ce2 | ||
|
|
812483931d | ||
|
|
01c5bc4e26 | ||
|
|
dd7ca9fd0c | ||
|
|
7ef9e2783b | ||
|
|
be994d033b | ||
|
|
cd0ce14254 | ||
|
|
a0929fd773 | ||
|
|
adc06911f3 | ||
|
|
ca1ce9ba76 | ||
|
|
f3bf9d28a7 | ||
|
|
61108337cc | ||
|
|
2aed5d1224 | ||
|
|
3777b4b1ce | ||
|
|
55b2208a49 | ||
|
|
5037af9100 | ||
|
|
fed6654f52 | ||
|
|
7ba42f059c | ||
|
|
cf2746fc28 | ||
|
|
b7b9da9509 | ||
|
|
24ec898a15 | ||
|
|
46359b4a1b | ||
|
|
1f170c1dca | ||
|
|
b189c9d2b0 | ||
|
|
85890b4e6e | ||
|
|
6d3cdb176e | ||
|
|
38abf63cc9 | ||
|
|
34334b8152 | ||
|
|
0b973f24ff | ||
|
|
abff1c763a | ||
|
|
9f4a913655 | ||
|
|
d363c977dc | ||
|
|
f004573ed2 | ||
|
|
fb9419f2ad | ||
|
|
9217239caf | ||
|
|
9fc0fc37db | ||
|
|
c442b8bb70 | ||
|
|
e1f897ec17 | ||
|
|
1803c740fc | ||
|
|
fac028c22b | ||
|
|
d75ae3aca8 | ||
|
|
a3216dd33e | ||
|
|
6b7ceb99c9 | ||
|
|
4ae9928224 | ||
|
|
dae8dc0509 | ||
|
|
017d560421 | ||
|
|
3f59ed1e46 | ||
|
|
9c38bab83b | ||
|
|
a9cb206415 | ||
|
|
c1b954965f | ||
|
|
e0713a5ec4 | ||
|
|
0ee56fe8d3 | ||
|
|
88390f13c7 | ||
|
|
7e709ec41d | ||
|
|
1bfecdd77c | ||
|
|
487e1e34db | ||
|
|
c054992741 | ||
|
|
12b83d3904 | ||
|
|
7ebba3018f | ||
|
|
0e089b8ff8 | ||
|
|
5695599f74 | ||
|
|
bef3e3e33c | ||
|
|
8dbd1789e0 | ||
|
|
ee548d7180 | ||
|
|
8ce9f2d4bb | ||
|
|
76f9e1487c | ||
|
|
e729da8e01 | ||
|
|
011d8830fb | ||
|
|
5713e2e2f6 | ||
|
|
f23a5132d6 | ||
|
|
101b6cbe59 | ||
|
|
7494752019 | ||
|
|
11999d751a | ||
|
|
2a85b9fdd2 | ||
|
|
fdddd5f8af | ||
|
|
6086bfd3cf | ||
|
|
2a8dc3b506 | ||
|
|
1a31d9eb97 | ||
|
|
bceebca39f | ||
|
|
1df48e27f1 | ||
|
|
5da8475806 | ||
|
|
1dea6267d7 | ||
|
|
8d5d73312d | ||
|
|
3a18cd88ad | ||
|
|
a1ee09ad9b | ||
|
|
ae09d5c72e | ||
|
|
2965877346 | ||
|
|
0b9c058ad8 | ||
|
|
5657fde7dc | ||
|
|
a69dd02e52 | ||
|
|
71cf565018 | ||
|
|
b4865546d4 | ||
|
|
b97b5e97f2 | ||
|
|
0265b9bc56 | ||
|
|
f06e171def | ||
|
|
9930bf5e7a | ||
|
|
2227ec423f | ||
|
|
8fafa93cf5 | ||
|
|
2991fe4ca5 | ||
|
|
1126ab48ad | ||
|
|
91f6c5a088 | ||
|
|
c8325c91a4 | ||
|
|
039c1b82d1 | ||
|
|
4fbb0678e0 | ||
|
|
fb82ef7faf | ||
|
|
b362da56ab | ||
|
|
5f9f712a9b | ||
|
|
94d253fdbc | ||
|
|
86e5e5e955 | ||
|
|
c3b2eb5a98 | ||
|
|
627ae50fda | ||
|
|
367c2df3da | ||
|
|
14a8fcd475 | ||
|
|
82d0d427ec | ||
|
|
4b068b22b7 | ||
|
|
2c757e9d57 | ||
|
|
8633a0f457 | ||
|
|
a7b33fdc3e | ||
|
|
ce131dd067 | ||
|
|
afb591d990 | ||
|
|
fac0f53ea8 | ||
|
|
89feea70fd | ||
|
|
77dae9922c | ||
|
|
427c6e10a4 | ||
|
|
30fe43624f | ||
|
|
2723be2eb9 | ||
|
|
1542888080 | ||
|
|
3a58088420 | ||
|
|
db91e2a27a | ||
|
|
39a2092310 | ||
|
|
a586756ac9 | ||
|
|
5197cab2b4 | ||
|
|
8cf04c5c66 | ||
|
|
3a8ed3033a | ||
|
|
7ffc7a76f5 | ||
|
|
988cb870bb | ||
|
|
343f009150 | ||
|
|
a167b6ce2c | ||
|
|
94f0205d9c | ||
|
|
f38e718373 | ||
|
|
c8f432268b | ||
|
|
759338dc02 | ||
|
|
aa0c41c342 | ||
|
|
840f3f7145 | ||
|
|
ca2c2469bf | ||
|
|
606871e4b7 | ||
|
|
1a8f954707 | ||
|
|
20cd38e47b | ||
|
|
70d92978c0 | ||
|
|
b750d9e9a9 | ||
|
|
887dd3825c | ||
|
|
adae679bc7 | ||
|
|
da8dee175d | ||
|
|
316971302b | ||
|
|
bdbffba902 | ||
|
|
703fabf586 | ||
|
|
2ec1d3fa51 | ||
|
|
93f2c95700 | ||
|
|
fce1d60d09 | ||
|
|
f9967dbc48 | ||
|
|
4fdb2d5af6 | ||
|
|
00512bfaf0 | ||
|
|
37148f69d5 | ||
|
|
08d6eadbb7 | ||
|
|
5dd08d84e5 | ||
|
|
10cba651e6 | ||
|
|
06d0da9286 | ||
|
|
1660d6c1d1 | ||
|
|
9d36426a1b | ||
|
|
b721b4e10a | ||
|
|
8e72381081 | ||
|
|
0dfff2ce53 | ||
|
|
54971f37ad | ||
|
|
cac6eecde2 | ||
|
|
3efd86a4cb | ||
|
|
299b083816 | ||
|
|
7d2a0cf7cc | ||
|
|
7c02f21210 | ||
|
|
a7ad7a2267 | ||
|
|
f0ac56221c | ||
|
|
cd45bf6042 | ||
|
|
895eb5a69a | ||
|
|
f6be48a4a8 | ||
|
|
2dcab1fb12 | ||
|
|
d8bce3f4ce | ||
|
|
8f3aa789f5 | ||
|
|
be6b1466c6 | ||
|
|
233bc68128 | ||
|
|
7fd09c3534 | ||
|
|
8102ef7157 | ||
|
|
1edf320779 | ||
|
|
d8a443e790 | ||
|
|
2fd7829b88 | ||
|
|
27c6d830d9 | ||
|
|
910acdd52b | ||
|
|
675e8fa604 | ||
|
|
5e74a044f3 | ||
|
|
9d5739b396 | ||
|
|
e0e481373b | ||
|
|
8e466befb4 | ||
|
|
c98cb02a4a | ||
|
|
351ebc9105 | ||
|
|
27ef2ad0d4 | ||
|
|
798acaf320 | ||
|
|
21122016da | ||
|
|
c9ae622ec3 | ||
|
|
b5a0483603 | ||
|
|
987acb64f5 | ||
|
|
43d92e5b76 | ||
|
|
618b8b5135 | ||
|
|
24d73b2770 | ||
|
|
36be112744 | ||
|
|
caac76bb3f | ||
|
|
3035ba0bcf | ||
|
|
562586c130 | ||
|
|
a7064bbfdb | ||
|
|
1b30327fe0 | ||
|
|
e1f7d1da99 | ||
|
|
bbfb4c8ae0 | ||
|
|
dbcd0aacb1 | ||
|
|
b0f6ab2b48 | ||
|
|
612adb2990 | ||
|
|
c4e3904a95 | ||
|
|
0d9d2d4871 | ||
|
|
69acb6d5a0 | ||
|
|
58aff2487a | ||
|
|
4f72f5d4ae | ||
|
|
27c44e942e | ||
|
|
22bbe5c614 | ||
|
|
bccce1d9bd | ||
|
|
a914cf1591 | ||
|
|
ba7ee5d327 | ||
|
|
f035a9d1e9 | ||
|
|
1c6f3b80c4 | ||
|
|
1b00cdbea3 | ||
|
|
1e41ae9402 | ||
|
|
86f48cea96 | ||
|
|
94f4c57038 | ||
|
|
97eee92499 | ||
|
|
5dfc5d87ad | ||
|
|
e4399b734e | ||
|
|
09ecd2a055 | ||
|
|
1fffa1427d | ||
|
|
cde3c4e17e | ||
|
|
ac28106251 | ||
|
|
3abbe3119d | ||
|
|
1dd1a0d986 | ||
|
|
a7d87f4ee1 | ||
|
|
025d0cf812 | ||
|
|
8ac93c9785 | ||
|
|
1ec71ebd00 | ||
|
|
48d4eeddf3 | ||
|
|
0615be9ab4 | ||
|
|
e0dc41b721 | ||
|
|
b46ad5448c | ||
|
|
8e9e61a610 | ||
|
|
341e8d9af6 | ||
|
|
674d7837f6 | ||
|
|
b827abffbb | ||
|
|
a48bfbaa69 | ||
|
|
dcfdec4df1 | ||
|
|
0fde69608a | ||
|
|
0225d38560 | ||
|
|
34178919dc | ||
|
|
7b76450eb5 | ||
|
|
3adc3e87cd | ||
|
|
b25fa9c7f3 | ||
|
|
f712e426e2 | ||
|
|
a9882c8904 | ||
|
|
a0f9242f02 | ||
|
|
fa6c0afe93 | ||
|
|
1d2f7b5298 | ||
|
|
5f9d47a199 | ||
|
|
29d7494385 | ||
|
|
6145fd2855 | ||
|
|
14d2b63f2e | ||
|
|
cf59bb213c | ||
|
|
cecf2f4606 | ||
|
|
bd7dfdbe55 | ||
|
|
98ee3c7b55 | ||
|
|
0fc0e9c467 | ||
|
|
398dedf75b | ||
|
|
d11ac54e1d | ||
|
|
764d562497 | ||
|
|
4548134e3f | ||
|
|
4efcd624c3 | ||
|
|
48af316380 | ||
|
|
98f46409ca | ||
|
|
dee5651709 | ||
|
|
c5cee13bd7 | ||
|
|
e40a30d200 | ||
|
|
0d66e42313 | ||
|
|
154949ff47 | ||
|
|
33e25403e7 | ||
|
|
c8dc1f84e6 | ||
|
|
7fcc7753f8 | ||
|
|
569260a4b9 | ||
|
|
2028e56f48 | ||
|
|
1c535bda89 | ||
|
|
1f9ff3f36d | ||
|
|
18b18a7aef | ||
|
|
f2ea269f7a | ||
|
|
bbad09d271 | ||
|
|
932cec27de | ||
|
|
2063c4a90e | ||
|
|
4d0d0735e8 | ||
|
|
bd73140f23 | ||
|
|
b77c330823 | ||
|
|
9f02165aa6 | ||
|
|
522857f0d6 | ||
|
|
f1e81575cb | ||
|
|
69ace56172 | ||
|
|
0a991c938a | ||
|
|
6a2b6bb845 | ||
|
|
78857e8de0 | ||
|
|
3c7e0462f5 | ||
|
|
c0a54b6538 | ||
|
|
15f07b1360 | ||
|
|
61604a7d07 | ||
|
|
dfcb9e24df | ||
|
|
7b38bac525 | ||
|
|
16847f9fec | ||
|
|
71af62585a | ||
|
|
29c7c9e810 | ||
|
|
c81d9c0bd1 | ||
|
|
0519b1d1ff | ||
|
|
046a17801e | ||
|
|
21342b8b81 | ||
|
|
ec436bcb39 | ||
|
|
59eff3e29e | ||
|
|
574cbf8e79 | ||
|
|
b3fc920919 | ||
|
|
1d089ee151 | ||
|
|
0434ecf755 | ||
|
|
48c291a9c3 | ||
|
|
c0ff9c1beb | ||
|
|
3d3e0491a4 | ||
|
|
c52e42c644 | ||
|
|
77d30bdbc1 | ||
|
|
efdf7a6b25 | ||
|
|
7eaad1541b | ||
|
|
0c7b35ffde | ||
|
|
e4ff48ea88 | ||
|
|
cbd7337dbc | ||
|
|
c0e9836a57 | ||
|
|
c7bf3c1a3c | ||
|
|
bb78ba9b0b | ||
|
|
62834ae412 | ||
|
|
0c62ff0f9f | ||
|
|
380f20d525 | ||
|
|
252ec8f6ac | ||
|
|
e95e0381c2 | ||
|
|
8ab31d7009 | ||
|
|
57f8a64cf4 | ||
|
|
0a190a9838 | ||
|
|
6ff2e38003 | ||
|
|
3a28eae384 | ||
|
|
5fbabcaa1b | ||
|
|
387316849a | ||
|
|
26c1d22ec3 | ||
|
|
eae5b85d1a | ||
|
|
dbe6e675c9 | ||
|
|
2c8366d6b7 | ||
|
|
ae3d1c1819 | ||
|
|
916596fe53 | ||
|
|
0f6bbb21a3 | ||
|
|
c2cbf91cad | ||
|
|
fe689c5888 | ||
|
|
e1f5fe1f77 | ||
|
|
848c04539c | ||
|
|
e4b225b4c2 | ||
|
|
787f11118c | ||
|
|
cca0a6df71 | ||
|
|
5073ef6c1b | ||
|
|
596cd57274 | ||
|
|
b02ccd792c | ||
|
|
ad5a6c539a | ||
|
|
a4591ecb55 | ||
|
|
6c95502b4e | ||
|
|
931b5ad3ac | ||
|
|
c0bb5e5cb2 | ||
|
|
1b33b62af0 | ||
|
|
bf0901e77f | ||
|
|
f164ad8013 | ||
|
|
f1bad9e8f2 | ||
|
|
ba51645c87 | ||
|
|
bc0a188c57 | ||
|
|
f497fdb277 | ||
|
|
a677c698bf | ||
|
|
73b4cfd2c4 | ||
|
|
5f758d3437 | ||
|
|
e8808f9232 | ||
|
|
6713397d02 | ||
|
|
9c00050f6a | ||
|
|
6259c1869d | ||
|
|
2e7a59a6e1 | ||
|
|
1ad942f06c | ||
|
|
6ffab51bcf | ||
|
|
71de5a33a9 | ||
|
|
a1978571e8 | ||
|
|
1510ade47a | ||
|
|
f330c385e5 | ||
|
|
886df1cdca | ||
|
|
91a25fc3c1 | ||
|
|
474a3a12ef | ||
|
|
9b9e9c0292 | ||
|
|
34292f467d | ||
|
|
c72dfdddf3 | ||
|
|
ec74262089 | ||
|
|
b3486ce614 | ||
|
|
85f9000a64 | ||
|
|
95580f5ffe | ||
|
|
57b7829189 | ||
|
|
551ac9dd90 | ||
|
|
2568f65fe5 | ||
|
|
8d6f79ab79 | ||
|
|
841ab53ea7 | ||
|
|
5147ca6b86 | ||
|
|
197d94ffb1 | ||
|
|
d23d3be4ef | ||
|
|
ff9ae03522 | ||
|
|
a50c16c00f | ||
|
|
d75257a832 | ||
|
|
074fbca231 | ||
|
|
8a02618d8a | ||
|
|
b5ab829f9b | ||
|
|
4bfc20b782 | ||
|
|
c6526a71ba | ||
|
|
3060f5097f | ||
|
|
b11783d54f | ||
|
|
bd01d5f3fe | ||
|
|
53baa02ada | ||
|
|
d2b6e0b235 | ||
|
|
52595bb3f8 | ||
|
|
62f1cd43cf | ||
|
|
8b39613148 | ||
|
|
1d986e7a0b | ||
|
|
79d312df50 | ||
|
|
faf75c5c2f | ||
|
|
33920afc74 | ||
|
|
bd83e9e118 | ||
|
|
2be8750d0e | ||
|
|
14ce9b1e77 | ||
|
|
51e40ce185 | ||
|
|
511cdd6197 | ||
|
|
c1f7ef4f32 | ||
|
|
5a4bf7a223 | ||
|
|
11d2f05f73 | ||
|
|
d07c7de292 | ||
|
|
942b65648b | ||
|
|
b1753ecdce | ||
|
|
7ea05e699e | ||
|
|
eb969f8050 | ||
|
|
dde1f7b5be | ||
|
|
343d4418b0 | ||
|
|
4c28e43e62 | ||
|
|
1030ca1fa2 | ||
|
|
b1bc97c8d5 | ||
|
|
18b802c555 | ||
|
|
1c16a75f6b | ||
|
|
8724cfe325 | ||
|
|
d3d7f909d8 | ||
|
|
c2f22bbf98 | ||
|
|
9b39017f25 | ||
|
|
075d62a32a | ||
|
|
2d92284622 | ||
|
|
e99b79687f | ||
|
|
9182d5b09c | ||
|
|
89b87b01df | ||
|
|
257f361998 | ||
|
|
02dff0c726 | ||
|
|
17f5800363 | ||
|
|
7aaad6ccd9 | ||
|
|
43cb0efe7e | ||
|
|
06ced883ba | ||
|
|
f7e6f67f4e | ||
|
|
307fe58cf3 | ||
|
|
48f2cce893 | ||
|
|
b597f730f6 | ||
|
|
d396f43fe3 | ||
|
|
799a21fecd | ||
|
|
7d41421526 | ||
|
|
9bd55b267e | ||
|
|
90470b8331 | ||
|
|
df737ad201 | ||
|
|
09ee854302 | ||
|
|
f88bd8b485 | ||
|
|
36ba7367d1 | ||
|
|
f9693c7154 | ||
|
|
445107ef64 | ||
|
|
1c3be913d3 | ||
|
|
09f9782505 | ||
|
|
1d6e9527e5 | ||
|
|
d0ce832319 | ||
|
|
2e7ee6bb8b | ||
|
|
ad3958a2e0 | ||
|
|
58acdef043 | ||
|
|
99aad0f74b | ||
|
|
5d718f6dd4 | ||
|
|
44ce71ef14 | ||
|
|
47cde9a842 | ||
|
|
219399a231 | ||
|
|
95114bc847 | ||
|
|
12047d1ae1 | ||
|
|
0362dad14b | ||
|
|
97b81cc1cc | ||
|
|
8d6b9dd23c | ||
|
|
9adf11e1d1 | ||
|
|
ef49d9a3d2 | ||
|
|
921eea59c8 | ||
|
|
c5cd88f4c0 | ||
|
|
762f4f7898 | ||
|
|
e400ae37f4 | ||
|
|
45b0ec48de | ||
|
|
f3f8c6b24e | ||
|
|
baa5bddd19 | ||
|
|
bea7d75db7 | ||
|
|
8e5edd65c7 | ||
|
|
9ae59ef998 | ||
|
|
0f382bd742 | ||
|
|
55b1f4831a | ||
|
|
3d2575413d | ||
|
|
c40393347a | ||
|
|
8c05e3df87 | ||
|
|
fa4adda61e | ||
|
|
ea06b334f6 | ||
|
|
203d3c7ee9 | ||
|
|
825e1b5e5c | ||
|
|
3db406bf26 | ||
|
|
c5e078f1e1 | ||
|
|
e6f18d8558 | ||
|
|
76f259a2aa | ||
|
|
3f90f72bee | ||
|
|
f211bdf34e | ||
|
|
d2e5633d91 | ||
|
|
b01e919029 | ||
|
|
62b407179f | ||
|
|
536ef70415 | ||
|
|
981c319810 | ||
|
|
19e23479ee | ||
|
|
86687f10d8 | ||
|
|
3b6bcba055 | ||
|
|
acdfaeee9f | ||
|
|
792d0b9a2d | ||
|
|
9be2379f30 | ||
|
|
1658b2594b | ||
|
|
d9e2fba569 | ||
|
|
d9cabeafe2 | ||
|
|
171a6d506b | ||
|
|
a19de7bfb4 | ||
|
|
6138768588 | ||
|
|
4c5f8b3359 | ||
|
|
605c87a799 | ||
|
|
1e0451ceba | ||
|
|
d60b869937 | ||
|
|
9f4c97e138 | ||
|
|
66518f50dd | ||
|
|
7328926ad7 | ||
|
|
a736c1620e | ||
|
|
d359d915fe | ||
|
|
235b2e1068 | ||
|
|
f183aeb748 | ||
|
|
143adc7c6f | ||
|
|
91714705ea | ||
|
|
64cc85eb24 | ||
|
|
7a043dac5b | ||
|
|
edd3e7f6fe | ||
|
|
3ef9bd5e52 | ||
|
|
b60196e455 | ||
|
|
d315458e0e | ||
|
|
abdb4cbe3e | ||
|
|
20380cd62f | ||
|
|
271f0cc237 | ||
|
|
864aa2be28 | ||
|
|
4c8894d6e9 | ||
|
|
dd5d06aa95 | ||
|
|
7e536cfb31 | ||
|
|
5fa4b4d734 | ||
|
|
e7c78a2465 | ||
|
|
95fadbd05a | ||
|
|
6d04b4add3 |
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"presets": [
|
||||
"es2015"
|
||||
["es2015", {modules: false}]
|
||||
],
|
||||
"plugins": [
|
||||
"add-module-exports",
|
||||
"transform-class-properties",
|
||||
"transform-decorators-legacy",
|
||||
"transform-object-assign",
|
||||
@@ -11,5 +10,12 @@
|
||||
"transform-async-to-generator",
|
||||
"transform-react-jsx",
|
||||
"syntax-dynamic-import"
|
||||
]
|
||||
}
|
||||
],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": [
|
||||
["transform-es2015-modules-commonjs", "dynamic-import-node"]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,5 +26,7 @@ plugins/*
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-deep-reply-count
|
||||
!plugins/talk-plugin-subscriber
|
||||
!plugins/talk-plugin-flag-details
|
||||
|
||||
node_modules
|
||||
|
||||
+1
-73
@@ -1,75 +1,3 @@
|
||||
{
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2017
|
||||
},
|
||||
"plugins": [
|
||||
"json",
|
||||
"promise",
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"promise/no-return-wrap": "error",
|
||||
"promise/param-names": "error",
|
||||
"promise/catch-or-return": "warn",
|
||||
"promise/no-native": "off",
|
||||
"promise/no-nesting": "warn",
|
||||
"promise/no-promise-in-callback": "warn",
|
||||
"promise/no-callback-in-promise": "warn",
|
||||
|
||||
"react/display-name": "off",
|
||||
|
||||
// We'll keeping using this, as we'll switch over to Preact and we don't
|
||||
// need to worry about deprecation.
|
||||
"react/no-find-dom-node": "off",
|
||||
|
||||
// TODO: Use default after fixing the errors.
|
||||
"react/prop-types": "warn",
|
||||
|
||||
"array-callback-return": "warn",
|
||||
"arrow-parens": ["warn", "always"],
|
||||
"comma-spacing": ["error", {"after": true}],
|
||||
"curly": "error",
|
||||
"eol-last": "warn",
|
||||
"eqeqeq": ["error", "smart"],
|
||||
"indent": ["error", 2],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"lines-around-comment": ["warn", {"beforeLineComment": true}],
|
||||
"newline-per-chained-call": ["error", {"ignoreChainWithDepth": 2}],
|
||||
"no-console": "off",
|
||||
"no-const-assign": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-eval": "error",
|
||||
"no-global-assign": "error",
|
||||
"no-implied-eval": "error",
|
||||
"no-lonely-if": "error",
|
||||
"no-multiple-empty-lines": ["error", {"max": 1}],
|
||||
"no-nested-ternary": "warn",
|
||||
"no-path-concat": "error",
|
||||
"no-script-url": "error",
|
||||
"no-tabs": "error",
|
||||
"no-template-curly-in-string": "warn",
|
||||
"no-throw-literal": "error",
|
||||
"no-unneeded-ternary": "warn",
|
||||
"no-unsafe-negation": "warn",
|
||||
"no-unused-vars": ["error", {"argsIgnorePattern": "^_|next", "varsIgnorePattern": "^_"}],
|
||||
"no-var": "error",
|
||||
"object-curly-spacing": "warn",
|
||||
"prefer-template": "warn",
|
||||
"quotes": ["error", "single"],
|
||||
"semi": ["error", "always"],
|
||||
"space-in-parens": ["error", "never"],
|
||||
"space-infix-ops": ["error"],
|
||||
"space-unary-ops": ["error", {"words": true, "nonwords": false}],
|
||||
"spaced-comment": ["warn", "always", {"line": {"exceptions": ["-", "="]}}],
|
||||
"template-curly-spacing": "warn",
|
||||
"yoda": "warn"
|
||||
}
|
||||
"extends": "@coralproject/eslint-config-talk"
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ client/coral-framework/graphql/introspection.json
|
||||
*.DS_STORE
|
||||
|
||||
coverage/
|
||||
test/e2e/reports/
|
||||
test/e2e/bslocal.log
|
||||
test/e2e/selenium-debug.log
|
||||
browserstack.err
|
||||
|
||||
plugins.json
|
||||
plugins/*
|
||||
@@ -43,5 +47,8 @@ plugins/*
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-deep-reply-count
|
||||
!plugins/talk-plugin-subscriber
|
||||
!plugins/talk-plugin-flag-details
|
||||
!plugins/talk-plugin-slack-notifications
|
||||
|
||||
**/node_modules/*
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"exceptions": [
|
||||
"https://nodesecurity.io/advisories/531",
|
||||
"https://nodesecurity.io/advisories/532"
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
---
|
||||
title: Contributor Covenant Code of Conduct
|
||||
permalink: /docs/development/code-of-conduct/
|
||||
---
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
@@ -46,7 +43,7 @@ Other inappropriate behavior:
|
||||
* Impersonation of someone else
|
||||
* Violating someone’s privacy
|
||||
|
||||
If you feel that someone has harassed you or otherwise treated you or someone else inappropriately, please alert the project lead at [andrewl@mozillafoundation.org](mailto:andrewl@mozillafoundation.org).
|
||||
If you feel that someone has harassed you or otherwise treated you or someone else inappropriately, please alert the project lead at [andrewl@mozillafoundation.org](mailto:andrewl@mozillafoundation.org).
|
||||
|
||||
|
||||
## Our Responsibilities
|
||||
@@ -71,11 +68,11 @@ If you’re too frustrated to have a direct conversation, there are a number of
|
||||
|
||||
* Contact the project lead, Andrew Losowsky, [andrewl@mozillafoundation.org](mailto:andrewl@mozillafoundation.org), or the technical lead. We will work with you to help you figure out how to ensure that any conflict doesn’t interfere with your work, in confidence if you would prefer.
|
||||
|
||||
* Talk to Chris Lawrence. Chris oversees the project. He can be contacted at [clawrence@mozillafoundation.org](mailto:clawrence@mozillafoundation.org).
|
||||
* Talk to Chris Lawrence. Chris oversees the project. He can be contacted at [clawrence@mozillafoundation.org](mailto:clawrence@mozillafoundation.org).
|
||||
|
||||
If you feel you have been unfairly accused of violating this code of conduct, you should contact Chris with a concise description of your grievance.
|
||||
|
||||
## Conclusion
|
||||
|
||||
We welcome your feedback on this and every other aspect of what we do as The Coral Project, and we thank you for working with us to make it a safe, enjoyable, and friendly experience for everyone involved in the project and what we do.
|
||||
Above text is licensed CC BY-SA 4.0, adapted from the SRCCON code of conduct, FreeBSD’s code of conduct, Vox Media’s product team code of conduct, Medium’s code of conduct, as well as adapted from the Contributor Covenant.
|
||||
Above text is licensed CC BY-SA 4.0, adapted from the SRCCON code of conduct, FreeBSD’s code of conduct, Vox Media’s product team code of conduct, Medium’s code of conduct, as well as adapted from the Contributor Covenant.
|
||||
@@ -1,13 +1,10 @@
|
||||
---
|
||||
title: Contributor's Guide
|
||||
permalink: /docs/development/contributing/
|
||||
---
|
||||
# Contributor's Guide
|
||||
|
||||
Welcome! We are very excited that you are interested in contributing to Talk.
|
||||
|
||||
This document is a companion to help you approach contributing. If it does not do so, please [let us know how we can improve it](https://github.com/coralproject/talk/issues)!
|
||||
|
||||
By contributing to this project you agree to the [Code of Conduct](https://coralproject.net/code-of-conduct).
|
||||
By contributing to this project you agree to the [Code of Conduct](CODE_OF_CONDUCT.md).
|
||||
|
||||
## What should I Contribute?
|
||||
|
||||
@@ -33,7 +30,7 @@ Please file issues if:
|
||||
|
||||
### What should I include?
|
||||
|
||||
Coral has adopted an iterative, agile development philosophy. All contributions that make it into the Talk repo should start with a story or this form:
|
||||
Coral has adopted an iterative, agile development philosophy. All contributions that make it into the Talk repository should start with a story or this form:
|
||||
|
||||
> As a [type of person] I'd like to be able to [do something] so that I can [get some result].
|
||||
|
||||
@@ -56,15 +53,24 @@ We are looking for _documentarians_ to:
|
||||
* create new / missing sections, and
|
||||
* take the lead in making sections, or the over all structure better.
|
||||
|
||||
Information about how to update docs can be found in our [FAQ]({{ "/docs/faq" | absolute_url }}#how-do-i-contribute-to-these-docs).
|
||||
Our documentation is stored in markdown files in the [docs](docs) directory. We
|
||||
use Jekyll to provide our docs. To preview:
|
||||
|
||||
```shell
|
||||
cd docs
|
||||
bundle install
|
||||
bundle exec jekyll serve
|
||||
```
|
||||
|
||||
Then visit http://127.0.0.1:4000/talk/.
|
||||
|
||||
If you'd like to discuss a contribution, please [file an issue](https://github.com/coralproject/talk/issues) describing the changes you would like to see.
|
||||
|
||||
## Contributing Translations
|
||||
|
||||
Talk's tranlations are stored in `.yml` files [here](https://github.com/coralproject/talk/tree/master/locales).
|
||||
Talk's translations are stored in `.yml` files [here](https://github.com/coralproject/talk/tree/master/locales).
|
||||
|
||||
Translations can be submitted via pull request. If you do not use github, you can use 'en.yml' as a template and [email](https://coralproject.net/contact) the translations to us. We can import it into the repo.
|
||||
Translations can be submitted via pull request. If you do not use github, you can use 'en.yml' as a template and [email](https://coralproject.net/contact) the translations to us. We can import it into the repository.
|
||||
|
||||
## I want to contribute but I'm not sure what to do!
|
||||
|
||||
@@ -84,4 +90,4 @@ Have a favorite analytics engine? Data science service? CMS? Auth platform? Depl
|
||||
|
||||
### Favorite Features?
|
||||
|
||||
Do you have a favorite feature of an existing platform that's not yet been done in Talk? Sounds like Talk needs that feature.
|
||||
Do you have a favorite feature of an existing platform that's not yet been done in Talk? Sounds like Talk needs that feature.
|
||||
+11
-4
@@ -1,11 +1,18 @@
|
||||
FROM coralproject/talk:latest
|
||||
|
||||
# Setup the build arguments
|
||||
ONBUILD ARG TALK_THREADING_LEVEL=3
|
||||
ONBUILD ARG TALK_DEFAULT_STREAM_TAB=all
|
||||
ONBUILD ARG TALK_DEFAULT_LANG=en
|
||||
ONBUILD ARG TALK_PLUGINS_JSON
|
||||
|
||||
# Bundle app source
|
||||
ONBUILD COPY . /usr/src/app
|
||||
|
||||
# At this stage, we need to install the development dependancies again because
|
||||
# we need to have webpack available. We then build the new dependancies and
|
||||
# clear out the development dependancies again. After this we of course need to
|
||||
# At this stage, we need to install the development dependencies again because
|
||||
# we need to have webpack available. We then build the new dependencies and
|
||||
# clear out the development dependencies again. After this we of course need to
|
||||
# clear out the yarn cache, this saves quite a lot of size.
|
||||
ONBUILD RUN cli plugins reconcile && \
|
||||
yarn build
|
||||
yarn build && \
|
||||
yarn cache clean
|
||||
@@ -4,8 +4,12 @@ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
||||
either express or implied.
|
||||
|
||||
See the License for the specific language governing permissions and limitations under the License.
|
||||
See the License for the specific language governing permissions
|
||||
and limitations under the License.
|
||||
|
||||
@@ -1,40 +1,37 @@
|
||||
# Talk [](https://circleci.com/gh/coralproject/talk)
|
||||
[](https://dashboard.heroku.com/new?template=https%3A%2F%2Fgithub.com%2Fcoralproject%2Ftalk&env[TALK_FACEBOOK_APP_ID]=ignore&env[TALK_FACEBOOK_APP_SECRET]=ignore)
|
||||
# Talk · [](https://circleci.com/gh/coralproject/talk) · [](https://nodesecurity.io/orgs/coralproject/projects/07ce2e4c-99fb-48f8-b50b-69d2d2c081b8) · [](CONTRIBUTING.md#pull-requests)
|
||||
|
||||
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)
|
||||
Online comments are broken. Our open-source commenting platform, Talk, 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.
|
||||
Built with <3 by The Coral Project & Mozilla.
|
||||
|
||||
## Try Talk!
|
||||
|
||||
- Developer Documentation & Setup Guides: https://coralproject.github.io/talk/ (includes Installation Guide, Quickstart, Plugin Guide, API Docs, and more)
|
||||
You're just one click away from trying Talk - all you need is a Heroku account and a few minutes of your time.
|
||||
|
||||
## Roadmap and Release Schedule
|
||||
[](https://dashboard.heroku.com/new?template=https%3A%2F%2Fgithub.com%2Fcoralproject%2Ftalk&env[TALK_FACEBOOK_APP_ID]=ignore&env[TALK_FACEBOOK_APP_SECRET]=ignore)
|
||||
|
||||
- Talk Roadmap: https://www.pivotaltracker.com/n/projects/1863625
|
||||
## Technical Documentation
|
||||
|
||||
From getting up and running, to advanced configuration, to how to scale Talk, our [Talk Technical Docs](https://coralproject.github.io/talk/) have everything you need to know.
|
||||
|
||||
## Learn More about Coral
|
||||
## Product Guide
|
||||
|
||||
- Community Forums: https://community.coralproject.net/
|
||||
Learn more about Talk, including a deep dive into features for commenters and moderators, and FAQs in our [Talk Product Guide](https://coralproject.github.io/talk/how-talk-works).
|
||||
|
||||
- Website: https://coralproject.net
|
||||
## Relevant Links
|
||||
|
||||
- Blog: https://blog.coralproject.net
|
||||
- [Our Blog](https://blog.coralproject.net/)
|
||||
- [Community Forums](https://community.coralproject.net/)
|
||||
- [Community Guides for Journalism](https://guides.coralproject.net/)
|
||||
- [More About Us](https://coralproject.net/)
|
||||
- [Talk Roadmap](https://www.pivotaltracker.com/n/projects/1863625)
|
||||
|
||||
- Community Guides for Journalism: https://guides.coralproject.net/
|
||||
## End-to-End Testing
|
||||
|
||||
Talk uses [Nightwatch](https://nightwatchjs.org/) as our e2e testing framework. The testing infrastructure that allows us to run our tests in real browsers is provided with love by our friends at [Browserstack](https://browserstack.com).
|
||||
|
||||
[](https://browserstack.com)
|
||||
|
||||
## 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.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
||||
See the License for the specific language governing permissions and limitations under the License.
|
||||
Talk is released under the [Apache License, v2.0](/LICENSE).
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const morgan = require('morgan');
|
||||
const path = require('path');
|
||||
const merge = require('lodash/merge');
|
||||
const helmet = require('helmet');
|
||||
const compression = require('compression');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const {
|
||||
BASE_URL,
|
||||
BASE_PATH,
|
||||
MOUNT_PATH,
|
||||
STATIC_URL,
|
||||
HELMET_CONFIGURATION,
|
||||
} = require('./url');
|
||||
const {HELMET_CONFIGURATION} = require('./config');
|
||||
const {MOUNT_PATH} = require('./url');
|
||||
const routes = require('./routes');
|
||||
const debug = require('debug')('talk:app');
|
||||
|
||||
@@ -40,12 +33,6 @@ app.use(helmet(merge(HELMET_CONFIGURATION, {
|
||||
// Compress the responses if appropriate.
|
||||
app.use(compression());
|
||||
|
||||
// Parse the cookies on the request.
|
||||
app.use(cookieParser());
|
||||
|
||||
// Parse the body json if it's there.
|
||||
app.use(bodyParser.json());
|
||||
|
||||
//==============================================================================
|
||||
// VIEW CONFIGURATION
|
||||
//==============================================================================
|
||||
@@ -57,13 +44,6 @@ app.set('view engine', 'ejs');
|
||||
// ROUTES
|
||||
//==============================================================================
|
||||
|
||||
// Apply the BASE_PATH, BASE_URL, and MOUNT_PATH on the app.locals, which will
|
||||
// make them available on the templates and the routers.
|
||||
app.locals.BASE_URL = BASE_URL;
|
||||
app.locals.BASE_PATH = BASE_PATH;
|
||||
app.locals.MOUNT_PATH = MOUNT_PATH;
|
||||
app.locals.STATIC_URL = STATIC_URL;
|
||||
|
||||
debug(`mounting routes on the ${MOUNT_PATH} path`);
|
||||
|
||||
// Actually apply the routes.
|
||||
|
||||
+44
-23
@@ -12,7 +12,7 @@ const mongoose = require('../services/mongoose');
|
||||
const kue = require('../services/kue');
|
||||
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
() => mongoose.disconnect(),
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -20,17 +20,17 @@ util.onshutdown([
|
||||
*/
|
||||
function processJobs() {
|
||||
|
||||
// Start the scraper processor.
|
||||
scraper.process();
|
||||
|
||||
// Start the mail processor.
|
||||
mailer.process();
|
||||
|
||||
// The scraper only needs to shutdown when the scraper has actually been
|
||||
// started.
|
||||
util.onshutdown([
|
||||
() => kue.Task.shutdown()
|
||||
]);
|
||||
|
||||
// Start the scraper processor.
|
||||
scraper.process();
|
||||
|
||||
// Start the mail processor.
|
||||
mailer.process();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,22 +48,13 @@ function removeJob(job) {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the jobs passed in and returns a promise.
|
||||
* @param {Array} jobs array of jobs
|
||||
* @return {Promise}
|
||||
*/
|
||||
function removeJobs(jobs) {
|
||||
return Promise.all(jobs.map(removeJob));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the top n jobs with a specific state.
|
||||
* @param {String} [state='complete'] state to list jobs by
|
||||
* @param {Number} limit limit of jobs to load
|
||||
* @return {Promise}
|
||||
*/
|
||||
function rangeJobsByState(state = 'complete', limit) {
|
||||
function rangeJobsByState(state, limit) {
|
||||
return new Promise((resolve, reject) => {
|
||||
kue.Job.rangeByState(state, 0, limit, 'asc', (err, jobs) => {
|
||||
if (err) {
|
||||
@@ -75,22 +66,52 @@ function rangeJobsByState(state = 'complete', limit) {
|
||||
});
|
||||
}
|
||||
|
||||
async function getJobBatch(n, includeStuck) {
|
||||
let jobs = [];
|
||||
|
||||
jobs = await rangeJobsByState('complete', n);
|
||||
|
||||
if (includeStuck) {
|
||||
jobs = jobs.concat(await rangeJobsByState('failed', n));
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the jobs that are in the queue.
|
||||
*/
|
||||
async function cleanupJobs(options) {
|
||||
|
||||
// The scraper only needs to shutdown when the scraper has actually been
|
||||
// started.
|
||||
util.onshutdown([
|
||||
() => kue.Task.shutdown()
|
||||
]);
|
||||
|
||||
const n = 100;
|
||||
|
||||
try {
|
||||
const joblists = await Promise.all([
|
||||
rangeJobsByState('complete', n),
|
||||
options.stuck ? rangeJobsByState('failed', n) : false
|
||||
]);
|
||||
|
||||
await joblists.filter((jobs) => jobs).map(removeJobs);
|
||||
// Connect to redis by establishing a queue.
|
||||
kue.Task.connect();
|
||||
|
||||
let jobCount = 0;
|
||||
let jobs = await getJobBatch(n, options.stuck);
|
||||
|
||||
while (jobs.length > 0) {
|
||||
|
||||
// Remove all the jobs.
|
||||
await Promise.all(jobs.map((job) => removeJob(job)));
|
||||
|
||||
jobCount += jobs.length;
|
||||
|
||||
// Get the next batch of jobs.
|
||||
jobs = await getJobBatch(n, options.stuck);
|
||||
}
|
||||
|
||||
util.shutdown();
|
||||
console.log('Removed old jobs');
|
||||
console.log(`Removed ${jobCount} jobs`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
|
||||
+6
-174
@@ -1,161 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const program = require('./commander');
|
||||
const app = require('../app');
|
||||
const debug = require('debug')('talk:cli:serve');
|
||||
const errors = require('../errors');
|
||||
const {createServer} = require('http');
|
||||
const scraper = require('../services/scraper');
|
||||
const mailer = require('../services/mailer');
|
||||
const MigrationService = require('../services/migration');
|
||||
const SetupService = require('../services/setup');
|
||||
const kue = require('../services/kue');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const util = require('./util');
|
||||
const cache = require('../services/cache');
|
||||
const {createSubscriptionManager} = require('../graph/subscriptions');
|
||||
const {
|
||||
PORT
|
||||
} = require('../config');
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
|
||||
const port = normalizePort(PORT);
|
||||
app.set('port', port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
const server = createServer(app);
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
*/
|
||||
function onError(error) {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
let bind = typeof port === 'string'
|
||||
? `Pipe ${port}`
|
||||
: `Port ${port}`;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(`${bind} requires elevated privileges`);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(`${bind} is already in use`);
|
||||
break;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
*/
|
||||
|
||||
function normalizePort(val) {
|
||||
let port = parseInt(val, 10);
|
||||
|
||||
if (isNaN(port)) {
|
||||
|
||||
// named pipe
|
||||
return val;
|
||||
}
|
||||
|
||||
if (port >= 0) {
|
||||
|
||||
// port number
|
||||
return port;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "listening" event.
|
||||
*/
|
||||
|
||||
async function onListening() {
|
||||
|
||||
// Start the cache instance.
|
||||
await cache.init();
|
||||
|
||||
let addr = server.address();
|
||||
let bind = typeof addr === 'string'
|
||||
? `pipe ${addr}`
|
||||
: `port ${addr.port}`;
|
||||
debug(`API Server Listening on ${bind}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the app.
|
||||
*/
|
||||
async function startApp(program) {
|
||||
|
||||
try {
|
||||
|
||||
// Check to see if the application is installed. If the application
|
||||
// has been installed, then it will throw errors.ErrSettingsNotInit, this
|
||||
// just means we don't have to check that the migrations have run.
|
||||
await SetupService.isAvailable();
|
||||
|
||||
debug('setup is currently available, migrations not being checked');
|
||||
|
||||
} catch (e) {
|
||||
|
||||
// Check the error.
|
||||
switch (e) {
|
||||
case errors.ErrInstallLock, errors.ErrSettingsInit:
|
||||
|
||||
debug('setup is not currently available, migrations now being checked');
|
||||
|
||||
// The error was expected, just continue.
|
||||
break;
|
||||
default:
|
||||
|
||||
// The error was not expected, throw the error!
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Now try and check the migration status.
|
||||
try {
|
||||
|
||||
// Verify that the minimum migration version is met.
|
||||
await MigrationService.verify();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
debug('migrations do not have to be run');
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
server.on('listening', () => {
|
||||
|
||||
});
|
||||
server.listen(port, () => {
|
||||
|
||||
// Mount the websocket server if requested.
|
||||
if (program.websockets) {
|
||||
debug(`Websocket Server Listening on ${port}`);
|
||||
|
||||
// Mount the subscriptions server on the application server.
|
||||
createSubscriptionManager(server);
|
||||
}
|
||||
});
|
||||
}
|
||||
const serve = require('../serve');
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
@@ -166,24 +13,9 @@ program
|
||||
.option('-w, --websockets', 'enable the websocket (subscriptions) handler on this thread')
|
||||
.parse(process.argv);
|
||||
|
||||
// Start the application serving.
|
||||
startApp(program);
|
||||
// Start serving.
|
||||
serve({jobs: program.jobs, websockets: program.websockets}).catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
|
||||
// Enable job processing on the thread if enabled.
|
||||
if (program.jobs) {
|
||||
|
||||
// Start the scraper processor.
|
||||
scraper.process();
|
||||
|
||||
// Start the mail processor.
|
||||
mailer.process();
|
||||
}
|
||||
|
||||
// Define a safe shutdown function to call in the event we need to shutdown
|
||||
// because the node hooks are below which will interrupt the shutdown process.
|
||||
// Shutdown the mongoose connection, the app server, and the scraper.
|
||||
util.onshutdown([
|
||||
() => program.jobs ? kue.Task.shutdown() : null,
|
||||
() => mongoose.disconnect(),
|
||||
() => server.close()
|
||||
]);
|
||||
|
||||
+1
-1
@@ -195,7 +195,7 @@ const performSetup = async () => {
|
||||
password: user.password
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
console.log('Settings created.');
|
||||
console.log(`User ${newUser.id} created.`);
|
||||
console.log('\nTalk is now installed!');
|
||||
|
||||
+70
-15
@@ -8,10 +8,13 @@ const program = require('./commander');
|
||||
const inquirer = require('inquirer');
|
||||
const UsersService = require('../services/users');
|
||||
const UserModel = require('../models/user');
|
||||
const CommentModel = require('../models/comment');
|
||||
const ActionModel = require('../models/action');
|
||||
const USER_ROLES = require('../models/enum/user_roles');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const util = require('./util');
|
||||
const Table = require('cli-table');
|
||||
const databaseVerifications = require('./verifications/database');
|
||||
|
||||
const validateRequired = (msg = 'Field is required', len = 1) => (input) => {
|
||||
if (input && input.length >= len) {
|
||||
@@ -122,26 +125,48 @@ async function createUser(options) {
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown();
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a user.
|
||||
*/
|
||||
function deleteUser(userID) {
|
||||
UserModel
|
||||
.findOneAndRemove({
|
||||
id: userID
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Deleted user');
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown();
|
||||
});
|
||||
async function deleteUser(userID) {
|
||||
|
||||
try {
|
||||
|
||||
// Find the user we're removing.
|
||||
const user = await UserModel.findOne({id: userID});
|
||||
if (!user) {
|
||||
throw new Error(`user with id ${userID} not found`);
|
||||
}
|
||||
|
||||
// Remove all the user's actions.
|
||||
await ActionModel
|
||||
.where({user_id: user.id})
|
||||
.setOptions({multi: true})
|
||||
.remove();
|
||||
|
||||
// Remove all the user's comments.
|
||||
await CommentModel
|
||||
.where({author_id: user.id})
|
||||
.setOptions({multi: true})
|
||||
.remove();
|
||||
|
||||
// Update the counts that might have changed.
|
||||
for (const verification of databaseVerifications) {
|
||||
await verification({fix: true, limit: Infinity, batch: 1000});
|
||||
}
|
||||
|
||||
// Remove the user.
|
||||
await user.remove();
|
||||
|
||||
util.shutdown();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -241,13 +266,21 @@ function listUsers() {
|
||||
});
|
||||
|
||||
users.forEach((user) => {
|
||||
let state = user.disabled ? 'Disabled' : 'Enabled';
|
||||
const profile = user.profiles.find(({provider}) => provider === 'local');
|
||||
if (profile && profile.metadata && profile.metadata.confirmed_at) {
|
||||
state += ', Verified';
|
||||
} else {
|
||||
state += ', Unverified';
|
||||
}
|
||||
|
||||
table.push([
|
||||
user.id,
|
||||
user.username,
|
||||
user.profiles.map((p) => p.provider).join(', '),
|
||||
user.roles.join(', '),
|
||||
user.status,
|
||||
user.disabled ? 'Disabled' : 'Enabled'
|
||||
state
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -396,6 +429,23 @@ function enableUser(userID) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies an email address for a user.
|
||||
*
|
||||
* @param userID the user's id
|
||||
* @param email the user's email address to be verified
|
||||
*/
|
||||
async function verify(userID, email) {
|
||||
try {
|
||||
await UsersService.confirmEmail(userID, email);
|
||||
console.log(`User ${userID} had their email ${email} verified.`);
|
||||
util.shutdown();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
@@ -467,6 +517,11 @@ program
|
||||
.description('enable a given user from logging in')
|
||||
.action(enableUser);
|
||||
|
||||
program
|
||||
.command('verify <userID> <email>')
|
||||
.description('verifies the given user\'s email address')
|
||||
.action(verify);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
"add-module-exports",
|
||||
"transform-class-properties",
|
||||
"transform-decorators-legacy",
|
||||
"transform-object-assign",
|
||||
"transform-object-rest-spread",
|
||||
"transform-async-to-generator",
|
||||
"transform-react-jsx"
|
||||
]
|
||||
}
|
||||
+15
-4
@@ -7,9 +7,7 @@ machine:
|
||||
environment:
|
||||
PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin"
|
||||
NODE_ENV: "test"
|
||||
|
||||
dependencies:
|
||||
override:
|
||||
pre:
|
||||
# TODO: use the following to add in support for MongoDB 3.4.
|
||||
# # Upgrade the database version to 3.4.
|
||||
# - sudo apt-get purge mongodb-org*
|
||||
@@ -19,10 +17,20 @@ dependencies:
|
||||
# - sudo apt-get install -y mongodb-org
|
||||
# - sudo service mongod restart
|
||||
|
||||
# Install chromium for e2e and remove old google-chrome
|
||||
- sudo rm -rf /opt/google/chrome
|
||||
- sudo rm -f /usr/bin/google-chrome*
|
||||
- sudo apt-get update
|
||||
- sudo apt-get install chromium-browser
|
||||
|
||||
dependencies:
|
||||
override:
|
||||
|
||||
# Install node dependencies.
|
||||
- yarn --version
|
||||
- yarn global add node-gyp --force
|
||||
- yarn global add node-gyp nsp --force
|
||||
- yarn
|
||||
|
||||
post:
|
||||
# Build the static assets.
|
||||
- yarn build
|
||||
@@ -40,6 +48,9 @@ test:
|
||||
override:
|
||||
# Run the tests using the junit reporter.
|
||||
- MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml MOCHA_REPORTER=mocha-junit-reporter yarn test
|
||||
# Check dependancies using nsp.
|
||||
- nsp check
|
||||
- yarn e2e-ci
|
||||
|
||||
deployment:
|
||||
release:
|
||||
|
||||
+1
-22
@@ -1,24 +1,3 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"extends": "../.eslintrc.json",
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error",
|
||||
"no-console": ["warn", { "allow": ["warn", "error"] }]
|
||||
}
|
||||
"extends": "@coralproject/eslint-config-talk/client"
|
||||
}
|
||||
|
||||
@@ -3,10 +3,9 @@ import PropTypes from 'prop-types';
|
||||
import {Router, Route, IndexRedirect, IndexRoute} from 'react-router';
|
||||
|
||||
import Configure from 'routes/Configure';
|
||||
import Dashboard from 'routes/Dashboard';
|
||||
import Install from 'routes/Install';
|
||||
import Stories from 'routes/Stories';
|
||||
import {CommunityLayout, Community} from 'routes/Community';
|
||||
import Community from 'routes/Community/containers/Community';
|
||||
import {ModerationLayout, Moderation} from 'routes/Moderation';
|
||||
|
||||
import Layout from 'containers/Layout';
|
||||
@@ -18,11 +17,10 @@ const routes = (
|
||||
<IndexRedirect to='/admin/moderate' />
|
||||
<Route path='configure' component={Configure} />
|
||||
<Route path='stories' component={Stories} />
|
||||
<Route path='dashboard' component={Dashboard} />
|
||||
|
||||
{/* Community Routes */}
|
||||
|
||||
<Route path='community' component={CommunityLayout}>
|
||||
<Route path='community'>
|
||||
<Route path='flagged' components={Community}>
|
||||
<Route path=':id' components={Community} />
|
||||
</Route>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import qs from 'qs';
|
||||
import queryString from 'query-string';
|
||||
|
||||
import {
|
||||
FETCH_COMMENTERS_REQUEST,
|
||||
FETCH_COMMENTERS_SUCCESS,
|
||||
FETCH_COMMENTERS_FAILURE,
|
||||
FETCH_USERS_REQUEST,
|
||||
FETCH_USERS_SUCCESS,
|
||||
FETCH_USERS_FAILURE,
|
||||
SORT_UPDATE,
|
||||
COMMENTERS_NEW_PAGE,
|
||||
SET_PAGE,
|
||||
SET_SEARCH_VALUE,
|
||||
SET_ROLE,
|
||||
SET_COMMENTER_STATUS,
|
||||
SHOW_BANUSER_DIALOG,
|
||||
@@ -16,14 +17,13 @@ import {
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const fetchAccounts = (query = {}) => (dispatch, _, {rest}) => {
|
||||
|
||||
dispatch(requestFetchAccounts());
|
||||
rest(`/users?${qs.stringify(query)}`)
|
||||
export const fetchUsers = (query = {}) => (dispatch, _, {rest}) => {
|
||||
dispatch(requestFetchUsers());
|
||||
rest(`/users?${queryString.stringify(query)}`)
|
||||
.then(({result, page, count, limit, totalPages}) =>{
|
||||
dispatch({
|
||||
type: FETCH_COMMENTERS_SUCCESS,
|
||||
accounts: result,
|
||||
type: FETCH_USERS_SUCCESS,
|
||||
users: result,
|
||||
page,
|
||||
count,
|
||||
limit,
|
||||
@@ -33,12 +33,12 @@ export const fetchAccounts = (query = {}) => (dispatch, _, {rest}) => {
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: FETCH_COMMENTERS_FAILURE, error: errorMessage});
|
||||
dispatch({type: FETCH_USERS_FAILURE, error: errorMessage});
|
||||
});
|
||||
};
|
||||
|
||||
const requestFetchAccounts = () => ({
|
||||
type: FETCH_COMMENTERS_REQUEST
|
||||
const requestFetchUsers = () => ({
|
||||
type: FETCH_USERS_REQUEST
|
||||
});
|
||||
|
||||
export const updateSorting = (sort) => ({
|
||||
@@ -46,8 +46,14 @@ export const updateSorting = (sort) => ({
|
||||
sort
|
||||
});
|
||||
|
||||
export const newPage = () => ({
|
||||
type: COMMENTERS_NEW_PAGE
|
||||
export const setPage = (page) => ({
|
||||
type: SET_PAGE,
|
||||
page,
|
||||
});
|
||||
|
||||
export const setSearchValue = (value) => ({
|
||||
type: SET_SEARCH_VALUE,
|
||||
value,
|
||||
});
|
||||
|
||||
export const setRole = (id, role) => (dispatch, _, {rest}) => {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import * as actions from 'constants/configure';
|
||||
|
||||
export const updatePending = ({updater, errorUpdater}) => {
|
||||
return {type: actions.UPDATE_PENDING, updater, errorUpdater};
|
||||
};
|
||||
|
||||
export const clearPending = () => {
|
||||
return {type: actions.CLEAR_PENDING};
|
||||
};
|
||||
|
||||
export const setActiveSection = (section) => {
|
||||
return {type: actions.SET_ACTIVE_SECTION, section};
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const SETTINGS_LOADING = 'SETTINGS_LOADING';
|
||||
export const SETTINGS_RECEIVED = 'SETTINGS_RECEIVED';
|
||||
export const SETTINGS_FETCH_ERROR = 'SETTINGS_FETCH_ERROR';
|
||||
|
||||
export const SETTINGS_UPDATED = 'SETTINGS_UPDATED';
|
||||
|
||||
export const SAVE_SETTINGS_LOADING = 'SAVE_SETTINGS_LOADING';
|
||||
export const SAVE_SETTINGS_SUCCESS = 'SAVE_SETTINGS_SUCCESS';
|
||||
export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED';
|
||||
|
||||
export const WORDLIST_UPDATED = 'WORDLIST_UPDATED';
|
||||
export const DOMAINLIST_UPDATED = 'DOMAINLIST_UPDATED';
|
||||
|
||||
export const fetchSettings = () => (dispatch, _, {rest}) => {
|
||||
dispatch({type: SETTINGS_LOADING});
|
||||
rest('/settings')
|
||||
.then((settings) => {
|
||||
dispatch({type: SETTINGS_RECEIVED, settings});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: SETTINGS_FETCH_ERROR, error: errorMessage});
|
||||
});
|
||||
};
|
||||
|
||||
// for updating top-level settings
|
||||
export const updateSettings = (settings) => {
|
||||
return {type: SETTINGS_UPDATED, settings};
|
||||
};
|
||||
|
||||
// this is a nested property, so it needs a special action.
|
||||
export const updateWordlist = (listName, list) => {
|
||||
return {type: WORDLIST_UPDATED, listName, list};
|
||||
};
|
||||
|
||||
export const updateDomainlist = (listName, list) => {
|
||||
return {type: DOMAINLIST_UPDATED, listName, list};
|
||||
};
|
||||
|
||||
export const saveSettingsToServer = () => (dispatch, getState, {rest}) => {
|
||||
let settings = getState().settings;
|
||||
if (settings.charCount) {
|
||||
settings.charCount = parseInt(settings.charCount);
|
||||
}
|
||||
dispatch({type: SAVE_SETTINGS_LOADING});
|
||||
rest('/settings', {method: 'PUT', body: settings})
|
||||
.then(() => {
|
||||
dispatch({type: SAVE_SETTINGS_SUCCESS, settings});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: SAVE_SETTINGS_FAILED, error: errorMessage});
|
||||
});
|
||||
};
|
||||
+29
-5
@@ -1,12 +1,17 @@
|
||||
import queryString from 'query-string';
|
||||
|
||||
import {
|
||||
FETCH_ASSETS_REQUEST,
|
||||
FETCH_ASSETS_SUCCESS,
|
||||
FETCH_ASSETS_FAILURE,
|
||||
SET_PAGE,
|
||||
SET_SEARCH_VALUE,
|
||||
SET_CRITERIA,
|
||||
UPDATE_ASSET_STATE_REQUEST,
|
||||
UPDATE_ASSET_STATE_SUCCESS,
|
||||
UPDATE_ASSET_STATE_FAILURE,
|
||||
UPDATE_ASSETS
|
||||
} from '../constants/assets';
|
||||
} from '../constants/stories';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
@@ -16,13 +21,16 @@ import t from 'coral-framework/services/i18n';
|
||||
|
||||
// Fetch a page of assets
|
||||
// Get comments to fill each of the three lists on the mod queue
|
||||
export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch, _, {rest}) => {
|
||||
export const fetchAssets = (query = {}) => (dispatch, _, {rest}) => {
|
||||
dispatch({type: FETCH_ASSETS_REQUEST});
|
||||
return rest(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`)
|
||||
.then(({result, count}) =>
|
||||
return rest(`/assets?${queryString.stringify(query)}`)
|
||||
.then(({result, page, count, limit, totalPages}) =>
|
||||
dispatch({type: FETCH_ASSETS_SUCCESS,
|
||||
assets: result,
|
||||
count
|
||||
page,
|
||||
count,
|
||||
limit,
|
||||
totalPages,
|
||||
}))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
@@ -47,3 +55,19 @@ export const updateAssetState = (id, closedAt) => (dispatch, _, {rest}) => {
|
||||
export const updateAssets = (assets) => (dispatch) => {
|
||||
dispatch({type: UPDATE_ASSETS, assets});
|
||||
};
|
||||
|
||||
export const setPage = (page) => ({
|
||||
type: SET_PAGE,
|
||||
page,
|
||||
});
|
||||
|
||||
export const setSearchValue = (value) => ({
|
||||
type: SET_SEARCH_VALUE,
|
||||
value,
|
||||
});
|
||||
|
||||
export const setCriteria = (criteria) => ({
|
||||
type: SET_CRITERIA,
|
||||
criteria,
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ export const viewUserDetail = (userId) => ({type: actions.VIEW_USER_DETAIL, user
|
||||
export const hideUserDetail = () => ({type: actions.HIDE_USER_DETAIL});
|
||||
|
||||
export const changeUserDetailStatuses = (tab) => {
|
||||
let statuses = [];
|
||||
let statuses = null;
|
||||
if (tab === 'rejected') {
|
||||
statuses = ['REJECTED'];
|
||||
}
|
||||
@@ -20,3 +20,9 @@ export const toggleSelectCommentInUserDetail = (id, active) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const toggleSelectAllCommentInUserDetail = (ids, active) => {
|
||||
return {
|
||||
type: active ? actions.SELECT_ALL_USER_DETAIL_COMMENT : actions.CLEAR_USER_DETAIL_SELECTIONS,
|
||||
ids
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import styles from './ModerationList.css';
|
||||
import {Button} from 'coral-ui';
|
||||
import {menuActionsMap} from '../utils/moderationQueueActionsMap';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
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={`${typeName} ${styles.actionButton} ${props.minimal ? styles.minimal : ''} ${active ? styles[`${typeName}__active`] : ''}`}
|
||||
cStyle={typeName}
|
||||
icon={menuActionsMap[type].icon}
|
||||
onClick={type === 'APPROVE' ? props.acceptComment : props.rejectComment}
|
||||
>{props.minimal ? '' : t(`modqueue.${text}`)}</Button>
|
||||
);
|
||||
};
|
||||
|
||||
ActionButton.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
type: PropTypes.oneOf(['APPROVE', 'REJECT']),
|
||||
minimal: PropTypes.bool,
|
||||
acceptComment: PropTypes.func,
|
||||
rejectComment: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ActionButton;
|
||||
@@ -32,8 +32,9 @@ class ActionsMenu extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {className = ''} = this.props;
|
||||
return (
|
||||
<div className={styles.root} onBlur={this.syncOpenState} >
|
||||
<div className={cn(styles.root, className)} onBlur={this.syncOpenState} >
|
||||
<Button
|
||||
cStyle='actions'
|
||||
className={cn(styles.button, {[styles.buttonOpen]: this.state.open})}
|
||||
@@ -59,6 +60,7 @@ class ActionsMenu extends React.Component {
|
||||
ActionsMenu.propTypes = {
|
||||
icon: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ActionsMenu;
|
||||
|
||||
@@ -3,12 +3,14 @@ import cn from 'classnames';
|
||||
import {MenuItem} from 'react-mdl';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './ActionsMenu.css';
|
||||
import camelCase from 'lodash/camelCase';
|
||||
|
||||
const ActionsMenuItem = (props) =>
|
||||
<MenuItem className={cn(styles.menuItem, props.className)} {...props} />;
|
||||
|
||||
<MenuItem className={cn(styles.menuItem, props.className, 'action-menu-item')} {...props} id={camelCase(props.children)}/>;
|
||||
|
||||
ActionsMenuItem.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ActionsMenuItem;
|
||||
|
||||
@@ -4,6 +4,7 @@ import Layout from 'coral-admin/src/components/ui/Layout';
|
||||
import styles from './NotFound.css';
|
||||
import {Button, TextField, Alert, Success} from 'coral-ui';
|
||||
import Recaptcha from 'react-recaptcha';
|
||||
import cn from 'classnames';
|
||||
|
||||
class AdminLogin extends React.Component {
|
||||
|
||||
@@ -34,19 +35,22 @@ class AdminLogin extends React.Component {
|
||||
render () {
|
||||
const {errorMessage, loginMaxExceeded, recaptchaPublic} = this.props;
|
||||
const signInForm = (
|
||||
<form onSubmit={this.handleSignIn}>
|
||||
<form className="talk-admin-login-sign-in" onSubmit={this.handleSignIn}>
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
id="email"
|
||||
label='Email Address'
|
||||
value={this.state.email}
|
||||
onChange={(e) => this.setState({email: e.target.value})} />
|
||||
<TextField
|
||||
id="password"
|
||||
label='Password'
|
||||
value={this.state.password}
|
||||
onChange={(e) => this.setState({password: e.target.value})}
|
||||
type='password' />
|
||||
<div style={{height: 10}}></div>
|
||||
<Button
|
||||
className="talk-admin-login-sign-in-button"
|
||||
type='submit'
|
||||
cStyle='black'
|
||||
full
|
||||
@@ -90,7 +94,7 @@ class AdminLogin extends React.Component {
|
||||
);
|
||||
return (
|
||||
<Layout fixedDrawer restricted={true}>
|
||||
<div className={styles.loginLayout}>
|
||||
<div className={cn(styles.loginLayout, 'talk-admin-login')}>
|
||||
<h1 className={styles.loginHeader}>Team sign in</h1>
|
||||
<p className={styles.loginCTA}>Sign in to interact with your community.</p>
|
||||
{ this.state.requestPassword ? requestPasswordForm : signInForm }
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
.root {
|
||||
display: block;
|
||||
color: #519954;
|
||||
border: solid 2px rgba(81, 153, 84, 0.75);
|
||||
background: white;
|
||||
padding: 10px 12px;
|
||||
box-sizing: border-box;
|
||||
vertical-align: middle;
|
||||
line-height: 24px;
|
||||
font-size: 17px;
|
||||
height: 47px;
|
||||
border-radius: 3px;
|
||||
text-transform: capitalize;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
|
||||
width: 129px;
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
background-color: #519954;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
background-color: #519954;
|
||||
|
||||
&:hover {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.minimal {
|
||||
width: 45px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import cn from 'classnames';
|
||||
import styles from './ApproveButton.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const ApproveButton = ({active, minimal, onClick, className}) => {
|
||||
const text = active ? t('modqueue.approved') : t('modqueue.approve');
|
||||
return (
|
||||
<button
|
||||
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active}, className)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon name={'done'} className={styles.icon} />
|
||||
{!minimal && text}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
ApproveButton.propTypes = {
|
||||
className: PropTypes.string,
|
||||
active: PropTypes.bool,
|
||||
minimal: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ApproveButton;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Dialog} from 'coral-ui';
|
||||
import styles from './BanUserDialog.css';
|
||||
@@ -8,28 +9,34 @@ import t from 'coral-framework/services/i18n';
|
||||
|
||||
const BanUserDialog = ({open, onCancel, onPerform, username, info}) => (
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
className={cn(styles.dialog, 'talk-ban-user-dialog')}
|
||||
id="banUserDialog"
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
title={t('bandialog.ban_user')}>
|
||||
<span className={styles.close} onClick={onCancel}>×</span>
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<h2>{t('bandialog.ban_user')}</h2>
|
||||
</div>
|
||||
<div className={styles.separator}>
|
||||
<h3>{t('bandialog.are_you_sure', username)}</h3>
|
||||
<i>{info}</i>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<Button cStyle="cancel" className={styles.cancel} onClick={onCancel} raised>
|
||||
{t('bandialog.cancel')}
|
||||
</Button>
|
||||
<Button cStyle="black" className={styles.ban} onClick={onPerform} raised>
|
||||
{t('bandialog.yes_ban_user')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.header}>
|
||||
<h2>{t('bandialog.ban_user')}</h2>
|
||||
</div>
|
||||
<div className={styles.separator}>
|
||||
<h3>{t('bandialog.are_you_sure', username)}</h3>
|
||||
<i>{info}</i>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
className={cn(styles.cancel, 'talk-ban-user-dialog-button-cancel')}
|
||||
cStyle="cancel"
|
||||
onClick={onCancel}
|
||||
raised >
|
||||
{t('bandialog.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className={cn(styles.ban, 'talk-ban-user-dialog-button-confirm')}
|
||||
cStyle="black"
|
||||
onClick={onPerform}
|
||||
raised >
|
||||
{t('bandialog.yes_ban_user')}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,15 @@ function generateRegExp(phrases) {
|
||||
.join('[\\s"?!.]+')
|
||||
).join('|');
|
||||
|
||||
return new RegExp(`(^|[^\\w])(${inner})(?=[^\\w]|$)`, 'iu');
|
||||
const pattern = `(^|[^\\w])(${inner})(?=[^\\w]|$)`;
|
||||
try {
|
||||
return new RegExp(pattern, 'iu');
|
||||
}
|
||||
catch (_err) {
|
||||
|
||||
// IE does not support unicode support, so we'll create one without.
|
||||
return new RegExp(pattern, 'i');
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a regular expression detecting `suspectWords` and `bannedWords` phrases.
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
.root {
|
||||
min-height: 25px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.moreDetail {
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: black;
|
||||
right: 16px;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './CommentDetails.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty';
|
||||
|
||||
class CommentDetails extends Component {
|
||||
state = {
|
||||
showDetail: false
|
||||
};
|
||||
|
||||
constructor () {
|
||||
super();
|
||||
this.state = {
|
||||
showDetail: false
|
||||
};
|
||||
}
|
||||
|
||||
toggleDetail = () => {
|
||||
this.setState((state) => ({
|
||||
showDetail: !state.showDetail
|
||||
}));
|
||||
}
|
||||
|
||||
render() {
|
||||
const {data, root, comment} = this.props;
|
||||
const {showDetail} = this.state;
|
||||
const queryData = {
|
||||
root,
|
||||
comment,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<IfSlotIsNotEmpty
|
||||
queryData={queryData}
|
||||
slot={['adminCommentMoreDetails', 'adminCommentMoreFlagDetails']}
|
||||
>
|
||||
<a onClick={this.toggleDetail} className={styles.moreDetail}>
|
||||
{showDetail ? t('modqueue.less_detail') : t('modqueue.more_detail')}
|
||||
</a>
|
||||
</IfSlotIsNotEmpty>
|
||||
<Slot
|
||||
fill="adminCommentDetailArea"
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
more={showDetail}
|
||||
/>
|
||||
{showDetail && <Slot
|
||||
fill="adminCommentMoreDetails"
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CommentDetails.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
comment: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default CommentDetails;
|
||||
@@ -6,10 +6,21 @@ import FlagLabel from 'coral-ui/components/FlagLabel';
|
||||
import cn from 'classnames';
|
||||
import styles from './CommentLabels.css';
|
||||
|
||||
const staffRoles = ['ADMIN', 'STAFF', 'MODERATOR'];
|
||||
|
||||
function isUserFlagged(actions) {
|
||||
return actions.some((action) => action.__typename === 'FlagAction' && action.user);
|
||||
}
|
||||
|
||||
function getUserFlaggedType(actions) {
|
||||
return actions
|
||||
.some((action) =>
|
||||
action.__typename === 'FlagAction' &&
|
||||
action.user &&
|
||||
action.user.roles.some((role) => staffRoles.includes(role))
|
||||
) ? 'Staff' : 'User';
|
||||
}
|
||||
|
||||
function hasSuspectedWords(actions) {
|
||||
return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'Matched suspect word filter');
|
||||
}
|
||||
@@ -24,7 +35,7 @@ const CommentLabels = ({comment, comment: {className, status, actions, hasParent
|
||||
<div className={styles.coreLabels}>
|
||||
{hasParent && <Label iconName="reply" className={styles.replyLabel}>reply</Label>}
|
||||
{status === 'PREMOD' && <Label iconName="query_builder" className={styles.premodLabel}>Pre-Mod</Label>}
|
||||
{isUserFlagged(actions) && <FlagLabel iconName="person">User</FlagLabel>}
|
||||
{isUserFlagged(actions) && <FlagLabel iconName="person">{getUserFlaggedType(actions)}</FlagLabel>}
|
||||
{hasSuspectedWords(actions) && <FlagLabel iconName="sms_failed">Suspect</FlagLabel>}
|
||||
{hasHistoryFlag(actions) && <FlagLabel iconName="sentiment_very_dissatisfied">History</FlagLabel>}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
@custom-media --table-viewport (max-width: 1024px);
|
||||
|
||||
:global {
|
||||
.mdl-layout__drawer-button {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
@media (--table-viewport) {
|
||||
.mdl-layout__drawer-button {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-15
@@ -5,34 +5,31 @@ import {IndexLink, Link} from 'react-router';
|
||||
import styles from './Drawer.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
import cn from 'classnames';
|
||||
|
||||
const CoralDrawer = ({handleLogout, auth}) => (
|
||||
<Drawer className={styles.header}>
|
||||
const CoralDrawer = ({handleLogout, auth = {}}) => (
|
||||
<Drawer className={cn('talk-admin-drawer-nav', styles.drawer)}>
|
||||
{ auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
|
||||
<div>
|
||||
<Navigation className={styles.nav}>
|
||||
<IndexLink
|
||||
className={styles.navLink}
|
||||
to="/admin/dashboard"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.dashboard')}
|
||||
</IndexLink>
|
||||
{
|
||||
can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
<Link
|
||||
className={styles.navLink}
|
||||
<IndexLink
|
||||
className={cn('talk-admin-nav-moderate', styles.navLink)}
|
||||
to="/admin/moderate"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.moderate')}
|
||||
</Link>
|
||||
</IndexLink>
|
||||
)
|
||||
}
|
||||
<Link className={styles.navLink}
|
||||
<Link
|
||||
className={cn('talk-admin-nav-stories', styles.navLink)}
|
||||
to="/admin/stories"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.stories')}
|
||||
</Link>
|
||||
<Link className={styles.navLink}
|
||||
<Link
|
||||
className={cn('talk-admin-nav-community', styles.navLink)}
|
||||
to="/admin/community"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.community')}
|
||||
@@ -41,7 +38,7 @@ const CoralDrawer = ({handleLogout, auth}) => (
|
||||
can(auth.user, 'UPDATE_CONFIG') &&
|
||||
(
|
||||
<Link
|
||||
className={styles.navLink}
|
||||
className={cn('talk-admin-nav-configure', styles.navLink)}
|
||||
to="/admin/configure"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.configure')}
|
||||
@@ -57,7 +54,8 @@ const CoralDrawer = ({handleLogout, auth}) => (
|
||||
|
||||
CoralDrawer.propTypes = {
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
restricted: PropTypes.bool // hide app elements from a logged out user
|
||||
restricted: PropTypes.bool, // hide app elements from a logged out user
|
||||
auth: PropTypes.object
|
||||
};
|
||||
|
||||
export default CoralDrawer;
|
||||
@@ -1,83 +0,0 @@
|
||||
.flagBox {
|
||||
border-top: 1px solid rgba(66, 66, 66, 0.12);
|
||||
margin-top: 10px;
|
||||
.container {
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
.detail {
|
||||
padding: 0 20px 16px;
|
||||
ul {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
.moreDetail {
|
||||
float: right;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin-right: 10px;
|
||||
margin-top: 8px;
|
||||
color: black;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
i {
|
||||
vertical-align: middle;
|
||||
font-size: 12px;
|
||||
}
|
||||
ul {
|
||||
vertical-align: middle;
|
||||
list-style: none;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
vertical-align: middle;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
display: inline-block;
|
||||
margin-left: 7px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.lessDetail {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.subDetail {
|
||||
font-weight: normal;
|
||||
color: #888;
|
||||
|
||||
span {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
color: #393B44;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
padding: 2px 5px;
|
||||
border-radius: 2px;
|
||||
margin-left: -5px;
|
||||
transition: background-color 200ms ease;
|
||||
&:hover {
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Icon} from 'coral-ui';
|
||||
import styles from './FlagBox.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const shortReasons = {
|
||||
'This comment is offensive': t('modqueue.offensive'),
|
||||
'This looks like an ad/marketing': t('modqueue.spam_ads'),
|
||||
'This user is impersonating': t('modqueue.impersonating'),
|
||||
'I don\'t like this username': t('modqueue.dont_like_username'),
|
||||
'Other': t('modqueue.other')
|
||||
};
|
||||
|
||||
class FlagBox extends Component {
|
||||
constructor () {
|
||||
super();
|
||||
this.state = {
|
||||
showDetail: false
|
||||
};
|
||||
}
|
||||
|
||||
toggleDetail = () => {
|
||||
this.setState((state) => ({
|
||||
showDetail: !state.showDetail
|
||||
}));
|
||||
}
|
||||
|
||||
reasonMap = (reason) => {
|
||||
const shortReason = shortReasons[reason];
|
||||
|
||||
// if the short reason isn't found, just return the long one.
|
||||
return shortReason ? shortReason : reason;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {actionSummaries, actions, viewUserDetail} = this.props;
|
||||
const {showDetail} = this.state;
|
||||
|
||||
return (
|
||||
<div className={styles.flagBox}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<Icon name='flag'/><h3>{t('community.flags')} ({actionSummaries.length}):</h3>
|
||||
<ul>
|
||||
{actionSummaries.map((action, i) =>
|
||||
<li key={i} className={styles.lessDetail}> {this.reasonMap(action.reason)} (<strong>{action.count}</strong>)</li>
|
||||
)}
|
||||
</ul>
|
||||
<a onClick={this.toggleDetail} className={styles.moreDetail}>{showDetail ? t('modqueue.less_detail') : t('modqueue.more_detail')}</a>
|
||||
</div>
|
||||
{showDetail && (
|
||||
<div className={styles.detail}>
|
||||
<ul>
|
||||
{actionSummaries.map((summary, i) => {
|
||||
|
||||
const actionList = actions.filter((a) => a.reason === summary.reason);
|
||||
|
||||
return (
|
||||
<li key={i}>
|
||||
{this.reasonMap(summary.reason)} (<strong>{summary.count}</strong>)
|
||||
<ul>
|
||||
{actionList.map((action, j) =>
|
||||
<li key={`${i}_${j}`} className={styles.subDetail}>
|
||||
{action.user &&
|
||||
<a className={styles.username} onClick={() => viewUserDetail(action.user.id)}>
|
||||
{action.user.username}
|
||||
</a>
|
||||
}
|
||||
{action.message}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FlagBox.propTypes = {
|
||||
actionSummaries: PropTypes.arrayOf(PropTypes.shape({
|
||||
reason: PropTypes.string,
|
||||
count: PropTypes.number
|
||||
})).isRequired,
|
||||
actions: PropTypes.arrayOf(PropTypes.shape({
|
||||
message: PropTypes.string,
|
||||
user: PropTypes.shape({username: PropTypes.string})
|
||||
})).isRequired
|
||||
};
|
||||
|
||||
export default FlagBox;
|
||||
@@ -21,7 +21,6 @@
|
||||
|
||||
.callToAction {
|
||||
position: fixed;
|
||||
left: 10px;
|
||||
bottom: 10px;
|
||||
width: 280px;
|
||||
height: 200px;
|
||||
@@ -29,6 +28,7 @@
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.15);
|
||||
z-index: 10;
|
||||
|
||||
.ctaHeader {
|
||||
font-size: 16px;
|
||||
|
||||
@@ -4,32 +4,38 @@ import Modal from 'components/Modal';
|
||||
import styles from './ModerationKeysModal.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const shortcuts = [
|
||||
{
|
||||
title: 'modqueue.navigation',
|
||||
shortcuts: {
|
||||
'j': 'modqueue.next_comment',
|
||||
'k': 'modqueue.prev_comment',
|
||||
's': 'modqueue.singleview',
|
||||
'?': 'modqueue.thismenu'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'modqueue.actions',
|
||||
shortcuts: {
|
||||
'd': 'modqueue.approve',
|
||||
'f': 'modqueue.reject'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default class ModerationKeysModal extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
open: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
hideShortcutsNote: PropTypes.func.isRequired,
|
||||
shortcutsNoteVisible: PropTypes.string.isRequired
|
||||
shortcutsNoteVisible: PropTypes.string.isRequired,
|
||||
queueCount: PropTypes.number.isRequired
|
||||
}
|
||||
|
||||
buildShortcuts = () => {
|
||||
return [
|
||||
{
|
||||
title: 'modqueue.navigation',
|
||||
shortcuts: {
|
||||
'j': 'modqueue.next_comment',
|
||||
'k': 'modqueue.prev_comment',
|
||||
'ctrl+f': 'modqueue.toggle_search',
|
||||
't': 'modqueue.next_queue',
|
||||
[`1...${this.props.queueCount}`]: 'modqueue.jump_to_queue',
|
||||
's': 'modqueue.singleview',
|
||||
'?': 'modqueue.thismenu'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'modqueue.actions',
|
||||
shortcuts: {
|
||||
'd': 'modqueue.approve',
|
||||
'f': 'modqueue.reject'
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
render () {
|
||||
@@ -49,7 +55,7 @@ export default class ModerationKeysModal extends React.Component {
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<h3>{t('modqueue.shortcuts')}</h3>
|
||||
<div className={styles.container}>
|
||||
{shortcuts.map((shortcut, i) => (
|
||||
{this.buildShortcuts().map((shortcut, i) => (
|
||||
<table className={styles.table} key={i}>
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.list {
|
||||
padding: 8px 0;
|
||||
list-style: none;
|
||||
display: block;
|
||||
|
||||
&.singleView .listItem {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.singleView .listItem.activeItem {
|
||||
display: block;
|
||||
height: 100%;
|
||||
font-size: 1.5em;
|
||||
line-height: 1.5em;
|
||||
border: none;
|
||||
|
||||
.actions {
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
left: 25%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 50%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.listItem {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
max-width: 660px;
|
||||
min-width: 400px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 14px;
|
||||
position: relative;
|
||||
transition: box-shadow 200ms;
|
||||
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.sideActions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
padding: 40px 18px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.itemHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.author {
|
||||
min-width: 230px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.itemBody {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-right: 16px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: #757575;
|
||||
font-size: 40px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.created {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-top: 20px;
|
||||
flex: 1;
|
||||
font-size: 0.88em;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.flagged {
|
||||
color: rgba(255, 0, 0, .5);
|
||||
padding-top: 15px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.flagCount{
|
||||
font-size: 12px;
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: #444;
|
||||
margin-top: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@media (--big-viewport) {
|
||||
.listItem {
|
||||
border: 1px solid #e0e0e0;
|
||||
margin-bottom: 30px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
&.activeItem {
|
||||
border: 2px solid #333;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.hasLinks {
|
||||
color: #f00;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.banned {
|
||||
color: #f00;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.ban {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.banButton {
|
||||
width: 114px;
|
||||
letter-spacing: 1px;
|
||||
|
||||
i {
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.selected {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
|
||||
.actionButton {
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.minimal {
|
||||
width: 45px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './ModerationList.css';
|
||||
import key from 'keymaster';
|
||||
import Hammer from 'hammerjs';
|
||||
import Comment from './Comment';
|
||||
import User from './User';
|
||||
import SuspendUserModal from './SuspendUserModal';
|
||||
|
||||
// Each action has different meaning and configuration
|
||||
const menuOptionsMap = {
|
||||
'reject': {status: 'REJECTED', icon: 'close', key: 'f'},
|
||||
'approve': {status: 'ACCEPTED', icon: 'done', key: 'd'},
|
||||
'flag': {status: 'FLAGGED', icon: 'flag', filter: 'Untouched'},
|
||||
'ban': {status: 'BANNED', icon: 'not interested'}
|
||||
};
|
||||
|
||||
// Renders a comment list and allow performing actions
|
||||
export default class ModerationList extends React.Component {
|
||||
static propTypes = {
|
||||
isActive: PropTypes.bool,
|
||||
singleView: PropTypes.bool,
|
||||
commentIds: PropTypes.arrayOf(PropTypes.string),
|
||||
actionIds: PropTypes.arrayOf(PropTypes.string),
|
||||
comments: PropTypes.object,
|
||||
users: PropTypes.object.isRequired,
|
||||
actions: PropTypes.object,
|
||||
userStatusUpdate: PropTypes.func.isRequired,
|
||||
suspendUser: PropTypes.func.isRequired,
|
||||
|
||||
// list of actions (flags, etc) associated with the comments
|
||||
modActions: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
loading: PropTypes.bool,
|
||||
|
||||
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired
|
||||
}
|
||||
|
||||
state = {active: null, suspendUserModal: null, email: null};
|
||||
|
||||
// remove key handlers before leaving
|
||||
componentWillUnmount () {
|
||||
this.unbindKeyHandlers();
|
||||
}
|
||||
|
||||
// add key handlers and gestures
|
||||
componentDidMount () {
|
||||
this.bindKeyHandlers();
|
||||
|
||||
// this.bindGestures() // need to check whether we're on a mobile device or this throws an Error
|
||||
}
|
||||
|
||||
// If entering to singleview and no active, active is the first eleement
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.singleView && !this.state.active) {
|
||||
this.setState({active: nextProps.commentIds[0]});
|
||||
}
|
||||
}
|
||||
|
||||
// Add swipe to approve or reject
|
||||
bindGestures () {
|
||||
const {modActions} = this.props;
|
||||
this._hammer = new Hammer(this.base);
|
||||
this._hammer.get('swipe').set({direction: Hammer.DIRECTION_HORIZONTAL});
|
||||
|
||||
if (modActions.indexOf('reject') !== -1) {
|
||||
this._hammer.on('swipeleft', () => this.props.singleView && this.actionKeyHandler('Rejected'));
|
||||
}
|
||||
if (modActions.indexOf('approve') !== -1) {
|
||||
this._hammer.on('swiperight', () => this.props.singleView && this.actionKeyHandler('Approved'));
|
||||
}
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
key(menuOptionsMap[action].key, 'moderationList', () => isActive && this.actionKeyHandler(menuOptionsMap[action].status));
|
||||
});
|
||||
key('j', 'moderationList', () => isActive && this.moveKeyHandler('down'));
|
||||
key('k', 'moderationList', () => isActive && this.moveKeyHandler('up'));
|
||||
key.setScope('moderationList');
|
||||
}
|
||||
|
||||
// Perform an action using the keys only if the comment is active
|
||||
actionKeyHandler (action) {
|
||||
if (this.props.isActive && this.state.active) {
|
||||
this.onClickAction(action, this.state.active);
|
||||
}
|
||||
}
|
||||
|
||||
// move around with j/k
|
||||
moveKeyHandler (direction) {
|
||||
if (!this.props.isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {commentIds} = this.props;
|
||||
const {active} = this.state;
|
||||
|
||||
// check boundaries
|
||||
if (active === null || !commentIds.length) {
|
||||
this.setState({active: commentIds[0]});
|
||||
} else if (direction === 'up' && active !== commentIds[0]) {
|
||||
this.setState({active: commentIds[commentIds.indexOf(active) - 1]});
|
||||
} else if (direction === 'down' && active !== commentIds[commentIds.length - 1]) {
|
||||
this.setState({active: commentIds[commentIds.indexOf(active) + 1]});
|
||||
}
|
||||
|
||||
// scroll to the position
|
||||
const index = Math.max(commentIds.indexOf(this.state.active), 0);
|
||||
this.base.childNodes[index] && this.base.childNodes[index].focus();
|
||||
}
|
||||
|
||||
unbindKeyHandlers () {
|
||||
key.deleteScope('moderationList');
|
||||
}
|
||||
|
||||
// If we are performing an action over a comment (aka removing from the list) we need to select a new active.
|
||||
// TODO: In the future this can be improved and look at the actual state to
|
||||
// resolve since the content of the list could change externally. For now it works as expected
|
||||
onClickAction = (menuOption, id, action) => {
|
||||
|
||||
// activate the next comment
|
||||
if (id === this.state.active) {
|
||||
const moderationIds = this.getModerationIds();
|
||||
if (moderationIds[moderationIds.length - 1] === this.state.active) {
|
||||
this.setState({active: moderationIds[moderationIds.length - 2]});
|
||||
} else {
|
||||
this.setState({active: moderationIds[Math.min(moderationIds.indexOf(this.state.active) + 1, moderationIds.length - 1)]});
|
||||
}
|
||||
}
|
||||
|
||||
// Update the status right away if this is a comment
|
||||
if (action.item_type === 'COMMENTS') {
|
||||
this.props.updateCommentStatus(menuOption, id);
|
||||
} else if (action.item_type === 'USERS') {
|
||||
|
||||
// If a user bio or name is rejected, bring up a dialog before suspending them.
|
||||
if (menuOption === 'REJECTED') {
|
||||
this.setState({suspendUserModal: action});
|
||||
} else if (menuOption === 'ACCEPTED') {
|
||||
this.props.userStatusUpdate('APPROVED', action.item_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClickShowBanDialog = (userId, userName, commentId) => {
|
||||
this.props.onClickShowBanDialog(userId, userName, commentId);
|
||||
}
|
||||
|
||||
mapModItems = (itemId, index) => {
|
||||
|
||||
const {comments = {}, users, actions = {}, modActions, suspectWords, hideActive} = this.props;
|
||||
const {active} = this.state;
|
||||
|
||||
// Because ids are unique, the id will either appear as an action or as a comment.
|
||||
|
||||
const item = comments[itemId] || actions[itemId];
|
||||
let modItem;
|
||||
|
||||
if (item.body) {
|
||||
|
||||
// If the item is a comment...
|
||||
const author = users[item.author_id];
|
||||
modItem = <Comment
|
||||
suspectWords={suspectWords}
|
||||
comment={item}
|
||||
author={author}
|
||||
key={index}
|
||||
index={index}
|
||||
onClickAction={this.onClickAction}
|
||||
onClickShowBanDialog={this.onClickShowBanDialog}
|
||||
modActions={modActions}
|
||||
menuOptionsMap={menuOptionsMap}
|
||||
isActive={itemId === active}
|
||||
hideActive={hideActive} />;
|
||||
} else {
|
||||
|
||||
// If the item is an action...
|
||||
const user = users[item.item_id];
|
||||
modItem = user && <User
|
||||
suspectWords={suspectWords}
|
||||
action={item}
|
||||
user={user}
|
||||
key={index}
|
||||
index={index}
|
||||
onClickAction={this.onClickAction}
|
||||
onClickShowBanDialog={this.onClickShowBanDialog}
|
||||
modActions={modActions}
|
||||
menuOptionsMap={menuOptionsMap}
|
||||
isActive={itemId === active}
|
||||
hideActive={hideActive} />;
|
||||
}
|
||||
return modItem;
|
||||
}
|
||||
|
||||
getModerationIds = () => {
|
||||
const {commentIds = [], actionIds = [], comments, actions} = this.props;
|
||||
if (comments && actions) {
|
||||
return [ ...commentIds, ...actionIds ].sort((a, b) => {
|
||||
const itemA = comments[a] || actions[a];
|
||||
const itemB = comments[b] || actions[b];
|
||||
return itemB.updated_at - itemA.updated_at;
|
||||
});
|
||||
} else {
|
||||
return comments ? commentIds : actionIds;
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {singleView, key, suspendUser} = this.props;
|
||||
|
||||
// Combine moderations and actions into a single stream and sort by most recently updated.
|
||||
const moderationIds = this.getModerationIds();
|
||||
|
||||
return (
|
||||
<ul
|
||||
className={`${styles.list} ${singleView ? styles.singleView : ''}`} {...key}
|
||||
id='moderationList'>
|
||||
{moderationIds.map(this.mapModItems)}
|
||||
<SuspendUserModal
|
||||
action = {this.state.suspendUserModal}
|
||||
onClose={() => this.setState({suspendUserModal:null})}
|
||||
suspendUser={suspendUser} />
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
.root {
|
||||
display: block;
|
||||
color: #D03235;
|
||||
border: solid 1px #D03235;
|
||||
background: white;
|
||||
padding: 10px 11px;
|
||||
box-sizing: border-box;
|
||||
vertical-align: middle;
|
||||
line-height: 24px;
|
||||
font-size: 17px;
|
||||
height: 47px;
|
||||
border-radius: 3px;
|
||||
text-transform: capitalize;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
|
||||
width: 129px;
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: #D03235;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
color: white;
|
||||
background-color: #D03235;
|
||||
box-shadow: none;
|
||||
|
||||
&:hover {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.minimal {
|
||||
width: 45px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import cn from 'classnames';
|
||||
import styles from './RejectButton.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const RejectButton = ({active, minimal, onClick, className}) => {
|
||||
const text = active ? t('modqueue.rejected') : t('modqueue.reject');
|
||||
return (
|
||||
<button
|
||||
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active}, className)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon name={'close'} className={styles.icon} />
|
||||
{!minimal && text}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
RejectButton.propTypes = {
|
||||
className: PropTypes.string,
|
||||
active: PropTypes.bool,
|
||||
minimal: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default RejectButton;
|
||||
|
||||
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import {Dialog} from 'coral-ui';
|
||||
import {RadioGroup, Radio} from 'react-mdl';
|
||||
import styles from './SuspendUserDialog.css';
|
||||
import cn from 'classnames';
|
||||
|
||||
import Button from 'coral-ui/components/Button';
|
||||
|
||||
@@ -87,7 +88,7 @@ class SuspendUserDialog extends React.Component {
|
||||
<Button cStyle="white" className={styles.cancel} onClick={onCancel} raised>
|
||||
{t('suspenduser.cancel')}
|
||||
</Button>
|
||||
<Button cStyle="black" className={styles.perform} onClick={this.goToStep1} raised>
|
||||
<Button cStyle="black" className={cn(styles.perform, 'talk-admin-suspend-user-dialog-confirm')} onClick={this.goToStep1} raised>
|
||||
{t('suspenduser.suspend_user')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -120,7 +121,7 @@ class SuspendUserDialog extends React.Component {
|
||||
</Button>
|
||||
<Button
|
||||
cStyle="black"
|
||||
className={styles.perform}
|
||||
className={cn(styles.perform, 'talk-admin-suspend-user-dialog-send')}
|
||||
onClick={this.handlePerform}
|
||||
disabled={this.state.message.length === 0}
|
||||
raised
|
||||
@@ -137,7 +138,7 @@ class SuspendUserDialog extends React.Component {
|
||||
const {step} = this.state;
|
||||
return (
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
className={cn(styles.dialog, 'talk-admin-suspend-user-dialog')}
|
||||
onCancel={onCancel}
|
||||
open={open}
|
||||
>
|
||||
|
||||
@@ -2,17 +2,25 @@ import React from 'react';
|
||||
import TagsInput from 'react-tagsinput';
|
||||
import styles from './TagsInput.css';
|
||||
import AutosizeInput from 'react-input-autosize';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
|
||||
const autosizingRenderInput = ({onChange, value, addTag: _, ...other}) =>
|
||||
<AutosizeInput type='text' onChange={onChange} value={value} {...other} />;
|
||||
|
||||
export default (props) => {
|
||||
autosizingRenderInput.propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
addTag: PropTypes.func,
|
||||
};
|
||||
|
||||
const TagsInputComponent = ({className = '', ...props}) => {
|
||||
return (
|
||||
<TagsInput
|
||||
addOnBlur={true}
|
||||
addOnPaste={true}
|
||||
pasteSplit={(data) => data.split(',').map((d) => d.trim())}
|
||||
className={styles.root}
|
||||
className={cn(styles.root, 'tags-input', className)}
|
||||
focusedClassName={styles.rootFocus}
|
||||
renderInput={autosizingRenderInput}
|
||||
{...props}
|
||||
@@ -29,3 +37,11 @@ export default (props) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
TagsInputComponent.propTypes = {
|
||||
className: PropTypes.string,
|
||||
inputProps: PropTypes.object,
|
||||
tagProps: PropTypes.object,
|
||||
};
|
||||
|
||||
export default TagsInputComponent;
|
||||
|
||||
@@ -39,7 +39,8 @@
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.statItem, .statReportResult {
|
||||
.statItem,
|
||||
.statReportResult {
|
||||
padding: 3px 5px;
|
||||
background-color: #D8D8D8;
|
||||
border-radius: 3px;
|
||||
@@ -48,7 +49,7 @@
|
||||
font-size: 0.9em;
|
||||
line-height: normal;
|
||||
letter-spacing: 0.4px;
|
||||
min-width: 60px;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.statResult {
|
||||
@@ -94,12 +95,11 @@
|
||||
}
|
||||
|
||||
.commentStatuses {
|
||||
padding: 10px 0 0 0;
|
||||
padding: 0 0 0 10px;
|
||||
margin: 0;
|
||||
height: 52px;
|
||||
align-self: center;
|
||||
list-style: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
@@ -116,11 +116,11 @@
|
||||
.bulkActionGroup {
|
||||
height: 52px;
|
||||
background-color: #efefef;
|
||||
|
||||
padding: 0 0 0 10px;
|
||||
display: flex;
|
||||
i {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.bulkAction {
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
@@ -128,17 +128,39 @@
|
||||
transform: scale(.7);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bulkAction:last-child {
|
||||
margin-left: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
.loadMore > button {
|
||||
background-color: #696969;
|
||||
.selectedCommentsInfo {
|
||||
align-self: center;
|
||||
font-weight: 500;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.loadMore>button {
|
||||
background-color: #696969;
|
||||
&:hover {
|
||||
background-color: #404040;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.toggleAll {
|
||||
padding: 0 10px 0 0;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.commentList {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.bulkActionHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 52px;
|
||||
&.selected {
|
||||
background-color: #efefef;
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,17 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Comment from '../containers/UserDetailComment';
|
||||
import styles from './UserDetail.css';
|
||||
import {Icon, Button, Drawer, Spinner} from 'coral-ui';
|
||||
import {Icon, Drawer, Spinner} from 'coral-ui';
|
||||
import {Slot} from 'coral-framework/components';
|
||||
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
|
||||
import {actionsMap} from '../utils/moderationQueueActionsMap';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
import LoadMore from '../components/LoadMore';
|
||||
import cn from 'classnames';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import {getReliability} from 'coral-framework/utils/user';
|
||||
import ApproveButton from './ApproveButton';
|
||||
import RejectButton from './RejectButton';
|
||||
import {getErrorMessages} from 'coral-framework/utils';
|
||||
|
||||
export default class UserDetail extends React.Component {
|
||||
|
||||
@@ -18,14 +20,22 @@ export default class UserDetail extends React.Component {
|
||||
userId: PropTypes.string.isRequired,
|
||||
hideUserDetail: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
bannedWords: PropTypes.array.isRequired,
|
||||
suspectWords: PropTypes.array.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
changeStatus: PropTypes.func.isRequired,
|
||||
toggleSelect: PropTypes.func.isRequired,
|
||||
bulkAccept: PropTypes.func.isRequired,
|
||||
bulkReject: PropTypes.func.isRequired,
|
||||
toggleSelectAll: PropTypes.func.isRequired,
|
||||
loading: PropTypes.bool.isRequired,
|
||||
data: PropTypes.shape({
|
||||
refetch: PropTypes.func.isRequired,
|
||||
}),
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
selectedCommentIds: PropTypes.array.isRequired,
|
||||
viewUserDetail: PropTypes.any.isRequired,
|
||||
loadMore: PropTypes.any.isRequired,
|
||||
notify: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
rejectThenReload = async (info) => {
|
||||
@@ -34,8 +44,8 @@ export default class UserDetail extends React.Component {
|
||||
this.props.data.refetch();
|
||||
} catch (err) {
|
||||
|
||||
// TODO: handle error.
|
||||
console.error(err);
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,8 +55,30 @@ export default class UserDetail extends React.Component {
|
||||
this.props.data.refetch();
|
||||
} catch (err) {
|
||||
|
||||
// TODO: handle error.
|
||||
console.error(err);
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
}
|
||||
|
||||
bulkAcceptThenReload = async () => {
|
||||
try {
|
||||
await this.props.bulkAccept();
|
||||
this.props.data.refetch();
|
||||
} catch (err) {
|
||||
|
||||
console.error(err);
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
}
|
||||
|
||||
bulkRejectThenReload = async () => {
|
||||
try {
|
||||
await this.props.bulkReject();
|
||||
this.props.data.refetch();
|
||||
} catch (err) {
|
||||
|
||||
console.error(err);
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +102,7 @@ export default class UserDetail extends React.Component {
|
||||
|
||||
renderLoaded() {
|
||||
const {
|
||||
data,
|
||||
root,
|
||||
root: {
|
||||
user,
|
||||
@@ -79,14 +112,11 @@ export default class UserDetail extends React.Component {
|
||||
},
|
||||
activeTab,
|
||||
selectedCommentIds,
|
||||
bannedWords,
|
||||
suspectWords,
|
||||
toggleSelect,
|
||||
bulkAccept,
|
||||
bulkReject,
|
||||
hideUserDetail,
|
||||
viewUserDetail,
|
||||
loadMore,
|
||||
toggleSelectAll
|
||||
} = this.props;
|
||||
|
||||
let rejectedPercent = (rejectedComments / totalComments) * 100;
|
||||
@@ -104,14 +134,14 @@ export default class UserDetail extends React.Component {
|
||||
<div>
|
||||
<ul className={styles.userDetailList}>
|
||||
<li>
|
||||
<Icon name="assignment_ind"/>
|
||||
<Icon name="assignment_ind" />
|
||||
<span className={styles.userDetailItem}>Member Since:</span>
|
||||
{new Date(user.created_at).toLocaleString()}
|
||||
</li>
|
||||
|
||||
{user.profiles.map(({id}) =>
|
||||
<li key={id}>
|
||||
<Icon name="email"/>
|
||||
<Icon name="email" />
|
||||
<span className={styles.userDetailItem}>Email:</span>
|
||||
{id} <ButtonCopyToClipboard className={styles.copyButton} icon="content_copy" copyText={id} />
|
||||
</li>
|
||||
@@ -120,24 +150,22 @@ export default class UserDetail extends React.Component {
|
||||
|
||||
<ul className={styles.stats}>
|
||||
<li className={styles.stat}>
|
||||
<span className={styles.statItem}> Total Comments </span>
|
||||
<spam className={styles.statResult}> {totalComments} </spam>
|
||||
<span className={styles.statItem}>Total Comments</span>
|
||||
<span className={styles.statResult}>{totalComments}</span>
|
||||
</li>
|
||||
<li className={styles.stat}>
|
||||
<spam className={styles.statItem}> Reject Rate </spam>
|
||||
<spam className={styles.statResult}> {`${(rejectedPercent).toFixed(1)}%`} </spam>
|
||||
<span className={styles.statItem}>Reject Rate</span>
|
||||
<span className={styles.statResult}>
|
||||
{rejectedPercent.toFixed(1)}%
|
||||
</span>
|
||||
</li>
|
||||
<li className={styles.stat}>
|
||||
<spam className={styles.statItem}> Reports </spam>
|
||||
<spam className={cn(styles.statReportResult, styles[getReliability(user.reliable.flagger)])}>
|
||||
<span className={styles.statItem}>Reports</span>
|
||||
<span className={cn(styles.statReportResult, styles[getReliability(user.reliable.flagger)])}>
|
||||
{capitalize(getReliability(user.reliable.flagger))}
|
||||
</spam>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p className={styles.small}>
|
||||
Data represents the last six months of activity
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Slot
|
||||
@@ -145,47 +173,51 @@ export default class UserDetail extends React.Component {
|
||||
data={this.props.data}
|
||||
queryData={{root, user}}
|
||||
/>
|
||||
|
||||
<hr/>
|
||||
{
|
||||
selectedCommentIds.length === 0
|
||||
? (
|
||||
<ul className={styles.commentStatuses}>
|
||||
<li className={activeTab === 'all' ? styles.active : ''} onClick={this.showAll}>All</li>
|
||||
<li className={activeTab === 'rejected' ? styles.active : ''} onClick={this.showRejected}>Rejected</li>
|
||||
</ul>
|
||||
)
|
||||
: (
|
||||
<div className={styles.bulkActionGroup}>
|
||||
<Button
|
||||
onClick={bulkAccept}
|
||||
className={styles.bulkAction}
|
||||
cStyle='approve'
|
||||
icon='done'>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={bulkReject}
|
||||
className={styles.bulkAction}
|
||||
cStyle='reject'
|
||||
icon='close'>
|
||||
</Button>
|
||||
{`${selectedCommentIds.length} comments selected`}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div>
|
||||
<hr />
|
||||
<div className={(selectedCommentIds.length > 0) ? cn(styles.bulkActionHeader, styles.selected) : styles.bulkActionHeader}>
|
||||
{
|
||||
selectedCommentIds.length === 0
|
||||
? (
|
||||
<ul className={styles.commentStatuses}>
|
||||
<li className={activeTab === 'all' ? styles.active : ''} onClick={this.showAll}>All</li>
|
||||
<li className={activeTab === 'rejected' ? styles.active : ''} onClick={this.showRejected}>Rejected</li>
|
||||
</ul>
|
||||
)
|
||||
: (
|
||||
<div className={styles.bulkActionGroup}>
|
||||
<ApproveButton
|
||||
onClick={this.bulkAcceptThenReload}
|
||||
minimal
|
||||
/>
|
||||
<RejectButton
|
||||
onClick={this.bulkRejectThenReload}
|
||||
minimal
|
||||
/>
|
||||
<span className={styles.selectedCommentsInfo}> {selectedCommentIds.length} comments selected</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className={styles.toggleAll}>
|
||||
<input
|
||||
type='checkbox'
|
||||
id='toogleAll'
|
||||
checked={selectedCommentIds.length > 0 && selectedCommentIds.length === nodes.length}
|
||||
onChange={(e) => {
|
||||
toggleSelectAll(nodes.map((comment) => comment.id), e.target.checked);
|
||||
}} />
|
||||
<label htmlFor='toogleAll'>Select all</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.commentList}>
|
||||
{
|
||||
nodes.map((comment) => {
|
||||
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
|
||||
const selected = selectedCommentIds.indexOf(comment.id) !== -1;
|
||||
return <Comment
|
||||
key={comment.id}
|
||||
user={user}
|
||||
root={root}
|
||||
data={data}
|
||||
comment={comment}
|
||||
suspectWords={suspectWords}
|
||||
bannedWords={bannedWords}
|
||||
actions={actionsMap[status]}
|
||||
acceptComment={this.acceptThenReload}
|
||||
rejectComment={this.rejectThenReload}
|
||||
selected={selected}
|
||||
@@ -205,7 +237,7 @@ export default class UserDetail extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
if (this.props.loading) {
|
||||
return this.renderLoading();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.root {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
|
||||
@@ -3,37 +3,40 @@ import PropTypes from 'prop-types';
|
||||
import {Link} from 'react-router';
|
||||
|
||||
import {Icon} from 'coral-ui';
|
||||
import FlagBox from './FlagBox';
|
||||
import CommentDetails from './CommentDetails';
|
||||
import styles from './UserDetailComment.css';
|
||||
import {getActionSummary} from 'coral-framework/utils';
|
||||
import ActionButton from 'coral-admin/src/components/ActionButton';
|
||||
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
|
||||
import IfHasLink from 'coral-admin/src/components/IfHasLink';
|
||||
import cn from 'classnames';
|
||||
import CommentAnimatedEdit from './CommentAnimatedEdit';
|
||||
import CommentLabels from '../containers/CommentLabels';
|
||||
import ApproveButton from './ApproveButton';
|
||||
import RejectButton from 'coral-admin/src/components/RejectButton';
|
||||
|
||||
import t, {timeago} from 'coral-framework/services/i18n';
|
||||
|
||||
class UserDetailComment extends React.Component {
|
||||
|
||||
approve = () => (this.props.comment.status === 'ACCEPTED'
|
||||
? null
|
||||
: this.props.acceptComment({commentId: this.props.comment.id})
|
||||
);
|
||||
|
||||
reject = () => (this.props.comment.status === 'REJECTED'
|
||||
? null
|
||||
: this.props.rejectComment({commentId: this.props.comment.id})
|
||||
);
|
||||
|
||||
render() {
|
||||
const {
|
||||
actions = [],
|
||||
comment,
|
||||
viewUserDetail,
|
||||
suspectWords,
|
||||
bannedWords,
|
||||
selected,
|
||||
toggleSelect,
|
||||
className,
|
||||
user,
|
||||
...props
|
||||
data,
|
||||
root: {settings: {wordlist: {banned, suspect}}},
|
||||
} = this.props;
|
||||
|
||||
const flagActionSummaries = getActionSummary('FlagActionSummary', comment);
|
||||
const flagActions = comment.actions && comment.actions.filter((a) => a.__typename === 'FlagAction');
|
||||
|
||||
return (
|
||||
<li
|
||||
tabIndex={0}
|
||||
@@ -66,10 +69,10 @@ class UserDetailComment extends React.Component {
|
||||
</div>
|
||||
<CommentAnimatedEdit body={comment.body}>
|
||||
<div className={styles.bodyContainer}>
|
||||
<p className={styles.body}>
|
||||
<div className={styles.body}>
|
||||
<CommentBodyHighlighter
|
||||
suspectWords={suspectWords}
|
||||
bannedWords={bannedWords}
|
||||
suspectWords={suspect}
|
||||
bannedWords={banned}
|
||||
body={comment.body}
|
||||
/>
|
||||
{' '}
|
||||
@@ -80,7 +83,7 @@ class UserDetailComment extends React.Component {
|
||||
>
|
||||
<Icon name="open_in_new" /> {t('comment.view_context')}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.sideActions}>
|
||||
<IfHasLink text={comment.body}>
|
||||
<span className={styles.hasLinks}>
|
||||
@@ -88,41 +91,26 @@ class UserDetailComment extends React.Component {
|
||||
</span>
|
||||
</IfHasLink>
|
||||
<div className={styles.actions}>
|
||||
{actions.map((action, i) => {
|
||||
const active =
|
||||
(action === 'REJECT' && comment.status === 'REJECTED') ||
|
||||
(action === 'APPROVE' && comment.status === 'ACCEPTED');
|
||||
return (
|
||||
<ActionButton
|
||||
minimal={true}
|
||||
key={i}
|
||||
type={action}
|
||||
user={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}))}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<ApproveButton
|
||||
active={comment.status === 'ACCEPTED'}
|
||||
onClick={this.approve}
|
||||
minimal
|
||||
/>
|
||||
<RejectButton
|
||||
active={comment.status === 'REJECTED'}
|
||||
onClick={this.reject}
|
||||
minimal
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CommentAnimatedEdit>
|
||||
</div>
|
||||
{flagActions && flagActions.length
|
||||
? <FlagBox
|
||||
actions={flagActions}
|
||||
actionSummaries={flagActionSummaries}
|
||||
viewUserDetail={viewUserDetail}
|
||||
/>
|
||||
: null}
|
||||
<CommentDetails
|
||||
data={data}
|
||||
root={root}
|
||||
comment={comment}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -134,20 +122,27 @@ UserDetailComment.propTypes = {
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
className: PropTypes.string,
|
||||
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
toggleSelect: PropTypes.func,
|
||||
root: PropTypes.shape({
|
||||
settings: PropTypes.shape({
|
||||
wordlist: PropTypes.shape({
|
||||
suspect: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
banned: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
comment: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
body: PropTypes.string.isRequired,
|
||||
action_summaries: PropTypes.array,
|
||||
actions: PropTypes.array,
|
||||
created_at: PropTypes.string.isRequired,
|
||||
asset: PropTypes.shape({
|
||||
title: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
id: PropTypes.string
|
||||
})
|
||||
})
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
export default UserDetailComment;
|
||||
|
||||
@@ -15,20 +15,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
.headerWrapper {
|
||||
background-color: #696969;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
min-height: 58px;
|
||||
display: block;
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
background-color: #696969;
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
|
||||
}
|
||||
|
||||
.header > div {
|
||||
background-color: #696969;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
min-width: 1280px;
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
|
||||
height: 58px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Navigation, Header, IconButton, MenuItem, Menu} from 'react-mdl';
|
||||
import {Link, IndexLink} from 'react-router';
|
||||
@@ -6,101 +7,105 @@ import styles from './Header.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {Logo} from './Logo';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
import Indicator from './Indicator';
|
||||
|
||||
const CoralHeader = ({
|
||||
handleLogout,
|
||||
showShortcuts = () => {},
|
||||
auth
|
||||
}) => (
|
||||
<Header className={styles.header}>
|
||||
<Logo className={styles.logo} />
|
||||
<div>
|
||||
{
|
||||
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
|
||||
<Navigation className={styles.nav}>
|
||||
<IndexLink
|
||||
id='dashboardNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/dashboard"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.dashboard')}
|
||||
</IndexLink>
|
||||
{
|
||||
can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
auth,
|
||||
root
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.headerWrapper}>
|
||||
<Header className={styles.header}>
|
||||
<Logo className={styles.logo} />
|
||||
<div>
|
||||
{
|
||||
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
|
||||
<Navigation className={styles.nav}>
|
||||
{
|
||||
can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
<IndexLink
|
||||
id='moderateNav'
|
||||
className={cn('talk-admin-nav-moderate', styles.navLink)}
|
||||
to="/admin/moderate"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.moderate')}
|
||||
{(root.premodCount !== 0 || root.reportedCount !== 0) && <Indicator />}
|
||||
</IndexLink>
|
||||
)
|
||||
}
|
||||
<Link
|
||||
id='moderateNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/moderate"
|
||||
id='storiesNav'
|
||||
className={cn('talk-admin-nav-stories', styles.navLink)}
|
||||
to="/admin/stories"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.moderate')}
|
||||
{t('configure.stories')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
<Link
|
||||
id='streamsNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/stories"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.stories')}
|
||||
</Link>
|
||||
<Link
|
||||
id='communityNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/community"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.community')}
|
||||
</Link>
|
||||
{
|
||||
can(auth.user, 'UPDATE_CONFIG') && (
|
||||
|
||||
<Link
|
||||
id='configureNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/configure"
|
||||
id='communityNav'
|
||||
className={cn('talk-admin-nav-community', styles.navLink)}
|
||||
to="/admin/community"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.configure')}
|
||||
{t('configure.community')}
|
||||
{root.flaggedUsernamesCount !== 0 && <Indicator />}
|
||||
</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={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://github.com/coralproject/talk/releases" target="_blank" rel="noopener noreferrer">
|
||||
View latest version
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://coralproject.net/contribute.html#other-ideas-and-bug-reports" target="_blank" rel="noopener noreferrer">
|
||||
Report a bug or give feedback
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleLogout}>
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
{`v${process.env.VERSION}`}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{
|
||||
can(auth.user, 'UPDATE_CONFIG') && (
|
||||
<Link
|
||||
id='configureNav'
|
||||
className={cn('talk-admin-nav-configure', styles.navLink)}
|
||||
to="/admin/configure"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.configure')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
</Navigation>
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className={styles.rightPanel}>
|
||||
<ul>
|
||||
<li className={cn(styles.settings, 'talk-admin-header-settings')}>
|
||||
<div>
|
||||
<IconButton name="settings" id="menu-settings" className="talk-admin-header-settings-button"/>
|
||||
<Menu target="menu-settings" align="right">
|
||||
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://github.com/coralproject/talk/releases" target="_blank" rel="noopener noreferrer">
|
||||
View latest version
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://support.coralproject.net" target="_blank" rel="noopener noreferrer">
|
||||
Report a bug or give feedback
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleLogout} className="talk-admin-header-sign-out">
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
{`v${process.env.VERSION}`}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Header>
|
||||
</div>
|
||||
</Header>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
CoralHeader.propTypes = {
|
||||
auth: PropTypes.object,
|
||||
showShortcuts: PropTypes.func,
|
||||
handleLogout: PropTypes.func.isRequired
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default CoralHeader;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
.indicator {
|
||||
background-color: #E46D59;
|
||||
border-radius: 10px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin-top: -4px;
|
||||
margin-left: 7px;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import styles from './Indicator.css';
|
||||
|
||||
const Indicator = () =>
|
||||
<span className={styles.indicator}></span>;
|
||||
|
||||
export default Indicator;
|
||||
@@ -1,5 +1,4 @@
|
||||
.layout {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
background-color: #FAFAFA;
|
||||
margin: 0 auto;
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Layout as LayoutMDL} from 'react-mdl';
|
||||
import Header from './Header';
|
||||
import Drawer from './Drawer';
|
||||
import Header from '../../containers/Header';
|
||||
import Drawer from '../Drawer';
|
||||
import styles from './Layout.css';
|
||||
|
||||
const Layout = ({
|
||||
children,
|
||||
handleLogout = () => {},
|
||||
toggleShortcutModal,
|
||||
toggleShortcutModal = () => {},
|
||||
restricted = false,
|
||||
...props}) => (
|
||||
auth,
|
||||
}) => (
|
||||
<LayoutMDL className={styles.layout} fixedDrawer>
|
||||
<Header
|
||||
handleLogout={handleLogout}
|
||||
showShortcuts={toggleShortcutModal}
|
||||
{...props} />
|
||||
<Drawer handleLogout={handleLogout} restricted={restricted} {...props} />
|
||||
auth={auth}
|
||||
/>
|
||||
<Drawer
|
||||
handleLogout={handleLogout}
|
||||
restricted={restricted}
|
||||
auth={auth}
|
||||
/>
|
||||
<div className={styles.layout}>
|
||||
{children}
|
||||
</div>
|
||||
@@ -24,6 +30,8 @@ const Layout = ({
|
||||
);
|
||||
|
||||
Layout.propTypes = {
|
||||
children: PropTypes.node,
|
||||
auth: PropTypes.object,
|
||||
handleLogout: PropTypes.func,
|
||||
toggleShortcutModal: PropTypes.func,
|
||||
restricted: PropTypes.bool // hide elements from a user that's logged out
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
background: #696969;
|
||||
height: 100%;
|
||||
width: 128px;
|
||||
z-index: 10;
|
||||
border-right: 1px #757575 solid;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import styles from './Logo.css';
|
||||
import {CoralLogo} from 'coral-ui';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const Logo = ({className = ''}) => (
|
||||
<div className={`${styles.logo} ${className}`}>
|
||||
@@ -10,3 +11,7 @@ export const Logo = ({className = ''}) => (
|
||||
</h1>
|
||||
</div>
|
||||
);
|
||||
|
||||
Logo.propTypes = {
|
||||
className: PropTypes.string
|
||||
};
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export const FETCH_ASSETS_REQUEST = 'FETCH_ASSETS_REQUEST';
|
||||
export const FETCH_ASSETS_SUCCESS = 'FETCH_ASSETS_SUCCESS';
|
||||
export const FETCH_ASSETS_FAILURE = 'FETCH_ASSETS_FAILURE';
|
||||
|
||||
export const UPDATE_ASSET_STATE_REQUEST = 'UPDATE_ASSET_STATE_REQUEST';
|
||||
export const UPDATE_ASSET_STATE_SUCCESS = 'UPDATE_ASSET_STATE_SUCCESS';
|
||||
export const UPDATE_ASSET_STATE_FAILURE = 'UPDATE_ASSET_STATE_FAILURE';
|
||||
|
||||
export const UPDATE_ASSETS = 'UPDATE_ASSETS';
|
||||
@@ -1,17 +1,22 @@
|
||||
export const FETCH_COMMENTERS_REQUEST = 'FETCH_COMMENTERS_REQUEST';
|
||||
export const FETCH_COMMENTERS_SUCCESS = 'FETCH_COMMENTERS_SUCCESS';
|
||||
export const FETCH_COMMENTERS_FAILURE = 'FETCH_COMMENTERS_FAILURE';
|
||||
export const SORT_UPDATE = 'SORT_UPDATE';
|
||||
export const COMMENTERS_NEW_PAGE = 'COMMENTERS_NEW_PAGE';
|
||||
export const SET_ROLE = 'SET_ROLE';
|
||||
export const SET_COMMENTER_STATUS = 'SET_COMMENTER_STATUS';
|
||||
const prefix = 'COMMUNITY';
|
||||
|
||||
export const FETCH_FLAGGED_COMMENTERS_REQUEST = 'FETCH_FLAGGED_COMMENTERS_REQUEST';
|
||||
export const FETCH_FLAGGED_COMMENTERS_SUCCESS = 'FETCH_FLAGGED_COMMENTERS_SUCCESS';
|
||||
export const FETCH_FLAGGED_COMMENTERS_FAILURE = 'FETCH_FLAGGED_COMMENTERS_FAILURE';
|
||||
export const FETCH_USERS_REQUEST = `${prefix}_FETCH_USERS_REQUEST`;
|
||||
export const FETCH_USERS_SUCCESS = `${prefix}_FETCH_USERS_SUCCESS`;
|
||||
export const FETCH_USERS_FAILURE = `${prefix}_FETCH_USERS_FAILURE`;
|
||||
|
||||
export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG';
|
||||
export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG';
|
||||
export const SORT_UPDATE = `${prefix}_SORT_UPDATE`;
|
||||
export const SET_PAGE = `${prefix}_SET_PAGE`;
|
||||
export const SET_ROLE = `${prefix}_SET_ROLE`;
|
||||
export const SET_COMMENTER_STATUS = `${prefix}_SET_COMMENTER_STATUS`;
|
||||
|
||||
export const SHOW_REJECT_USERNAME_DIALOG = 'SHOW_REJECT_USERNAME_DIALOG';
|
||||
export const HIDE_REJECT_USERNAME_DIALOG = 'HIDE_REJECT_USERNAME_DIALOG';
|
||||
export const FETCH_FLAGGED_COMMENTERS_REQUEST = `${prefix}_FETCH_FLAGGED_COMMENTERS_REQUEST`;
|
||||
export const FETCH_FLAGGED_COMMENTERS_SUCCESS = `${prefix}_FETCH_FLAGGED_COMMENTERS_SUCCESS`;
|
||||
export const FETCH_FLAGGED_COMMENTERS_FAILURE = `${prefix}_FETCH_FLAGGED_COMMENTERS_FAILURE`;
|
||||
|
||||
export const SHOW_BANUSER_DIALOG = `${prefix}_SHOW_BANUSER_DIALOG`;
|
||||
export const HIDE_BANUSER_DIALOG = `${prefix}_HIDE_BANUSER_DIALOG`;
|
||||
|
||||
export const SHOW_REJECT_USERNAME_DIALOG = `${prefix}_SHOW_REJECT_USERNAME_DIALOG`;
|
||||
export const HIDE_REJECT_USERNAME_DIALOG = `${prefix}_HIDE_REJECT_USERNAME_DIALOG`;
|
||||
|
||||
export const SET_SEARCH_VALUE = `${prefix}_SET_SEARCH_VALUE`;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
const prefix = 'TALK_ADMIN_CONFIGURE';
|
||||
|
||||
export const UPDATE_PENDING = `${prefix}_UPDATE_PENDING`;
|
||||
export const CLEAR_PENDING = `${prefix}_CLEAR_PENDING`;
|
||||
export const SET_ACTIVE_SECTION = `${prefix}_SET_ACTIVE_SECTION`;
|
||||
@@ -0,0 +1,15 @@
|
||||
const prefix = 'STORIES';
|
||||
|
||||
export const FETCH_ASSETS_REQUEST = `${prefix}_FETCH_ASSETS_REQUEST`;
|
||||
export const FETCH_ASSETS_SUCCESS = `${prefix}_FETCH_ASSETS_SUCCESS`;
|
||||
export const FETCH_ASSETS_FAILURE = `${prefix}_FETCH_ASSETS_FAILURE`;
|
||||
|
||||
export const UPDATE_ASSET_STATE_REQUEST = `${prefix}_UPDATE_ASSET_STATE_REQUEST`;
|
||||
export const UPDATE_ASSET_STATE_SUCCESS = `${prefix}_UPDATE_ASSET_STATE_SUCCESS`;
|
||||
export const UPDATE_ASSET_STATE_FAILURE = `${prefix}_UPDATE_ASSET_STATE_FAILURE`;
|
||||
|
||||
export const UPDATE_ASSETS = `${prefix}_UPDATE_ASSETS`;
|
||||
|
||||
export const SET_PAGE = `${prefix}_SET_PAGE`;
|
||||
export const SET_SEARCH_VALUE = `${prefix}_SET_SEARCH_VALUE`;
|
||||
export const SET_CRITERIA = `${prefix}_SET_CRITERIA`;
|
||||
@@ -4,4 +4,4 @@ export const CHANGE_USER_DETAIL_STATUSES = 'CHANGE_USER_DETAIL_STATUSES';
|
||||
export const SELECT_USER_DETAIL_COMMENT = 'SELECT_USER_DETAIL_COMMENT';
|
||||
export const UNSELECT_USER_DETAIL_COMMENT = 'UNSELECT_USER_DETAIL_COMMENT';
|
||||
export const CLEAR_USER_DETAIL_SELECTIONS = 'CLEAR_USER_DETAIL_SELECTIONS';
|
||||
|
||||
export const SELECT_ALL_USER_DETAIL_COMMENT = 'SELECT_ALL_USER_DETAIL_COMMENT';
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import {gql} from 'react-apollo';
|
||||
import CommentDetails from '../components/CommentDetails';
|
||||
import {getSlotFragmentSpreads} from 'coral-framework/utils';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
|
||||
const slots = [
|
||||
'adminCommentDetailArea',
|
||||
'adminCommentMoreDetails',
|
||||
];
|
||||
|
||||
export default withFragments({
|
||||
root: gql`
|
||||
fragment CoralAdmin_CommentDetails_root on RootQuery {
|
||||
__typename
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
}
|
||||
`,
|
||||
comment: gql`
|
||||
fragment CoralAdmin_CommentDetails_comment on Comment {
|
||||
__typename
|
||||
${getSlotFragmentSpreads(slots, 'comment')}
|
||||
}
|
||||
`
|
||||
})(CommentDetails);
|
||||
@@ -8,6 +8,12 @@ const slots = [
|
||||
];
|
||||
|
||||
export default withFragments({
|
||||
root: gql`
|
||||
fragment CoralAdmin_CommentLabels_root on RootQuery {
|
||||
__typename
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
}
|
||||
`,
|
||||
comment: gql`
|
||||
fragment CoralAdmin_CommentLabels_comment on Comment {
|
||||
hasParent
|
||||
@@ -19,6 +25,7 @@ export default withFragments({
|
||||
}
|
||||
user {
|
||||
id
|
||||
roles
|
||||
}
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'comment')}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import {gql} from 'react-apollo';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import Header from '../components/ui/Header';
|
||||
|
||||
export default withQuery(gql`
|
||||
query TalkAdmin_Header {
|
||||
__typename
|
||||
premodCount: commentCount(query: {
|
||||
statuses: [PREMOD]
|
||||
})
|
||||
reportedCount: commentCount(query: {
|
||||
statuses: [NONE, PREMOD, SYSTEM_WITHHELD],
|
||||
action_type: FLAG
|
||||
})
|
||||
flaggedUsernamesCount: userCount(query: {
|
||||
action_type: FLAG,
|
||||
statuses: [PENDING]
|
||||
})
|
||||
}
|
||||
`, {
|
||||
options: {
|
||||
pollInterval: 10000
|
||||
}
|
||||
})(Header);
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, {Component} from 'react';
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import Layout from '../components/ui/Layout';
|
||||
import {fetchConfig} from '../actions/config';
|
||||
import AdminLogin from '../components/AdminLogin';
|
||||
@@ -10,15 +11,18 @@ import {toggleModal as toggleShortcutModal} from '../actions/moderation';
|
||||
import {checkLogin, handleLogin, requestPasswordReset, logout} from '../actions/auth';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
import UserDetail from 'coral-admin/src/containers/UserDetail';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class LayoutContainer extends Component {
|
||||
class LayoutContainer extends React.Component {
|
||||
componentWillMount() {
|
||||
const {checkLogin, fetchConfig} = this.props;
|
||||
|
||||
checkLogin();
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const {
|
||||
user,
|
||||
loggedIn,
|
||||
@@ -29,13 +33,16 @@ class LayoutContainer extends Component {
|
||||
} = this.props.auth;
|
||||
|
||||
const {
|
||||
handleLogout,
|
||||
children,
|
||||
logout,
|
||||
toggleShortcutModal,
|
||||
TALK_RECAPTCHA_PUBLIC
|
||||
TALK_RECAPTCHA_PUBLIC,
|
||||
} = this.props;
|
||||
|
||||
if (loadingUser) {
|
||||
return <FullLoading />;
|
||||
}
|
||||
|
||||
if (!loggedIn) {
|
||||
return (
|
||||
<AdminLogin
|
||||
@@ -48,17 +55,17 @@ class LayoutContainer extends Component {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (can(user, 'ACCESS_ADMIN') && loggedIn) {
|
||||
return (
|
||||
<Layout
|
||||
handleLogout={handleLogout}
|
||||
handleLogout={logout}
|
||||
toggleShortcutModal={toggleShortcutModal}
|
||||
{...this.props}
|
||||
>
|
||||
auth={this.props.auth} >
|
||||
<BanUserDialog />
|
||||
<SuspendUserDialog />
|
||||
<UserDetail />
|
||||
{this.props.children}
|
||||
{children}
|
||||
</Layout>
|
||||
);
|
||||
} else if (loggedIn) {
|
||||
@@ -72,19 +79,32 @@ class LayoutContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
LayoutContainer.propTypes = {
|
||||
children: PropTypes.node,
|
||||
requestPasswordReset: PropTypes.func,
|
||||
handleLogin: PropTypes.func,
|
||||
auth: PropTypes.object,
|
||||
handleLogout: PropTypes.func,
|
||||
logout: PropTypes.func,
|
||||
toggleShortcutModal: PropTypes.func,
|
||||
TALK_RECAPTCHA_PUBLIC: PropTypes.string,
|
||||
checkLogin: PropTypes.func,
|
||||
fetchConfig: PropTypes.func
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
auth: state.auth,
|
||||
TALK_RECAPTCHA_PUBLIC: state.config.data.TALK_RECAPTCHA_PUBLIC,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
checkLogin: () => dispatch(checkLogin()),
|
||||
fetchConfig: () => dispatch(fetchConfig()),
|
||||
handleLogin: (username, password, recaptchaResponse) =>
|
||||
dispatch(handleLogin(username, password, recaptchaResponse)),
|
||||
requestPasswordReset: (email) => dispatch(requestPasswordReset(email)),
|
||||
toggleShortcutModal: (toggle) => dispatch(toggleShortcutModal(toggle)),
|
||||
handleLogout: () => dispatch(logout())
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
checkLogin,
|
||||
fetchConfig,
|
||||
handleLogin,
|
||||
requestPasswordReset,
|
||||
toggleShortcutModal,
|
||||
logout
|
||||
}, dispatch);
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import SuspendUserDialog from '../components/SuspendUserDialog';
|
||||
@@ -44,6 +45,12 @@ class SuspendUserDialogContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
SuspendUserDialogContainer.propTypes = {
|
||||
open: PropTypes.bool,
|
||||
hideSuspendUserDialog: PropTypes.func,
|
||||
username: PropTypes.string,
|
||||
};
|
||||
|
||||
const withOrganizationName = withQuery(gql`
|
||||
query CoralAdmin_SuspendUserDialog {
|
||||
__typename
|
||||
|
||||
@@ -11,13 +11,15 @@ import {
|
||||
changeUserDetailStatuses,
|
||||
clearUserDetailSelections,
|
||||
toggleSelectCommentInUserDetail,
|
||||
toggleSelectAllCommentInUserDetail
|
||||
} from 'coral-admin/src/actions/userDetail';
|
||||
import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
|
||||
import UserDetailComment from './UserDetailComment';
|
||||
import update from 'immutability-helper';
|
||||
import {notify} from 'coral-framework/actions/notification';
|
||||
|
||||
const commentConnectionFragment = gql`
|
||||
fragment CoralAdmin_Moderation_CommentConnection on CommentConnection {
|
||||
fragment CoralAdmin_UserDetail_CommentConnection on CommentConnection {
|
||||
nodes {
|
||||
...${getDefinitionName(UserDetailComment.fragments.comment)}
|
||||
}
|
||||
@@ -41,22 +43,16 @@ class UserDetailContainer extends React.Component {
|
||||
return this.props.setCommentStatus({commentId, status});
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(changes);
|
||||
this.props.clearUserDetailSelections(); // un-select everything
|
||||
} catch (err) {
|
||||
|
||||
// TODO: handle error.
|
||||
console.error(err);
|
||||
}
|
||||
await Promise.all(changes);
|
||||
this.props.clearUserDetailSelections(); // un-select everything
|
||||
}
|
||||
|
||||
bulkReject = () => {
|
||||
this.bulkSetCommentStatus('REJECTED');
|
||||
return this.bulkSetCommentStatus('REJECTED');
|
||||
}
|
||||
|
||||
bulkAccept = () => {
|
||||
this.bulkSetCommentStatus('ACCEPTED');
|
||||
return this.bulkSetCommentStatus('ACCEPTED');
|
||||
}
|
||||
|
||||
acceptComment = ({commentId}) => {
|
||||
@@ -120,6 +116,7 @@ class UserDetailContainer extends React.Component {
|
||||
bulkAccept={this.bulkAccept}
|
||||
changeStatus={this.props.changeUserDetailStatuses}
|
||||
toggleSelect={this.props.toggleSelectCommentInUserDetail}
|
||||
toggleSelectAll={this.props.toggleSelectAllCommentInUserDetail}
|
||||
acceptComment={this.acceptComment}
|
||||
rejectComment={this.rejectComment}
|
||||
loading={loading}
|
||||
@@ -131,7 +128,7 @@ class UserDetailContainer extends React.Component {
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $author_id: ID!, $statuses: [COMMENT_STATUS!]) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, author_id: $author_id, statuses: $statuses}) {
|
||||
...CoralAdmin_Moderation_CommentConnection
|
||||
...CoralAdmin_UserDetail_CommentConnection
|
||||
}
|
||||
}
|
||||
${commentConnectionFragment}
|
||||
@@ -152,21 +149,24 @@ export const withUserDetailQuery = withQuery(gql`
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'user')}
|
||||
}
|
||||
totalComments: commentCount(query: {author_id: $author_id})
|
||||
totalComments: commentCount(query: {author_id: $author_id, statuses: []})
|
||||
rejectedComments: commentCount(query: {author_id: $author_id, statuses: [REJECTED]})
|
||||
comments: comments(query: {
|
||||
author_id: $author_id,
|
||||
statuses: $statuses
|
||||
}) {
|
||||
...CoralAdmin_Moderation_CommentConnection
|
||||
...CoralAdmin_UserDetail_CommentConnection
|
||||
}
|
||||
...${getDefinitionName(UserDetailComment.fragments.root)}
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
}
|
||||
${UserDetailComment.fragments.root}
|
||||
${commentConnectionFragment}
|
||||
`, {
|
||||
options: ({userId, statuses}) => {
|
||||
return {
|
||||
variables: {author_id: userId, statuses}
|
||||
variables: {author_id: userId, statuses},
|
||||
fetchPolicy: 'network-only',
|
||||
};
|
||||
},
|
||||
skip: (ownProps) => !ownProps.userId,
|
||||
@@ -177,8 +177,6 @@ const mapStateToProps = (state) => ({
|
||||
selectedCommentIds: state.userDetail.selectedCommentIds,
|
||||
statuses: state.userDetail.statuses,
|
||||
activeTab: state.userDetail.activeTab,
|
||||
bannedWords: state.settings.wordlist.banned,
|
||||
suspectWords: state.settings.wordlist.suspect,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
@@ -188,6 +186,8 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
toggleSelectCommentInUserDetail,
|
||||
viewUserDetail,
|
||||
hideUserDetail,
|
||||
toggleSelectAllCommentInUserDetail,
|
||||
notify
|
||||
}, dispatch)
|
||||
});
|
||||
|
||||
|
||||
@@ -3,10 +3,25 @@ import UserDetailComment from '../components/UserDetailComment';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
import {getDefinitionName} from 'coral-framework/utils';
|
||||
import CommentLabels from './CommentLabels';
|
||||
import CommentDetails from './CommentDetails';
|
||||
|
||||
export default withFragments({
|
||||
root: gql`
|
||||
fragment CoralAdmin_UserDetailComment_root on RootQuery {
|
||||
settings {
|
||||
wordlist {
|
||||
banned
|
||||
suspect
|
||||
}
|
||||
}
|
||||
...${getDefinitionName(CommentLabels.fragments.root)}
|
||||
...${getDefinitionName(CommentDetails.fragments.root)}
|
||||
}
|
||||
${CommentLabels.fragments.root}
|
||||
${CommentDetails.fragments.root}
|
||||
`,
|
||||
comment: gql`
|
||||
fragment CoralAdmin_UserDetail_comment on Comment {
|
||||
fragment CoralAdmin_UserDetailComment_comment on Comment {
|
||||
id
|
||||
body
|
||||
created_at
|
||||
@@ -17,28 +32,13 @@ export default withFragments({
|
||||
title
|
||||
url
|
||||
}
|
||||
action_summaries {
|
||||
count
|
||||
... on FlagActionSummary {
|
||||
reason
|
||||
}
|
||||
}
|
||||
actions {
|
||||
... on FlagAction {
|
||||
id
|
||||
reason
|
||||
message
|
||||
user {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
editing {
|
||||
edited
|
||||
}
|
||||
...${getDefinitionName(CommentLabels.fragments.comment)}
|
||||
...${getDefinitionName(CommentDetails.fragments.comment)}
|
||||
}
|
||||
${CommentLabels.fragments.comment}
|
||||
${CommentDetails.fragments.comment}
|
||||
`
|
||||
})(UserDetailComment);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import update from 'immutability-helper';
|
||||
import {mapLeaves} from 'coral-framework/utils';
|
||||
|
||||
export default {
|
||||
mutations: {
|
||||
SetUserStatus: ({variables: {status, userId}}) => ({
|
||||
updateQueries: {
|
||||
TalkAdmin_FlaggedAccounts: (prev) => {
|
||||
TalkAdmin_Community: (prev) => {
|
||||
if (status !== 'APPROVED') {
|
||||
return prev;
|
||||
}
|
||||
@@ -19,7 +20,7 @@ export default {
|
||||
}),
|
||||
RejectUsername: ({variables: {input: {id: userId}}}) => ({
|
||||
updateQueries: {
|
||||
TalkAdmin_FlaggedAccounts: (prev) => {
|
||||
TalkAdmin_Community: (prev) => {
|
||||
const updated = update(prev, {
|
||||
users: {
|
||||
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)},
|
||||
@@ -29,6 +30,16 @@ export default {
|
||||
}
|
||||
}
|
||||
}),
|
||||
UpdateSettings: ({variables: {input}}) => ({
|
||||
updateQueries: {
|
||||
TalkAdmin_Configure: (prev) => {
|
||||
const updated = update(prev, {
|
||||
settings: mapLeaves(input, (leaf) => ({$set: leaf})),
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ function init({store, storage}) {
|
||||
async function main() {
|
||||
const notification = createNotificationService(toast);
|
||||
const context = await createContext({reducers, graphqlExtension, pluginsConfig, notification, init});
|
||||
|
||||
render(
|
||||
<TalkProvider {...context}>
|
||||
<App />
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import * as actions from '../constants/assets';
|
||||
import update from 'immutability-helper';
|
||||
|
||||
const initialState = {
|
||||
byId: {},
|
||||
ids: [],
|
||||
assets: []
|
||||
};
|
||||
|
||||
export default function assets (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.FETCH_ASSETS_SUCCESS: {
|
||||
const assets = action.assets.reduce((prev, curr) => {
|
||||
prev[curr.id] = curr;
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
return update(state, {
|
||||
byId: {$set: assets},
|
||||
count: {$set: action.count},
|
||||
ids: {$set: Object.keys(assets)},
|
||||
});
|
||||
}
|
||||
case actions.UPDATE_ASSET_STATE_REQUEST:
|
||||
return update(state, {
|
||||
byId: {
|
||||
[action.id]: {
|
||||
closedAt: {$set: action.closedAt},
|
||||
},
|
||||
},
|
||||
});
|
||||
case actions.UPDATE_ASSETS:
|
||||
return update(state, {
|
||||
assets: {$set: action.assets},
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import {
|
||||
FETCH_COMMENTERS_REQUEST,
|
||||
FETCH_COMMENTERS_FAILURE,
|
||||
FETCH_COMMENTERS_SUCCESS,
|
||||
FETCH_USERS_REQUEST,
|
||||
FETCH_USERS_SUCCESS,
|
||||
FETCH_USERS_FAILURE,
|
||||
SORT_UPDATE,
|
||||
SET_PAGE,
|
||||
SET_SEARCH_VALUE,
|
||||
SET_ROLE,
|
||||
SET_COMMENTER_STATUS,
|
||||
SHOW_BANUSER_DIALOG,
|
||||
@@ -15,7 +17,8 @@ const initialState = {
|
||||
community: {},
|
||||
isFetchingPeople: false,
|
||||
errorPeople: '',
|
||||
accounts: [],
|
||||
users: [],
|
||||
searchValue: '',
|
||||
fieldPeople: 'created_at',
|
||||
ascPeople: false,
|
||||
totalPagesPeople: 0,
|
||||
@@ -27,19 +30,19 @@ const initialState = {
|
||||
|
||||
export default function community (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case FETCH_COMMENTERS_REQUEST :
|
||||
case FETCH_USERS_REQUEST :
|
||||
return {
|
||||
...state,
|
||||
isFetchingPeople: true,
|
||||
};
|
||||
case FETCH_COMMENTERS_FAILURE :
|
||||
case FETCH_USERS_FAILURE :
|
||||
return {
|
||||
...state,
|
||||
isFetchingPeople: false,
|
||||
errorPeople: action.error,
|
||||
};
|
||||
case FETCH_COMMENTERS_SUCCESS : {
|
||||
const {accounts, type, page, count, limit, totalPages, ...rest} = action; // eslint-disable-line
|
||||
case FETCH_USERS_SUCCESS : {
|
||||
const {users, type, page, count, limit, totalPages, ...rest} = action; // eslint-disable-line
|
||||
return {
|
||||
...state,
|
||||
isFetchingPeople: false,
|
||||
@@ -49,27 +52,32 @@ export default function community (state = initialState, action) {
|
||||
limitPeople: limit,
|
||||
totalPagesPeople: totalPages,
|
||||
...rest,
|
||||
accounts, // Sets to normal array
|
||||
users, // Sets to normal array
|
||||
};
|
||||
}
|
||||
case SET_PAGE:
|
||||
return {
|
||||
...state,
|
||||
pagePeople: action.page,
|
||||
};
|
||||
case SET_ROLE : {
|
||||
const commenters = state.accounts;
|
||||
const commenters = state.users;
|
||||
const idx = commenters.findIndex((el) => el.id === action.id);
|
||||
|
||||
commenters[idx].roles[0] = action.role;
|
||||
return {
|
||||
...state,
|
||||
accounts: commenters.map((id) => id),
|
||||
users: commenters.map((id) => id),
|
||||
};
|
||||
}
|
||||
case SET_COMMENTER_STATUS: {
|
||||
const commenters = state.accounts;
|
||||
const commenters = state.users;
|
||||
const idx = commenters.findIndex((el) => el.id === action.id);
|
||||
|
||||
commenters[idx].status = action.status;
|
||||
return {
|
||||
...state,
|
||||
accounts: commenters.map((id) => id),
|
||||
users: commenters.map((id) => id),
|
||||
};
|
||||
|
||||
}
|
||||
@@ -101,6 +109,11 @@ export default function community (state = initialState, action) {
|
||||
user: action.user,
|
||||
rejectUsernameDialog: true
|
||||
};
|
||||
case SET_SEARCH_VALUE:
|
||||
return {
|
||||
...state,
|
||||
searchValue: action.value,
|
||||
};
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import * as actions from '../constants/configure';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import update from 'immutability-helper';
|
||||
|
||||
const initialState = {
|
||||
canSave: false,
|
||||
pending: {},
|
||||
errors: {},
|
||||
activeSection: 'stream',
|
||||
};
|
||||
|
||||
export default function configure(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.UPDATE_PENDING: {
|
||||
let next = state;
|
||||
if (action.updater) {
|
||||
next = update(next, {
|
||||
pending: action.updater,
|
||||
});
|
||||
}
|
||||
if (action.errorUpdater) {
|
||||
next = update(next, {
|
||||
errors: action.errorUpdater,
|
||||
});
|
||||
}
|
||||
const noErrors = Object.keys(next.errors).reduce((res, error) => res && !next.errors[error], true);
|
||||
const canSave = !isEmpty(next.pending) && noErrors;
|
||||
next = update(next, {
|
||||
canSave: {$set: canSave},
|
||||
});
|
||||
|
||||
return next;
|
||||
}
|
||||
case actions.CLEAR_PENDING:
|
||||
return {
|
||||
...state,
|
||||
pending: {},
|
||||
canSave: false,
|
||||
};
|
||||
case actions.SET_ACTIVE_SECTION:
|
||||
return {
|
||||
...state,
|
||||
activeSection: action.section,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import auth from './auth';
|
||||
import assets from './assets';
|
||||
import settings from './settings';
|
||||
import stories from './stories';
|
||||
import configure from './configure';
|
||||
import community from './community';
|
||||
import moderation from './moderation';
|
||||
import install from './install';
|
||||
@@ -12,10 +12,10 @@ import userDetail from './userDetail';
|
||||
export default {
|
||||
auth,
|
||||
banUserDialog,
|
||||
configure,
|
||||
suspendUserDialog,
|
||||
userDetail,
|
||||
assets,
|
||||
settings,
|
||||
stories,
|
||||
community,
|
||||
moderation,
|
||||
install,
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import * as actions from '../actions/settings';
|
||||
import update from 'immutability-helper';
|
||||
|
||||
// this is initialized here because
|
||||
// currently you have to reload the dashboard to get new stats
|
||||
// cleaner updates are planned in the future.
|
||||
// TODO: if there are more than two fields for the dashboard being created here,
|
||||
// please create a new reducer specifically for the Dashboard.
|
||||
const DASHBOARD_WINDOW_MINUTES = 5;
|
||||
let then = new Date();
|
||||
then.setMinutes(then.getMinutes() - DASHBOARD_WINDOW_MINUTES);
|
||||
|
||||
const initialState = {
|
||||
wordlist: {
|
||||
banned: [],
|
||||
suspect: []
|
||||
},
|
||||
dashboardWindowStart: then.toISOString(),
|
||||
dashboardWindowEnd: new Date().toISOString(),
|
||||
domains: {
|
||||
whitelist: []
|
||||
},
|
||||
saveSettingsError: null,
|
||||
fetchSettingsError: null,
|
||||
fetchingSettings: false
|
||||
};
|
||||
|
||||
export default function settings (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.SETTINGS_LOADING:
|
||||
return {
|
||||
...state,
|
||||
fetchingSettings: true,
|
||||
fetchSettingsError: null,
|
||||
};
|
||||
case actions.SETTINGS_RECEIVED:
|
||||
return {
|
||||
...state,
|
||||
fetchingSettings: false,
|
||||
fetchSettingsError: null,
|
||||
...action.settings
|
||||
};
|
||||
case actions.SETTINGS_FETCH_ERROR:
|
||||
return {
|
||||
...state,
|
||||
fetchingSettings: false,
|
||||
fetchSettingsError: action.error,
|
||||
};
|
||||
case actions.SETTINGS_UPDATED:
|
||||
return {
|
||||
...state,
|
||||
fetchingSettings: false,
|
||||
fetchSettingsError: null,
|
||||
...action.settings
|
||||
};
|
||||
case actions.SAVE_SETTINGS_LOADING:
|
||||
return {
|
||||
...state,
|
||||
fetchingSettings: true,
|
||||
saveSettingsError: null,
|
||||
};
|
||||
case actions.SAVE_SETTINGS_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
fetchingSettings: false,
|
||||
fetchSettingsError: null,
|
||||
...action.settings
|
||||
};
|
||||
case actions.SAVE_SETTINGS_FAILED:
|
||||
return {
|
||||
...state,
|
||||
fetchingSettings: false,
|
||||
fetchSettingsError: action.error,
|
||||
};
|
||||
case actions.WORDLIST_UPDATED:
|
||||
return update(state, {
|
||||
wordlist: {
|
||||
[action.listName]: {
|
||||
$set: action.list
|
||||
}
|
||||
}
|
||||
});
|
||||
case actions.DOMAINLIST_UPDATED:
|
||||
return update(state, {
|
||||
domains: {
|
||||
[action.listName]: {$set: action.list},
|
||||
}
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import * as actions from '../constants/stories';
|
||||
import update from 'immutability-helper';
|
||||
|
||||
const initialState = {
|
||||
assets: {
|
||||
byId: {},
|
||||
ids: [],
|
||||
assets: []
|
||||
},
|
||||
searchValue: '',
|
||||
criteria: {
|
||||
asc: 'false',
|
||||
filter: 'all',
|
||||
},
|
||||
};
|
||||
|
||||
export default function assets (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.FETCH_ASSETS_SUCCESS: {
|
||||
const assets = action.assets.reduce((prev, curr) => {
|
||||
prev[curr.id] = curr;
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
return update(state, {
|
||||
assets: {
|
||||
totalPages: {$set: action.totalPages},
|
||||
page: {$set: action.page},
|
||||
byId: {$set: assets},
|
||||
count: {$set: action.count},
|
||||
ids: {$set: Object.keys(assets)},
|
||||
},
|
||||
});
|
||||
}
|
||||
case actions.UPDATE_ASSET_STATE_REQUEST:
|
||||
return update(state, {
|
||||
assets: {
|
||||
byId: {
|
||||
[action.id]: {
|
||||
closedAt: {$set: action.closedAt},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
case actions.UPDATE_ASSETS:
|
||||
return update(state, {
|
||||
assets: {
|
||||
assets: {$set: action.assets},
|
||||
},
|
||||
});
|
||||
case actions.SET_PAGE:
|
||||
return {
|
||||
...state,
|
||||
page: action.page,
|
||||
};
|
||||
case actions.SET_SEARCH_VALUE:
|
||||
return {
|
||||
...state,
|
||||
searchValue: action.value,
|
||||
};
|
||||
case actions.SET_CRITERIA:
|
||||
return {
|
||||
...state,
|
||||
criteria: {
|
||||
...state.criteria,
|
||||
...action.criteria,
|
||||
},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as actions from '../constants/userDetail';
|
||||
const initialState = {
|
||||
userId: null,
|
||||
activeTab: 'all',
|
||||
statuses: [],
|
||||
statuses: null,
|
||||
selectedCommentIds: [],
|
||||
};
|
||||
|
||||
@@ -41,6 +41,11 @@ export default function banUserDialog(state = initialState, action) {
|
||||
...state,
|
||||
selectedCommentIds: state.selectedCommentIds.filter((id) => id !== action.id),
|
||||
};
|
||||
case actions.SELECT_ALL_USER_DETAIL_COMMENT:
|
||||
return {
|
||||
...state,
|
||||
selectedCommentIds: action.ids
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.actionButton {
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import React from 'react';
|
||||
import styles from './ActionButton.css';
|
||||
import {Button} from 'coral-ui';
|
||||
import {menuActionsMap} from '../../../utils/moderationQueueActionsMap';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
// TODO: Needs refactoring.
|
||||
const ActionButton = ({type = '', user, ...props}) => {
|
||||
return (
|
||||
<Button
|
||||
className={`${type.toLowerCase()} ${styles.actionButton}`}
|
||||
cStyle={type.toLowerCase()}
|
||||
icon={menuActionsMap[type].icon}
|
||||
onClick={() => {
|
||||
type === 'APPROVE' ? props.approveUser({userId: user.id}) : props.showRejectUsernameDialog({user: user});
|
||||
}}
|
||||
>{t(`modqueue.${menuActionsMap[type].text}`)}</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionButton;
|
||||
@@ -0,0 +1,4 @@
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@@ -1,78 +1,18 @@
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import CommunityMenu from './CommunityMenu';
|
||||
import People from './People';
|
||||
import FlaggedAccounts from '../containers/FlaggedAccounts';
|
||||
import RejectUsernameDialog from './RejectUsernameDialog';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Community.css';
|
||||
import People from '../containers/People';
|
||||
import CommunityMenu from './CommunityMenu';
|
||||
import RejectUsernameDialog from './RejectUsernameDialog';
|
||||
import FlaggedAccounts from '../containers/FlaggedAccounts';
|
||||
|
||||
export default class Community extends Component {
|
||||
|
||||
state = {
|
||||
searchValue: '',
|
||||
timer: null
|
||||
};
|
||||
|
||||
onKeyDownHandler = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.search();
|
||||
}
|
||||
}
|
||||
|
||||
onSearchChange = (e) => {
|
||||
const value = e.target.value;
|
||||
this.setState((prevState) => {
|
||||
prevState.searchValue = value;
|
||||
clearTimeout(prevState.timer);
|
||||
const fetchAccounts = this.props.fetchAccounts;
|
||||
prevState.timer = setTimeout(() => {
|
||||
fetchAccounts({value});
|
||||
}, 350);
|
||||
return prevState;
|
||||
});
|
||||
}
|
||||
|
||||
onHeaderClickHandler = (sort) => {
|
||||
this.props.updateSorting(sort);
|
||||
this.search();
|
||||
}
|
||||
|
||||
onNewPageHandler = (page) => {
|
||||
this.props.newPage(page);
|
||||
this.search({page});
|
||||
}
|
||||
|
||||
search(query = {}) {
|
||||
const {community} = this.props;
|
||||
|
||||
this.props.fetchAccounts({
|
||||
value: this.state.searchValue,
|
||||
field: community.fieldPeople,
|
||||
asc: community.ascPeople,
|
||||
...query
|
||||
});
|
||||
}
|
||||
|
||||
getTabContent(searchValue, props) {
|
||||
const {community} = props;
|
||||
const activeTab = props.route.path === ':id' ? 'flagged' : props.route.path;
|
||||
class Community extends Component {
|
||||
renderTab() {
|
||||
const {route, community, ...props} = this.props;
|
||||
const activeTab = route.path === ':id' ? 'flagged' : route.path;
|
||||
|
||||
if (activeTab === 'people') {
|
||||
return (
|
||||
<People
|
||||
isFetching={community.isFetchingPeople}
|
||||
commenters={community.accounts}
|
||||
searchValue={searchValue}
|
||||
onSearchChange={this.onSearchChange}
|
||||
error={community.errorPeople}
|
||||
totalPages={community.totalPagesPeople}
|
||||
page={community.pagePeople}
|
||||
onKeyDown={this.onKeyDownHandler}
|
||||
onHeaderClickHandler={this.onHeaderClickHandler}
|
||||
onNewPageHandler={this.onNewPageHandler}
|
||||
/>
|
||||
);
|
||||
return <People community={community} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -82,37 +22,36 @@ export default class Community extends Component {
|
||||
root={this.props.root}
|
||||
/>
|
||||
<RejectUsernameDialog
|
||||
open={community.rejectUsernameDialog}
|
||||
handleClose={props.hideRejectUsernameDialog}
|
||||
user={community.user}
|
||||
open={community.rejectUsernameDialog}
|
||||
rejectUsername={props.rejectUsername}
|
||||
handleClose={props.hideRejectUsernameDialog}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {searchValue} = this.state;
|
||||
const tab = this.getTabContent(searchValue, this.props);
|
||||
const {root: {flaggedUsernamesCount}} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="talk-admin-community">
|
||||
<CommunityMenu flaggedUsernamesCount={flaggedUsernamesCount} />
|
||||
<div>{tab}</div>
|
||||
<div className={styles.container}>
|
||||
{this.renderTab()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Community.propTypes = {
|
||||
community: PropTypes.object,
|
||||
fetchAccounts: PropTypes.func,
|
||||
hideRejectUsernameDialog: PropTypes.func,
|
||||
updateSorting: PropTypes.func,
|
||||
newPage: PropTypes.func,
|
||||
route: PropTypes.object,
|
||||
rejectUsername: PropTypes.func,
|
||||
community: PropTypes.object,
|
||||
rejectUsername: PropTypes.func.isRequired,
|
||||
hideRejectUsernameDialog: PropTypes.func.isRequired,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object
|
||||
root: PropTypes.object,
|
||||
};
|
||||
|
||||
export default Community;
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const CommunityLayout = (props) => (
|
||||
<div>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default CommunityLayout;
|
||||
@@ -13,11 +13,11 @@ const CommunityMenu = ({flaggedUsernamesCount = 0}) => {
|
||||
<div className='mdl-tabs'>
|
||||
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
|
||||
<div>
|
||||
<Link to={flaggedPath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
|
||||
<Link to={flaggedPath} className={`mdl-tabs__tab ${styles.tab} talk-admin-nav-flagged-accounts`} activeClassName={styles.active}>
|
||||
{t('community.flaggedaccounts')}
|
||||
<CountBadge count={flaggedUsernamesCount} />
|
||||
</Link>
|
||||
<Link to={peoplePath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
|
||||
<Link to={peoplePath} className={`mdl-tabs__tab ${styles.tab} talk-admin-nav-people`} activeClassName={styles.active}>
|
||||
{t('community.people')}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
padding-bottom: 200px;
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.mainFlaggedContent {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import EmptyCard from 'coral-admin/src/components/EmptyCard';
|
||||
import LoadMore from '../../../components/LoadMore';
|
||||
@@ -23,7 +24,7 @@ class FlaggedAccounts extends React.Component {
|
||||
const hasResults = users.nodes && !!users.nodes.length;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={cn('talk-adnin-community-flagged-accounts', styles.container)}>
|
||||
<div className={styles.mainFlaggedContent}>
|
||||
{
|
||||
hasResults
|
||||
@@ -47,7 +48,6 @@ class FlaggedAccounts extends React.Component {
|
||||
<FlaggedUser
|
||||
user={user}
|
||||
key={user.id}
|
||||
modActionButtons={['APPROVE', 'REJECT']}
|
||||
showBanUserDialog={showBanUserDialog}
|
||||
showSuspendUserDialog={showSuspendUserDialog}
|
||||
showRejectUsernameDialog={showRejectUsernameDialog}
|
||||
@@ -71,4 +71,15 @@ class FlaggedAccounts extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
FlaggedAccounts.propTypes = {
|
||||
users: PropTypes.object,
|
||||
loadMore: PropTypes.func,
|
||||
showBanUserDialog: PropTypes.func,
|
||||
showSuspendUserDialog: PropTypes.func,
|
||||
showRejectUsernameDialog: PropTypes.func,
|
||||
approveUser: PropTypes.func,
|
||||
me: PropTypes.object,
|
||||
viewUserDetail: PropTypes.func,
|
||||
};
|
||||
|
||||
export default FlaggedAccounts;
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import React from 'react';
|
||||
import styles from './FlaggedUser.css';
|
||||
|
||||
import ActionButton from './ActionButton';
|
||||
|
||||
// TODO: Should not rely on plugin.
|
||||
import {username} from 'talk-plugin-flags/helpers/flagReasons';
|
||||
|
||||
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
|
||||
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {username} from 'talk-plugin-flags/helpers/flagReasons';
|
||||
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
|
||||
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
import ApproveButton from 'coral-admin/src/components/ApproveButton';
|
||||
import RejectButton from 'coral-admin/src/components/RejectButton';
|
||||
|
||||
// TODO: Should work with custom flags too.
|
||||
const shortReasons = {
|
||||
[username.other]: t('community.other'),
|
||||
[username.spam]: t('community.spam_ads'),
|
||||
@@ -21,7 +17,6 @@ const shortReasons = {
|
||||
[username.impersonating]: t('community.impersonating'),
|
||||
};
|
||||
|
||||
// Render a single user for the list
|
||||
class User extends React.Component {
|
||||
|
||||
showSuspenUserDialog = () => this.props.showSuspendUserDialog({
|
||||
@@ -35,26 +30,25 @@ class User extends React.Component {
|
||||
});
|
||||
|
||||
viewAuthorDetail = () => this.props.viewUserDetail(this.props.user.id);
|
||||
showRejectUsernameDialog = () => this.props.showRejectUsernameDialog({id: this.props.user.id});
|
||||
approveUser = () => this.props.approveUser({
|
||||
userId: this.props.user.id,
|
||||
});
|
||||
|
||||
render() {
|
||||
const {
|
||||
user,
|
||||
modActionButtons,
|
||||
viewUserDetail,
|
||||
selected,
|
||||
approveUser,
|
||||
showRejectUsernameDialog,
|
||||
me,
|
||||
className,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<li
|
||||
tabIndex={0}
|
||||
className={cn(className, styles.root, {[styles.rootSelected]: selected})}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<li tabIndex={0}
|
||||
className={cn(className, styles.root, {[styles.rootSelected]: selected})} >
|
||||
<div className={cn('talk-admin-community-flagged-user', styles.container)}>
|
||||
<div className={cn('talk-admin-community-flagged-user-header', styles.header)}>
|
||||
<div className={styles.author}>
|
||||
<button
|
||||
onClick={this.viewAuthorDetail}
|
||||
@@ -77,8 +71,7 @@ class User extends React.Component {
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.body}>
|
||||
<div className={cn('talk-admin-community-flagged-user-body', styles.body)}>
|
||||
<div className={styles.flagged}>
|
||||
<div className={styles.flaggedByCount}>
|
||||
<i className={cn('material-icons', styles.flagIcon)}>flag</i>
|
||||
@@ -125,14 +118,14 @@ class User extends React.Component {
|
||||
</div>
|
||||
<div className={styles.sideActions}>
|
||||
<div className={styles.actions}>
|
||||
{modActionButtons.map((action, i) =>
|
||||
<ActionButton key={i}
|
||||
type={action.toUpperCase()}
|
||||
user={user}
|
||||
approveUser={approveUser}
|
||||
showRejectUsernameDialog={showRejectUsernameDialog}
|
||||
/>
|
||||
)}
|
||||
<ApproveButton
|
||||
className="talk-admin-flagged-user-approve-button"
|
||||
onClick={this.approveUser}
|
||||
/>
|
||||
<RejectButton
|
||||
className="talk-admin-flagged-user-reject-button"
|
||||
onClick={this.showRejectUsernameDialog}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -142,4 +135,16 @@ class User extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
User.propTypes = {
|
||||
showSuspendUserDialog: PropTypes.func,
|
||||
showBanUserDialog: PropTypes.func,
|
||||
viewUserDetail: PropTypes.func,
|
||||
showRejectUsernameDialog: PropTypes.func,
|
||||
approveUser: PropTypes.func,
|
||||
user: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
selected: PropTypes.bool,
|
||||
me: PropTypes.object,
|
||||
};
|
||||
|
||||
export default User;
|
||||
|
||||
@@ -1,34 +1,31 @@
|
||||
import React from 'react';
|
||||
|
||||
import cn from 'classnames';
|
||||
import styles from './styles.css';
|
||||
import Table from '../containers/Table';
|
||||
import {Pager, Icon} from 'coral-ui';
|
||||
import Table from './Table';
|
||||
import {Icon} from 'coral-ui';
|
||||
import EmptyCard from '../../../components/EmptyCard';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const tableHeaders = [
|
||||
{
|
||||
title: t('community.username_and_email'),
|
||||
field: 'username'
|
||||
},
|
||||
{
|
||||
title: t('community.account_creation_date'),
|
||||
field: 'created_at'
|
||||
},
|
||||
{
|
||||
title: t('community.status'),
|
||||
field: 'status'
|
||||
},
|
||||
{
|
||||
title: t('community.newsroom_role'),
|
||||
field: 'role'
|
||||
}
|
||||
];
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const People = (props) => {
|
||||
const {
|
||||
users = [],
|
||||
searchValue,
|
||||
onSearchChange,
|
||||
onHeaderClickHandler,
|
||||
onPageChange,
|
||||
totalPages,
|
||||
page,
|
||||
setRole,
|
||||
setCommenterStatus,
|
||||
viewUserDetail,
|
||||
} = props;
|
||||
|
||||
const hasResults = !!users.length;
|
||||
|
||||
const People = ({commenters, searchValue, onSearchChange, ...props}) => {
|
||||
const hasResults = !!commenters.length;
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={cn(styles.container, 'talk-admin-community-people-container')}>
|
||||
<div className={styles.leftColumn}>
|
||||
<div className={styles.searchBox}>
|
||||
<Icon name='search' className={styles.searchIcon}/>
|
||||
@@ -46,20 +43,33 @@ const People = ({commenters, searchValue, onSearchChange, ...props}) => {
|
||||
{
|
||||
hasResults
|
||||
? <Table
|
||||
headers={tableHeaders}
|
||||
commenters={commenters}
|
||||
onHeaderClickHandler={props.onHeaderClickHandler}
|
||||
users={users}
|
||||
setRole={setRole}
|
||||
viewUserDetail={viewUserDetail}
|
||||
setCommenterStatus={setCommenterStatus}
|
||||
onHeaderClickHandler={onHeaderClickHandler}
|
||||
pageCount={totalPages}
|
||||
onPageChange={onPageChange}
|
||||
page={page}
|
||||
/>
|
||||
: <EmptyCard>{t('community.no_results')}</EmptyCard>
|
||||
}
|
||||
<Pager
|
||||
totalPages={props.totalPages}
|
||||
page={props.page}
|
||||
onNewPageHandler={props.onNewPageHandler}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
People.propTypes = {
|
||||
onHeaderClickHandler: PropTypes.func,
|
||||
users: PropTypes.array,
|
||||
page: PropTypes.number.isRequired,
|
||||
searchValue: PropTypes.string,
|
||||
onSearchChange: PropTypes.func,
|
||||
totalPages: PropTypes.number,
|
||||
onPageChange: PropTypes.func,
|
||||
setCommenterStatus: PropTypes.func.isRequired,
|
||||
setRole: PropTypes.func.isRequired,
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default People;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import cn from 'classnames';
|
||||
import {Dialog, Button} from 'coral-ui';
|
||||
import styles from './RejectUsernameDialog.css';
|
||||
|
||||
@@ -29,12 +29,6 @@ class RejectUsernameDialog extends Component {
|
||||
|
||||
state = {email: '', stage: 0}
|
||||
|
||||
static propTypes = {
|
||||
stage: PropTypes.number,
|
||||
handleClose: PropTypes.func.isRequired,
|
||||
rejectUsername: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({email: t('reject_username.email_message_reject'), about: t('reject_username.username')});
|
||||
}
|
||||
@@ -51,7 +45,7 @@ class RejectUsernameDialog extends Component {
|
||||
const next = () => this.setState({stage: stage + 1});
|
||||
const suspend = async () => {
|
||||
try {
|
||||
await rejectUsername({id: user.user.id, message: this.state.email});
|
||||
await rejectUsername({id: user.id, message: this.state.email});
|
||||
this.props.handleClose();
|
||||
} catch (err) {
|
||||
|
||||
@@ -76,7 +70,7 @@ class RejectUsernameDialog extends Component {
|
||||
const {stage} = this.state;
|
||||
|
||||
return <Dialog
|
||||
className={styles.suspendDialog}
|
||||
className={cn(styles.suspendDialog, 'talk-reject-username-dialog')}
|
||||
id="rejectUsernameDialog"
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
@@ -96,15 +90,18 @@ class RejectUsernameDialog extends Component {
|
||||
<div className={styles.emailContainer}>
|
||||
<textarea
|
||||
rows={5}
|
||||
className={styles.emailInput}
|
||||
className={cn(styles.emailInput, 'talk-reject-username-dialog-suspension-message')}
|
||||
value={this.state.email}
|
||||
onChange={this.onEmailChange}/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className={styles.modalButtons}>
|
||||
<div className={cn(styles.modalButtons, 'talk-reject-username-dialog-buttons')}>
|
||||
{Object.keys(stages[stage].options).map((key, i) => (
|
||||
<Button key={i} onClick={this.onActionClick(stage, i)}>
|
||||
<Button
|
||||
key={i}
|
||||
className={cn('talk-reject-username-dialog-button', `talk-reject-username-dialog-button-${key}`)}
|
||||
onClick={this.onActionClick(stage, i)} >
|
||||
{t(stages[stage].options[key], t('reject_username.username'))}
|
||||
</Button>
|
||||
))}
|
||||
@@ -114,4 +111,12 @@ class RejectUsernameDialog extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
RejectUsernameDialog.propTypes = {
|
||||
stage: PropTypes.number,
|
||||
handleClose: PropTypes.func.isRequired,
|
||||
rejectUsername: PropTypes.func.isRequired,
|
||||
user: PropTypes.object,
|
||||
open: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default RejectUsernameDialog;
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
width: 100%;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
th {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
th.header {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
th:nth-child(2), th:nth-child(3) {
|
||||
width: 100px;
|
||||
}
|
||||
th.header:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
th.header:nth-child(2), th.header:nth-child(3) {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.button {
|
||||
@@ -19,6 +23,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.username, .email {
|
||||
max-width: 215px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.email {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -1,56 +1,98 @@
|
||||
import React from 'react';
|
||||
import {SelectField, Option} from 'react-mdl-selectfield';
|
||||
import styles from '../components/Table.css';
|
||||
import styles from './Table.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Paginate, Dropdown, Option} from 'coral-ui';
|
||||
import cn from 'classnames';
|
||||
|
||||
export default ({headers, commenters, onHeaderClickHandler, onRoleChange, onCommenterStatusChange, viewUserDetail}) => (
|
||||
<table className={`mdl-data-table ${styles.dataTable}`}>
|
||||
<thead>
|
||||
<tr>
|
||||
{headers.map((header, i) =>(
|
||||
<th
|
||||
key={i}
|
||||
className="mdl-data-table__cell--non-numeric"
|
||||
onClick={() => onHeaderClickHandler({field: header.field})}>
|
||||
{header.title}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{commenters.map((row, i)=> (
|
||||
<tr key={i}>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<button onClick={() => {viewUserDetail(row.id);}} className={styles.button}>{row.username}</button>
|
||||
<span className={styles.email}>{row.profiles.map(({id}) => id)}</span>
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
{row.created_at}
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<SelectField
|
||||
value={row.status || ''}
|
||||
className={styles.selectField}
|
||||
label={t('community.status')}
|
||||
onChange={(status) => onCommenterStatusChange(row.id, status)}>
|
||||
<Option value={'ACTIVE'}>{t('community.active')}</Option>
|
||||
<Option value={'BANNED'}>{t('community.banned')}</Option>
|
||||
</SelectField>
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<SelectField
|
||||
value={row.roles[0] || ''}
|
||||
className={styles.selectField}
|
||||
label={t('community.role')}
|
||||
onChange={(role) => onRoleChange(row.id, role)}>
|
||||
<Option value={''}>.</Option>
|
||||
<Option value={'STAFF'}>{t('community.staff')}</Option>
|
||||
<Option value={'MODERATOR'}>{t('community.moderator')}</Option>
|
||||
<Option value={'ADMIN'}>{t('community.admin')}</Option>
|
||||
</SelectField>
|
||||
</td>
|
||||
const headers = [
|
||||
{
|
||||
title: t('community.username_and_email'),
|
||||
field: 'username'
|
||||
},
|
||||
{
|
||||
title: t('community.account_creation_date'),
|
||||
field: 'created_at'
|
||||
},
|
||||
{
|
||||
title: t('community.status'),
|
||||
field: 'status'
|
||||
},
|
||||
{
|
||||
title: t('community.newsroom_role'),
|
||||
field: 'role'
|
||||
}
|
||||
];
|
||||
|
||||
const Table = ({users, setRole, onHeaderClickHandler, setCommenterStatus, viewUserDetail, pageCount, page, onPageChange}) => (
|
||||
<div>
|
||||
<table className={`mdl-data-table ${styles.dataTable}`}>
|
||||
<thead>
|
||||
<tr>
|
||||
{headers.map((header, i) =>(
|
||||
<th
|
||||
key={i}
|
||||
className={cn('mdl-data-table__cell--non-numeric', styles.header)}
|
||||
scope="col"
|
||||
onClick={() => onHeaderClickHandler({field: header.field})}>
|
||||
{header.title}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((row, i)=> (
|
||||
<tr key={i} className="talk-admin-community-people-row">
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<button onClick={() => {viewUserDetail(row.id);}} className={cn(styles.username, styles.button)}>{row.username}</button>
|
||||
<span className={styles.email}>{row.profiles.map(({id}) => id)}</span>
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
{row.created_at}
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<Dropdown
|
||||
containerClassName="talk-admin-community-people-dd-status"
|
||||
value={row.status}
|
||||
placeholder={t('community.status')}
|
||||
onChange={(status) => setCommenterStatus(row.id, status)}>
|
||||
<Option value={'ACTIVE'} label={t('community.active')} />
|
||||
<Option value={'BANNED'} label={t('community.banned')} />
|
||||
</Dropdown>
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<Dropdown
|
||||
containerClassName="talk-admin-community-people-dd-role"
|
||||
value={row.roles[0] || ''}
|
||||
placeholder={t('community.role')}
|
||||
onChange={(role) => setRole(row.id, role)}>
|
||||
<Option value={''} label={t('community.none')} />
|
||||
<Option value={'STAFF'} label={t('community.staff')} />
|
||||
<Option value={'MODERATOR'} label={t('community.moderator')} />
|
||||
<Option value={'ADMIN'} label={t('community.admin')} />
|
||||
</Dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<Paginate
|
||||
pageCount={pageCount}
|
||||
page={page - 1}
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Table.propTypes = {
|
||||
users: PropTypes.array,
|
||||
onHeaderClickHandler: PropTypes.func.isRequired,
|
||||
setRole: PropTypes.func.isRequired,
|
||||
setCommenterStatus: PropTypes.func.isRequired,
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
pageCount: PropTypes.number.isRequired,
|
||||
page: PropTypes.number.isRequired,
|
||||
onPageChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Table;
|
||||
|
||||
@@ -1,94 +1,48 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {getDefinitionName} from 'coral-framework/utils';
|
||||
import {withSetUserStatus, withRejectUsername} from 'coral-framework/graphql/mutations';
|
||||
import FlaggedAccounts from '../containers/FlaggedAccounts';
|
||||
import FlaggedUser from '../containers/FlaggedUser';
|
||||
|
||||
import {withSetUserStatus, withRejectUsername} from 'coral-framework/graphql/mutations';
|
||||
import {getDefinitionName} from 'coral-framework/utils';
|
||||
|
||||
import {
|
||||
fetchAccounts,
|
||||
updateSorting,
|
||||
newPage,
|
||||
hideRejectUsernameDialog
|
||||
} from '../../../actions/community';
|
||||
|
||||
import {hideRejectUsernameDialog} from '../../../actions/community';
|
||||
import Community from '../components/Community';
|
||||
|
||||
class CommunityContainer extends Component {
|
||||
|
||||
componentWillMount() {
|
||||
this.props.fetchAccounts({});
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Community
|
||||
fetchAccounts={this.props.fetchAccounts}
|
||||
community={this.props.community}
|
||||
hideRejectUsernameDialog={this.props.hideRejectUsernameDialog}
|
||||
updateSorting={this.props.updateSorting}
|
||||
newPage={this.props.newPage}
|
||||
route={this.props.route}
|
||||
rejectUsername={this.props.rejectUsername}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
const mapStateToProps = (state) => ({
|
||||
community: state.community,
|
||||
});
|
||||
|
||||
CommunityContainer.propTypes = {
|
||||
community: PropTypes.object,
|
||||
fetchAccounts: PropTypes.func,
|
||||
hideRejectUsernameDialog: PropTypes.func,
|
||||
updateSorting: PropTypes.func,
|
||||
newPage: PropTypes.func,
|
||||
route: PropTypes.object,
|
||||
rejectUsername: PropTypes.func,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object
|
||||
};
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
hideRejectUsernameDialog,
|
||||
}, dispatch);
|
||||
|
||||
const withData = withQuery(gql`
|
||||
query TalkAdmin_FlaggedUsernamesCount {
|
||||
flaggedUsernamesCount: userCount(query: {
|
||||
action_type: FLAG,
|
||||
statuses: [PENDING]
|
||||
})
|
||||
...${getDefinitionName(FlaggedAccounts.fragments.root)}
|
||||
...${getDefinitionName(FlaggedUser.fragments.root)}
|
||||
me {
|
||||
...${getDefinitionName(FlaggedUser.fragments.me)}
|
||||
__typename
|
||||
}
|
||||
query TalkAdmin_Community {
|
||||
flaggedUsernamesCount: userCount(query: {
|
||||
action_type: FLAG,
|
||||
statuses: [PENDING]
|
||||
})
|
||||
...${getDefinitionName(FlaggedAccounts.fragments.root)}
|
||||
...${getDefinitionName(FlaggedUser.fragments.root)}
|
||||
me {
|
||||
...${getDefinitionName(FlaggedUser.fragments.me)}
|
||||
__typename
|
||||
}
|
||||
${FlaggedAccounts.fragments.root}
|
||||
${FlaggedUser.fragments.root}
|
||||
${FlaggedUser.fragments.me}
|
||||
`, {
|
||||
}
|
||||
${FlaggedAccounts.fragments.root}
|
||||
${FlaggedUser.fragments.root}
|
||||
${FlaggedUser.fragments.me}
|
||||
`, {
|
||||
options: {
|
||||
fetchPolicy: 'network-only',
|
||||
},
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
fetchAccounts,
|
||||
hideRejectUsernameDialog,
|
||||
updateSorting,
|
||||
newPage,
|
||||
}, dispatch);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withSetUserStatus,
|
||||
withRejectUsername,
|
||||
withData,
|
||||
)(CommunityContainer);
|
||||
withData
|
||||
)(Community);
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import People from '../components/People';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {viewUserDetail} from '../../../actions/userDetail';
|
||||
|
||||
import {
|
||||
fetchUsers,
|
||||
updateSorting,
|
||||
setPage,
|
||||
hideRejectUsernameDialog,
|
||||
setCommenterStatus,
|
||||
setRole,
|
||||
setSearchValue,
|
||||
} from '../../../actions/community';
|
||||
|
||||
class PeopleContainer extends React.Component {
|
||||
timer=null;
|
||||
|
||||
fetchUsers = (query = {}) => {
|
||||
const {community} = this.props;
|
||||
|
||||
this.props.fetchUsers({
|
||||
value: community.searchValue,
|
||||
field: community.fieldPeople,
|
||||
asc: community.ascPeople,
|
||||
...query
|
||||
});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.fetchUsers();
|
||||
}
|
||||
|
||||
onKeyDownHandler = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.fetchUsers();
|
||||
}
|
||||
}
|
||||
|
||||
onSearchChange = (e) => {
|
||||
const value = e.target.value;
|
||||
|
||||
this.props.setSearchValue(value);
|
||||
clearTimeout(this.timer);
|
||||
this.timer = setTimeout(() => {
|
||||
this.fetchUsers({value});
|
||||
}, 350);
|
||||
}
|
||||
|
||||
onHeaderClickHandler = (sort) => {
|
||||
this.props.updateSorting(sort);
|
||||
this.fetchUsers();
|
||||
}
|
||||
|
||||
onPageChange = ({selected}) => {
|
||||
const page = selected + 1;
|
||||
this.props.setPage(page);
|
||||
this.fetchUsers({page});
|
||||
}
|
||||
|
||||
render() {
|
||||
return <People
|
||||
users={this.props.community.users}
|
||||
searchValue={this.props.community.searchValue}
|
||||
onSearchChange={this.onSearchChange}
|
||||
onHeaderClickHandler={this.onHeaderClickHandler}
|
||||
onPageChange={this.onPageChange}
|
||||
totalPages={this.props.community.totalPagesPeople}
|
||||
setCommenterStatus={this.props.setCommenterStatus}
|
||||
setRole={this.props.setRole}
|
||||
page={this.props.community.pagePeople}
|
||||
viewUserDetail={this.props.viewUserDetail}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
PeopleContainer.propTypes = {
|
||||
setPage: PropTypes.func,
|
||||
fetchUsers: PropTypes.func,
|
||||
updateSorting: PropTypes.func,
|
||||
setRole: PropTypes.func.isRequired,
|
||||
setCommenterStatus: PropTypes.func.isRequired,
|
||||
setSearchValue: PropTypes.func.isRequired,
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
community: PropTypes.object,
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
setPage,
|
||||
fetchUsers,
|
||||
updateSorting,
|
||||
hideRejectUsernameDialog,
|
||||
setCommenterStatus,
|
||||
setRole,
|
||||
viewUserDetail,
|
||||
setSearchValue,
|
||||
}, dispatch);
|
||||
|
||||
export default connect(null, mapDispatchToProps)(PeopleContainer);
|
||||
@@ -1,39 +0,0 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {compose} from 'react-apollo';
|
||||
import {setRole, setCommenterStatus} from '../../../actions/community';
|
||||
import Table from '../components/Table';
|
||||
import {viewUserDetail} from '../../../actions/userDetail';
|
||||
|
||||
class TableContainer extends Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render () {
|
||||
return <Table
|
||||
{...this.props}
|
||||
onRoleChange={this.props.setRole}
|
||||
onCommenterStatusChange={this.props.setCommenterStatus}
|
||||
commenters={this.props.commenters}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
commenters: state.community.accounts,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
setCommenterStatus,
|
||||
setRole,
|
||||
viewUserDetail,
|
||||
}, dispatch);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
)(TableContainer);
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export {default as Community} from './containers/Community';
|
||||
export {default as CommunityLayout} from './components/CommunityLayout';
|
||||
@@ -1,15 +1,7 @@
|
||||
/**
|
||||
* @TODO: deprecated as this file contains styles from multiple components. Please refactor.
|
||||
*/
|
||||
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
|
||||
h3 {
|
||||
color: black;
|
||||
font-size: 1.26em;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.leftColumn {
|
||||
@@ -17,108 +9,8 @@
|
||||
width: 234px;
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
width: calc(100% - 300px);
|
||||
padding: 10px 14px;
|
||||
box-sizing: border-box;
|
||||
max-width: 718px;
|
||||
}
|
||||
|
||||
.configSetting {
|
||||
margin-bottom: 20px;
|
||||
align-items: flex-start;
|
||||
min-height: 100px;
|
||||
max-width: 600px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
|
||||
.copiedText {
|
||||
display: inline-block;
|
||||
color: #00796b;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.copyButton {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settingsError {
|
||||
color: #d50000;
|
||||
}
|
||||
|
||||
.settingsError i {
|
||||
font-size: 14px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.settingsHeader {
|
||||
margin-top: 3px;
|
||||
margin-bottom: 7px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.disabledSettingText {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.configSettingInfoBox {
|
||||
min-height: 100px;
|
||||
margin-bottom: 20px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
text-align: left;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.configSettingEmbed {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.configTimeoutSelect {
|
||||
display: inline-block;
|
||||
margin-left: 20px;
|
||||
|
||||
i { /* fix for firefox and react-mdl-selectfield@0.2.0 */
|
||||
padding: 20px 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.inlineTextfield {
|
||||
border-color: #ccc;
|
||||
border-style: solid;
|
||||
border-width: 0px 0px 1px 0px;
|
||||
text-align: center;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.inlineTextfield:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.charCountTexfield, .editCommentTimeframeTextfield {
|
||||
width: 4em;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.charCountTexfieldEnabled {
|
||||
border-color: #00796b;
|
||||
.saveBox {
|
||||
margin-top: 38px;
|
||||
}
|
||||
|
||||
.changedSave {
|
||||
@@ -126,81 +18,10 @@
|
||||
color: white;
|
||||
}
|
||||
|
||||
.embedInput {
|
||||
width: 100%;
|
||||
display: block;
|
||||
outline: none;
|
||||
border: 1px solid rgba(0,0,0,.12);
|
||||
padding: 6px;
|
||||
.mainContent {
|
||||
width: calc(100% - 300px);
|
||||
padding: 10px 14px 80px 14px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
margin: 5px auto;
|
||||
min-height: 175px;
|
||||
font-size: 14px;
|
||||
resize: none;
|
||||
max-width: 718px;
|
||||
}
|
||||
|
||||
.customCSSInput {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
padding: 14px;
|
||||
letter-spacing: 0.03em;
|
||||
color: #555;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.enabledSetting {
|
||||
border-left-color: #00796b;
|
||||
border-left-style: solid;
|
||||
border-left-width: 7px;
|
||||
}
|
||||
|
||||
.disabledSetting {
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.saveBox {
|
||||
margin-top: 38px;
|
||||
}
|
||||
|
||||
.settingsSection {
|
||||
padding-bottom: 200px;
|
||||
.action {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: inline-block;
|
||||
padding: 0px 30px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.Configure {
|
||||
|
||||
p {
|
||||
line-height: 1.2;
|
||||
max-width: 550px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.descriptionBox {
|
||||
margin-top: 15px;
|
||||
|
||||
input {
|
||||
height: 150px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,119 +1,40 @@
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import {Button, List, Item, Card, Spinner} from 'coral-ui';
|
||||
import {Button, List, Item} from 'coral-ui';
|
||||
import styles from './Configure.css';
|
||||
import StreamSettings from './StreamSettings';
|
||||
import ModerationSettings from './ModerationSettings';
|
||||
import TechSettings from './TechSettings';
|
||||
import StreamSettings from '../containers/StreamSettings';
|
||||
import ModerationSettings from '../containers/ModerationSettings';
|
||||
import TechSettings from '../containers/TechSettings';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class Configure extends Component {
|
||||
|
||||
state = {
|
||||
activeSection: 'stream',
|
||||
changed: false,
|
||||
errors: {}
|
||||
};
|
||||
|
||||
saveSettings = () => {
|
||||
this.props.saveSettingsToServer();
|
||||
this.setState({changed: false});
|
||||
}
|
||||
|
||||
changeSection = (activeSection) => {
|
||||
this.setState({activeSection});
|
||||
}
|
||||
|
||||
onChangeWordlist = (listName, list) => {
|
||||
this.setState({changed: true});
|
||||
this.props.updateWordlist(listName, list);
|
||||
}
|
||||
|
||||
onChangeDomainlist = (listName, list) => {
|
||||
this.setState({changed: true});
|
||||
this.props.updateDomainlist(listName, list);
|
||||
}
|
||||
|
||||
onSettingUpdate = (setting) => {
|
||||
this.setState({changed: true});
|
||||
this.props.updateSettings(setting);
|
||||
}
|
||||
|
||||
// Sets an arbitrary error string and a boolean state.
|
||||
// This allows the system to track multiple errors.
|
||||
onSettingError = (error, state) => {
|
||||
this.setState((prevState) => {
|
||||
prevState.errors[error] = state;
|
||||
return prevState;
|
||||
});
|
||||
}
|
||||
|
||||
getSection (section) {
|
||||
const pageTitle = this.getPageTitle(section);
|
||||
let sectionComponent;
|
||||
getSectionComponent(section) {
|
||||
switch(section){
|
||||
case 'stream':
|
||||
sectionComponent = <StreamSettings
|
||||
settings={this.props.settings}
|
||||
updateSettings={this.onSettingUpdate}
|
||||
errors={this.state.errors}
|
||||
settingsError={this.onSettingError}/>;
|
||||
break;
|
||||
return StreamSettings;
|
||||
case 'moderation':
|
||||
sectionComponent = <ModerationSettings
|
||||
onChangeWordlist={this.onChangeWordlist}
|
||||
settings={this.props.settings}
|
||||
updateSettings={this.onSettingUpdate} />;
|
||||
break;
|
||||
return ModerationSettings;
|
||||
case 'tech':
|
||||
sectionComponent = <TechSettings
|
||||
onChangeDomainlist={this.onChangeDomainlist}
|
||||
settings={this.props.settings}
|
||||
updateSettings={this.onSettingUpdate} />;
|
||||
}
|
||||
|
||||
if (this.props.settings.fetchingSettings) {
|
||||
return <Card shadow="4"><Spinner/>Loading settings...</Card>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.settingsSection}>
|
||||
<h3>{pageTitle}</h3>
|
||||
{sectionComponent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getPageTitle (section) {
|
||||
switch(section) {
|
||||
case 'stream':
|
||||
return t('configure.stream_settings');
|
||||
case 'moderation':
|
||||
return t('configure.moderation_settings');
|
||||
case 'tech':
|
||||
return t('configure.tech_settings');
|
||||
default:
|
||||
return '';
|
||||
return TechSettings;
|
||||
}
|
||||
throw new Error(`Unknown section ${section}`);
|
||||
}
|
||||
|
||||
render () {
|
||||
const {activeSection} = this.state;
|
||||
const section = this.getSection(activeSection);
|
||||
const {auth: {user}} = this.props;
|
||||
const {auth: {user}, canSave, savePending, setActiveSection, activeSection} = this.props;
|
||||
const SectionComponent = this.getSectionComponent(activeSection);
|
||||
|
||||
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);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.leftColumn}>
|
||||
<List onChange={this.changeSection} activeItem={activeSection}>
|
||||
<List onChange={setActiveSection} activeItem={activeSection}>
|
||||
<Item itemId='stream' icon='speaker_notes'>
|
||||
{t('configure.stream_settings')}
|
||||
</Item>
|
||||
@@ -126,10 +47,10 @@ export default class Configure extends Component {
|
||||
</List>
|
||||
<div className={styles.saveBox}>
|
||||
{
|
||||
showSave ?
|
||||
canSave ?
|
||||
<Button
|
||||
raised
|
||||
onClick={this.saveSettings}
|
||||
onClick={savePending}
|
||||
className={styles.changedSave}
|
||||
icon='check'
|
||||
full
|
||||
@@ -150,11 +71,25 @@ export default class Configure extends Component {
|
||||
|
||||
</div>
|
||||
<div className={styles.mainContent}>
|
||||
{ this.props.saveFetchingError }
|
||||
{ this.props.fetchSettingsError }
|
||||
{ section }
|
||||
<SectionComponent
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
settings={this.props.settings}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Configure.propTypes = {
|
||||
notify: PropTypes.func.isRequired,
|
||||
savePending: PropTypes.func.isRequired,
|
||||
auth: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
canSave: PropTypes.bool.isRequired,
|
||||
setActiveSection: PropTypes.func.isRequired,
|
||||
activeSection: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.title {
|
||||
color: black;
|
||||
font-size: 1.26em;
|
||||
font-weight: 500;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user