mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7446712109 | ||
|
|
6647e22d1c | ||
|
|
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 | ||
|
|
b189c9d2b0 | ||
|
|
85890b4e6e | ||
|
|
6d3cdb176e | ||
|
|
38abf63cc9 | ||
|
|
34334b8152 | ||
|
|
0b973f24ff | ||
|
|
abff1c763a | ||
|
|
9f4a913655 | ||
|
|
d363c977dc | ||
|
|
f004573ed2 | ||
|
|
fb9419f2ad | ||
|
|
9fc0fc37db | ||
|
|
c442b8bb70 | ||
|
|
e1f897ec17 | ||
|
|
1803c740fc | ||
|
|
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 | ||
|
|
525da57419 | ||
|
|
4913eda974 | ||
|
|
cee247f065 | ||
|
|
961111c3f7 | ||
|
|
2d96163b76 | ||
|
|
c3e24a0451 | ||
|
|
6d04b4add3 | ||
|
|
42954fb84a | ||
|
|
5f7c4f82e7 | ||
|
|
b482976c58 | ||
|
|
6e89b8c1aa | ||
|
|
e046a9652d | ||
|
|
7a74d5ee83 | ||
|
|
b9086244e2 | ||
|
|
7a4aae93b4 | ||
|
|
a5350c44ae | ||
|
|
a211f94a91 | ||
|
|
bccad5585e | ||
|
|
b3cb99925d | ||
|
|
a96d8c2b3c | ||
|
|
b0219948a0 | ||
|
|
563a6d23dd | ||
|
|
bc5d348c98 | ||
|
|
818ad07eac | ||
|
|
b8f6ab9237 | ||
|
|
f1f9868028 | ||
|
|
b5deac7b48 | ||
|
|
ae430245ec | ||
|
|
ebe7ccfd10 | ||
|
|
f00e8dc335 | ||
|
|
262213a520 | ||
|
|
00f98520f7 | ||
|
|
0dfcbc9927 | ||
|
|
a68e1c9c0b | ||
|
|
75635aff2b | ||
|
|
ab2e036047 | ||
|
|
52c1989780 | ||
|
|
abd458ab82 | ||
|
|
41afe9fae7 | ||
|
|
e6fa0a6d12 | ||
|
|
a0fb6030ca | ||
|
|
c95d0e45f8 | ||
|
|
7144f171ef | ||
|
|
23e1b56c6e | ||
|
|
9b999c6249 | ||
|
|
7a81a125f5 | ||
|
|
79cdd65e30 | ||
|
|
ed22bc34fc | ||
|
|
d7ea88c367 | ||
|
|
afec9932f0 | ||
|
|
2c297d7c2f | ||
|
|
2814070808 | ||
|
|
6ed6726daf | ||
|
|
21936ae467 | ||
|
|
613a7f7073 | ||
|
|
9258347886 | ||
|
|
959193f8b5 | ||
|
|
212c6cd0ce | ||
|
|
5f3931f34b | ||
|
|
aa3327945e | ||
|
|
c8dc5d4cd9 | ||
|
|
11c46439a2 | ||
|
|
a07d661655 | ||
|
|
24391de61d | ||
|
|
33822ff6c0 | ||
|
|
5d153e764d | ||
|
|
597d67f5d2 | ||
|
|
100dd519ee | ||
|
|
eb03d62e51 | ||
|
|
8bfc7e942c | ||
|
|
1a349eb0cd | ||
|
|
f19b333537 | ||
|
|
c6c2d545f8 | ||
|
|
ebe9945492 | ||
|
|
c6403b2da3 | ||
|
|
cd100a23cb | ||
|
|
25761f9e61 | ||
|
|
1a76a83806 | ||
|
|
55ebd1df8e | ||
|
|
c427691bef | ||
|
|
93afd793cb | ||
|
|
35232cf015 | ||
|
|
f864ebf2e3 | ||
|
|
a9cc78b61d | ||
|
|
2c3a52b48c | ||
|
|
0b1a75726c | ||
|
|
384c126d9e | ||
|
|
f97094177a | ||
|
|
4739f86739 | ||
|
|
f386909748 | ||
|
|
aa4f781d25 | ||
|
|
4567b4565f | ||
|
|
3133922100 | ||
|
|
e9bd5e4476 | ||
|
|
f2e06d6d27 | ||
|
|
e31cdebc12 | ||
|
|
e03e7de82f | ||
|
|
c7b755d2ff | ||
|
|
dc7013187d | ||
|
|
20c4148301 | ||
|
|
ba24af015c | ||
|
|
94456674df | ||
|
|
5231c2d627 | ||
|
|
f74e8dda16 | ||
|
|
825d53afcd | ||
|
|
029877080d | ||
|
|
83f10a4863 | ||
|
|
d820d2d2c3 | ||
|
|
a6443f2bcc | ||
|
|
7ace5b369a | ||
|
|
cf66c10262 | ||
|
|
99296ebe69 | ||
|
|
7600ce8d26 | ||
|
|
26256b979e | ||
|
|
0113f02994 | ||
|
|
b3eb3361bb | ||
|
|
fce65e616d | ||
|
|
6a4a0b7f6b | ||
|
|
c41ab58109 | ||
|
|
837a586234 | ||
|
|
2facde3894 | ||
|
|
76a6a0e82b | ||
|
|
bea25f2a11 | ||
|
|
ad361413c7 | ||
|
|
cf61d9e2ca | ||
|
|
77ef9ff111 | ||
|
|
c8cc5e6a34 | ||
|
|
bdd5e47427 | ||
|
|
1574e08197 | ||
|
|
abc651c55d | ||
|
|
fa335564ed | ||
|
|
0ae2919def | ||
|
|
838a3bad2e | ||
|
|
6487072be1 | ||
|
|
a16f101a12 | ||
|
|
647502d701 | ||
|
|
d728065fe5 | ||
|
|
18b8a18f1d | ||
|
|
1089807086 | ||
|
|
0c1c64fdf9 | ||
|
|
90a649db9b | ||
|
|
17ec666d97 | ||
|
|
cda86a67ab | ||
|
|
32d72129e5 | ||
|
|
2905ffb5cb | ||
|
|
433b0d310e | ||
|
|
08848057a5 | ||
|
|
7f517ed4fb | ||
|
|
c364b3e307 | ||
|
|
008df99100 | ||
|
|
bfdfd98135 | ||
|
|
64be131047 | ||
|
|
7115d4c31f | ||
|
|
a944cd2608 | ||
|
|
0e7d74559f | ||
|
|
297b905163 | ||
|
|
9c08b586ec | ||
|
|
dbeca830a2 | ||
|
|
0042767fad | ||
|
|
110a0093af | ||
|
|
6bac6ee5c5 | ||
|
|
aa364e1abe | ||
|
|
6e88cc94e0 | ||
|
|
7976aace00 | ||
|
|
1261e4e25c | ||
|
|
2a655331fc | ||
|
|
98b95cab86 | ||
|
|
d8ca9dfb07 | ||
|
|
ee3cee2a50 | ||
|
|
65a6c408a7 | ||
|
|
d18a9a057b | ||
|
|
b27e3e0a7f | ||
|
|
f2804f2394 | ||
|
|
d1c30ab982 | ||
|
|
ca411a3227 | ||
|
|
5e4a48f4cc | ||
|
|
c93b53c25b | ||
|
|
51f6c0738d | ||
|
|
49f09b87a0 | ||
|
|
06c45faeb0 | ||
|
|
70eee3d4b2 | ||
|
|
ecd4267ea9 | ||
|
|
4f0e8d9e47 | ||
|
|
1c630835c1 | ||
|
|
a9c4c12824 | ||
|
|
a22ee3fefe | ||
|
|
1e9cc82b17 | ||
|
|
65085fea65 | ||
|
|
908b3c20b4 | ||
|
|
59e21cccba | ||
|
|
ab1f1dee3e | ||
|
|
04ad649f01 | ||
|
|
18e36af50a | ||
|
|
8f1ce9cef6 | ||
|
|
e47cb9732e | ||
|
|
5159ecc64e | ||
|
|
851c5fbfbc | ||
|
|
0bc9ede8bc | ||
|
|
d8d758255d | ||
|
|
c1e52ba494 | ||
|
|
b7e501f7a3 | ||
|
|
ee41cc690c | ||
|
|
965b8f9bda | ||
|
|
823c570539 | ||
|
|
221db29539 | ||
|
|
9af21b4226 | ||
|
|
7f85ceb566 | ||
|
|
d20bd20005 | ||
|
|
ce3e29813b | ||
|
|
1f6da44b25 | ||
|
|
0ea951f3d9 | ||
|
|
b75539fb57 | ||
|
|
72530e7501 | ||
|
|
790c20e7ac | ||
|
|
2cbd59b4b5 | ||
|
|
d88919e27b | ||
|
|
7ea22c84ee | ||
|
|
be86a6f3dc | ||
|
|
4046b6e0a6 | ||
|
|
6b74877d01 | ||
|
|
9f33a77745 | ||
|
|
f33c31c70e | ||
|
|
e28f1caaef | ||
|
|
fefacf039f | ||
|
|
6494669262 | ||
|
|
4971ba4722 | ||
|
|
a9626ad12c | ||
|
|
fd71acf7e0 | ||
|
|
05d643d3c3 | ||
|
|
244a4b108d | ||
|
|
1221c6043f | ||
|
|
0cfd8afce7 | ||
|
|
c8d1da5715 | ||
|
|
e5bd8e2a3b | ||
|
|
7051c76b40 | ||
|
|
76fd261141 | ||
|
|
f2ecfa0a0d | ||
|
|
5e295fc337 | ||
|
|
299c6f1ffd | ||
|
|
42e15bd630 | ||
|
|
d16066a981 | ||
|
|
51077b689d | ||
|
|
f69703a8ba | ||
|
|
2e350c2244 | ||
|
|
b43a57328c | ||
|
|
d5fcdd6a76 | ||
|
|
589189023a | ||
|
|
9c5821f6a2 | ||
|
|
671c1d7449 | ||
|
|
22cea681a7 | ||
|
|
56bd3f23c7 | ||
|
|
2b91bcffda | ||
|
|
2b6a5bfe6e | ||
|
|
d57c4b253a | ||
|
|
462fcb6b94 | ||
|
|
8e96ef4692 | ||
|
|
94b9c4b2df | ||
|
|
96e37ac725 | ||
|
|
bd7f81fb35 | ||
|
|
53372ca1e8 | ||
|
|
856ae456f4 | ||
|
|
f6b02a52fe | ||
|
|
aa52063fa4 | ||
|
|
b72bba96fb | ||
|
|
32d9dc6646 | ||
|
|
a31a63e1c8 | ||
|
|
f82a0c80bf | ||
|
|
7b59323b26 | ||
|
|
f1c5d479ad | ||
|
|
d8a956d09c | ||
|
|
a78be6874b | ||
|
|
c95c20c9ad | ||
|
|
43b7326db4 | ||
|
|
b1b3db51a6 | ||
|
|
41cd5b320f | ||
|
|
9b0caa42cd | ||
|
|
aeb8852588 | ||
|
|
b3fe3afd4c | ||
|
|
e96e9c0d5c | ||
|
|
a11f149c5a | ||
|
|
a6b517b3dd | ||
|
|
6f82fd76f5 | ||
|
|
4a9b24f096 | ||
|
|
48875294c8 | ||
|
|
12be487783 | ||
|
|
5953ef1695 | ||
|
|
17ae62c66b | ||
|
|
bb66d7220b | ||
|
|
0cf7d19d4d | ||
|
|
24a8712159 | ||
|
|
7b27feee96 | ||
|
|
0b1e95e66d | ||
|
|
997a336483 | ||
|
|
51a4f0e02e | ||
|
|
869bf10c96 | ||
|
|
0375e7e039 | ||
|
|
9b29261f3d | ||
|
|
fea5f75aa6 | ||
|
|
345d183bc4 | ||
|
|
52490375e4 | ||
|
|
53871cb2c6 | ||
|
|
9a549a2b63 | ||
|
|
4dbcc65126 | ||
|
|
10693f3e5c | ||
|
|
dfd77dbf63 | ||
|
|
7b71511cae | ||
|
|
f260e41274 | ||
|
|
2631d9338a | ||
|
|
94bb223f0e | ||
|
|
0486d2926d | ||
|
|
cb655f15b3 | ||
|
|
2eb0637e61 | ||
|
|
079a20f13e | ||
|
|
f0dcef7b37 | ||
|
|
a2fdc11451 | ||
|
|
6b15c335b5 | ||
|
|
c246caa520 | ||
|
|
8ca32c5808 | ||
|
|
5a48705d49 | ||
|
|
173c257126 | ||
|
|
9e65f1b00d | ||
|
|
bf7dfefcdf | ||
|
|
d4c9749133 | ||
|
|
90a8a87eaf | ||
|
|
deb69d4e8c | ||
|
|
f528fb4345 | ||
|
|
df79530982 | ||
|
|
1a1e3d2d4a | ||
|
|
7517679c5e | ||
|
|
c6d7b714a1 | ||
|
|
d12d0cfe4f | ||
|
|
2c6447cd07 | ||
|
|
ce63113c5d | ||
|
|
3636bb71c4 | ||
|
|
788950fed2 | ||
|
|
4086fe8684 | ||
|
|
6e6d999c9b | ||
|
|
c2f866a077 | ||
|
|
0d037c3acb | ||
|
|
88f06744b4 | ||
|
|
2c1cd50828 | ||
|
|
8c4612a4dd | ||
|
|
4b13ccc5c1 | ||
|
|
b62ab9bff0 | ||
|
|
3740dd98e8 | ||
|
|
4db1cf2656 | ||
|
|
eb44c55e05 | ||
|
|
75da26ff7a | ||
|
|
93d0a265b5 | ||
|
|
494d52ca36 | ||
|
|
c67f92eb44 |
@@ -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"]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,5 +22,11 @@ plugins/*
|
||||
!plugins/talk-plugin-author-menu
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-ignore-user
|
||||
!plugins/talk-plugin-moderation-actions
|
||||
!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",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2017
|
||||
},
|
||||
"plugins": [
|
||||
"promise",
|
||||
"json"
|
||||
],
|
||||
"rules": {
|
||||
"indent": ["error",
|
||||
2
|
||||
],
|
||||
"no-console": "off",
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": ["error", "single"],
|
||||
"semi": ["error", "always"],
|
||||
"no-template-curly-in-string": "warn",
|
||||
"no-unsafe-negation": "warn",
|
||||
"array-callback-return": "warn",
|
||||
"arrow-parens": ["warn", "always"],
|
||||
"template-curly-spacing": "warn",
|
||||
"eqeqeq": ["error", "smart"],
|
||||
"no-eval": "error",
|
||||
"no-global-assign": "error",
|
||||
"no-implied-eval": "error",
|
||||
"lines-around-comment": ["warn", {"beforeLineComment": true}],
|
||||
"spaced-comment": ["warn", "always", { "line": { "exceptions": ["-", "="] } }],
|
||||
"no-script-url": "error",
|
||||
"no-throw-literal": "error",
|
||||
"yoda": "warn",
|
||||
"no-path-concat": "error",
|
||||
"eol-last": "warn",
|
||||
"no-nested-ternary": "warn",
|
||||
"no-tabs": "error",
|
||||
"no-unneeded-ternary": "warn",
|
||||
"object-curly-spacing": "warn",
|
||||
"space-infix-ops": ["error"],
|
||||
"space-in-parens": ["error", "never"],
|
||||
"space-unary-ops": ["error", {
|
||||
"words": true,
|
||||
"nonwords": false
|
||||
}],
|
||||
"no-const-assign": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"prefer-template": "warn",
|
||||
"comma-spacing": ["error", {
|
||||
"after": true
|
||||
}],
|
||||
"no-var": "error",
|
||||
"no-lonely-if": "error",
|
||||
"curly": "error",
|
||||
"no-unused-vars": ["error", {
|
||||
"argsIgnorePattern": "^_|next",
|
||||
"varsIgnorePattern": "^_"
|
||||
}],
|
||||
"no-multiple-empty-lines": ["error", {
|
||||
"max": 1
|
||||
}],
|
||||
"newline-per-chained-call": ["error", {
|
||||
"ignoreChainWithDepth": 2
|
||||
}],
|
||||
"promise/no-return-wrap": "error",
|
||||
"promise/param-names": "error",
|
||||
"promise/catch-or-return": "error",
|
||||
"promise/no-native": "off",
|
||||
"promise/no-nesting": "warn",
|
||||
"promise/no-promise-in-callback": "warn",
|
||||
"promise/no-callback-in-promise": "warn"
|
||||
}
|
||||
"extends": "@coralproject/eslint-config-talk"
|
||||
}
|
||||
|
||||
+12
@@ -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/*
|
||||
@@ -29,6 +33,7 @@ plugins/*
|
||||
!plugins/talk-plugin-comment-content
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-featured-comments
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-sort-newest
|
||||
!plugins/talk-plugin-sort-oldest
|
||||
!plugins/talk-plugin-sort-most-replied
|
||||
@@ -38,5 +43,12 @@ plugins/*
|
||||
!plugins/talk-plugin-author-menu
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-ignore-user
|
||||
!plugins/talk-plugin-moderation-actions
|
||||
!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/*
|
||||
|
||||
@@ -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.
|
||||
@@ -1,26 +1,31 @@
|
||||
# Talk [](https://circleci.com/gh/coralproject/talk)
|
||||
# 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)
|
||||
[](https://nodesecurity.io/orgs/coralproject/projects/07ce2e4c-99fb-48f8-b50b-69d2d2c081b8)
|
||||
|
||||
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 Talk tool rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here](https://coralproject.net/products/talk.html).
|
||||
|
||||
Third party licenses are available via the `/client/3rdpartylicenses.txt`
|
||||
endpoint when the server is running with built assets.
|
||||
Built with <3 by The Coral Project & Mozilla.
|
||||
|
||||
## Important Links
|
||||
## Getting Started
|
||||
|
||||
- Developer Documentation & Setup Guides: https://coralproject.github.io/talk/
|
||||
Check out our Docs: https://coralproject.github.io/talk/
|
||||
|
||||
- Pivotal Tracker Backlog & Release Schedule: https://www.pivotaltracker.com/n/projects/1863625
|
||||
|
||||
## Learn More about Coral
|
||||
## Relevant Links
|
||||
|
||||
- Blog: https://blog.coralproject.net/
|
||||
- Community Forums: https://community.coralproject.net/
|
||||
|
||||
- Website: https://coralproject.net
|
||||
|
||||
- Blog: https://blog.coralproject.net
|
||||
|
||||
- Community Guides for Journalism: https://guides.coralproject.net/
|
||||
- Project: https://coralproject.net/
|
||||
- Roadmap: https://www.pivotaltracker.com/n/projects/1863625
|
||||
|
||||
## End-to-End Testing
|
||||
|
||||
Talk uses [Nightwatch](http://nightwatchjs.org/) to write e2e tests. The testing infrastructure that allows us to run our tests in real browsers is provided with love by our friends at Browserstack.
|
||||
|
||||
|
||||

|
||||
|
||||
## License
|
||||
|
||||
@@ -30,8 +35,12 @@ endpoint when the server is running with built assets.
|
||||
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.
|
||||
|
||||
@@ -2,10 +2,13 @@ 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} = require('./url');
|
||||
const {HELMET_CONFIGURATION} = require('./config');
|
||||
const {MOUNT_PATH} = require('./url');
|
||||
const {applyLocals} = require('./services/locals');
|
||||
const routes = require('./routes');
|
||||
const debug = require('debug')('talk:app');
|
||||
|
||||
@@ -26,9 +29,9 @@ app.set('trust proxy', 1);
|
||||
|
||||
// Enable a suite of security good practices through helmet. We disable
|
||||
// frameguard to allow crossdomain injection of the embed.
|
||||
app.use(helmet({
|
||||
app.use(helmet(merge(HELMET_CONFIGURATION, {
|
||||
frameguard: false,
|
||||
}));
|
||||
})));
|
||||
|
||||
// Compress the responses if appropriate.
|
||||
app.use(compression());
|
||||
@@ -50,11 +53,8 @@ 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;
|
||||
// Add the locals to the app renderer.
|
||||
applyLocals(app.locals);
|
||||
|
||||
debug(`mounting routes on the ${MOUNT_PATH} path`);
|
||||
|
||||
|
||||
+3
-175
@@ -1,161 +1,7 @@
|
||||
#!/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 +12,6 @@ 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});
|
||||
|
||||
// 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!');
|
||||
|
||||
+31
-1
@@ -241,13 +241,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 +404,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 +492,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"
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import {Router, Route, IndexRedirect, IndexRoute} from 'react-router';
|
||||
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';
|
||||
@@ -22,7 +22,7 @@ const routes = (
|
||||
|
||||
{/* 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};
|
||||
};
|
||||
@@ -128,18 +128,18 @@ const checkInstallRequest = () => ({type: actions.CHECK_INSTALL_REQUEST});
|
||||
const checkInstallSuccess = (installed) => ({type: actions.CHECK_INSTALL_SUCCESS, installed});
|
||||
const checkInstallFailure = (error) => ({type: actions.CHECK_INSTALL_FAILURE, error});
|
||||
|
||||
export const checkInstall = (next) => (dispatch, _, {rest}) => {
|
||||
export const checkInstall = (next) => async (dispatch, _, {rest}) => {
|
||||
dispatch(checkInstallRequest());
|
||||
rest('/setup')
|
||||
.then(({installed}) => {
|
||||
dispatch(checkInstallSuccess(installed));
|
||||
if (installed) {
|
||||
next();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(checkInstallFailure(errorMessage));
|
||||
});
|
||||
|
||||
try {
|
||||
const {installed} = await rest('/setup');
|
||||
dispatch(checkInstallSuccess(installed));
|
||||
if (installed) {
|
||||
next();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(checkInstallFailure(errorMessage));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,10 +4,8 @@ export const viewUserDetail = (userId) => ({type: actions.VIEW_USER_DETAIL, user
|
||||
export const hideUserDetail = () => ({type: actions.HIDE_USER_DETAIL});
|
||||
|
||||
export const changeUserDetailStatuses = (tab) => {
|
||||
let statuses;
|
||||
if (tab === 'all') {
|
||||
statuses = ['NONE', 'ACCEPTED', 'REJECTED', 'PREMOD'];
|
||||
} else if (tab === 'rejected') {
|
||||
let statuses = null;
|
||||
if (tab === 'rejected') {
|
||||
statuses = ['REJECTED'];
|
||||
}
|
||||
return {type: actions.CHANGE_USER_DETAIL_STATUSES, tab, statuses};
|
||||
@@ -22,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,32 +0,0 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
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
|
||||
};
|
||||
|
||||
export default ActionButton;
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Button, Icon} from 'coral-ui';
|
||||
import {Menu} from 'react-mdl';
|
||||
import cn from 'classnames';
|
||||
@@ -57,6 +58,7 @@ class ActionsMenu extends React.Component {
|
||||
|
||||
ActionsMenu.propTypes = {
|
||||
icon: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default ActionsMenu;
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import {MenuItem} from 'react-mdl';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './ActionsMenu.css';
|
||||
|
||||
const ActionsMenuItem = (props) =>
|
||||
<MenuItem className={cn(styles.menuItem, props.className)} {...props} />;
|
||||
|
||||
ActionsMenuItem.propTypes = {
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ActionsMenuItem;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
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 {
|
||||
|
||||
@@ -33,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
|
||||
@@ -89,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 }
|
||||
@@ -104,7 +109,9 @@ AdminLogin.propTypes = {
|
||||
handleLogin: PropTypes.func.isRequired,
|
||||
passwordRequestSuccess: PropTypes.string,
|
||||
loginError: PropTypes.string,
|
||||
recaptchaPublic: PropTypes.string
|
||||
recaptchaPublic: PropTypes.string,
|
||||
requestPasswordReset: PropTypes.func,
|
||||
errorMessage: PropTypes.string,
|
||||
};
|
||||
|
||||
export default AdminLogin;
|
||||
|
||||
@@ -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,30 @@
|
||||
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}) => {
|
||||
const text = active ? t('modqueue.approved') : t('modqueue.approve');
|
||||
return (
|
||||
<button
|
||||
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active})}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon name={'done'} className={styles.icon} />
|
||||
{!minimal && text}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
ApproveButton.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
minimal: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ApproveButton;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Dialog} from 'coral-ui';
|
||||
import styles from './BanUserDialog.css';
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bodyLeave {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
||||
@@ -2,11 +2,13 @@ import React from 'react';
|
||||
import {murmur3} from 'murmurhash-js';
|
||||
import {CSSTransitionGroup} from 'react-transition-group';
|
||||
import styles from './CommentAnimatedEdit.css';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default ({children, body}) => {
|
||||
const CommentBodyHighlighter = ({children, body}) => {
|
||||
return (
|
||||
<CSSTransitionGroup
|
||||
component={'div'}
|
||||
className={styles.root}
|
||||
transitionName={{
|
||||
enter: styles.bodyEnter,
|
||||
enterActive: styles.bodyEnterActive,
|
||||
@@ -22,3 +24,10 @@ export default ({children, body}) => {
|
||||
</CSSTransitionGroup>
|
||||
);
|
||||
};
|
||||
|
||||
CommentBodyHighlighter.propTypes = {
|
||||
children: PropTypes.node,
|
||||
body: PropTypes.string,
|
||||
};
|
||||
|
||||
export default CommentBodyHighlighter;
|
||||
|
||||
@@ -1,25 +1,78 @@
|
||||
import React from 'react';
|
||||
import Highlighter from 'react-highlight-words';
|
||||
import Linkify from 'react-linkify';
|
||||
const linkify = new Linkify();
|
||||
import {matchLinks} from '../utils';
|
||||
import memoize from 'lodash/memoize';
|
||||
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
|
||||
// generate a regulare expression that catches the `phrases`.
|
||||
function generateRegExp(phrases) {
|
||||
const inner = phrases
|
||||
.map((phrase) =>
|
||||
phrase.split(/\s+/)
|
||||
.map((word) => escapeRegExp(word))
|
||||
.join('[\\s"?!.]+')
|
||||
).join('|');
|
||||
|
||||
return new RegExp(`(^|[^\\w])(${inner})(?=[^\\w]|$)`, 'iu');
|
||||
}
|
||||
|
||||
// Generate a regular expression detecting `suspectWords` and `bannedWords` phrases.
|
||||
function getPhrasesRegexp(suspectWords, bannedWords) {
|
||||
return generateRegExp([...suspectWords, ...bannedWords]);
|
||||
}
|
||||
|
||||
// Memoized version as arguments rarely change.
|
||||
const getPhrasesRegexpMemoized = memoize(getPhrasesRegexp);
|
||||
|
||||
// markPhrases looks for `supsectWords` and `bannedWords` inside `body` and highlights them by returning
|
||||
// an array of React Elements.
|
||||
function markPhrases(body, suspectWords, bannedWords, keyPrefix) {
|
||||
const regexp = getPhrasesRegexpMemoized(suspectWords, bannedWords);
|
||||
const tokens = body.split(regexp);
|
||||
return tokens.map((token, i) =>
|
||||
i % 3 === 2
|
||||
? <mark key={`${keyPrefix}_${i}`}>{token}</mark>
|
||||
: token
|
||||
);
|
||||
}
|
||||
|
||||
// markLinks looks for links inside `body` and highlights them by returning
|
||||
// an array of React Elements.
|
||||
function markLinks(body) {
|
||||
const matches = matchLinks(body);
|
||||
const content = [];
|
||||
let index = 0;
|
||||
if (matches) {
|
||||
matches
|
||||
.forEach((match, i) => {
|
||||
content.push(body.substring(index, match.index));
|
||||
content.push(<mark key={i}>{match.text}</mark>);
|
||||
index = match.lastIndex;
|
||||
});
|
||||
}
|
||||
content.push(body.substring(index));
|
||||
return content;
|
||||
}
|
||||
|
||||
export default ({suspectWords, bannedWords, body, ...rest}) => {
|
||||
|
||||
const links = linkify.getMatches(body);
|
||||
const linkText = links ? links.map((link) => link.raw) : [];
|
||||
// First highlight links.
|
||||
const content = markLinks(body)
|
||||
.map((element, index) => {
|
||||
|
||||
const searchWords = [
|
||||
...suspectWords,
|
||||
...bannedWords,
|
||||
...linkText
|
||||
];
|
||||
// Keep highlighted links.
|
||||
if (typeof element !== 'string') {
|
||||
return element;
|
||||
}
|
||||
|
||||
// Highlight suspect and banned phrase inside this part of text.
|
||||
return markPhrases(element, suspectWords, bannedWords, index);
|
||||
});
|
||||
return (
|
||||
<Highlighter
|
||||
{...rest}
|
||||
autoEscape={true}
|
||||
searchWords={searchWords}
|
||||
textToHighlight={body}
|
||||
/>
|
||||
<div {...rest}>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
|
||||
.textareaContainer {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
|
||||
import React from 'react';
|
||||
import styles from './CommentBox.css';
|
||||
import {Button} from 'react-mdl';
|
||||
|
||||
// Renders a comment box for creating a new comment
|
||||
export default class CommentBox extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {name: '', body: ''};
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
}
|
||||
|
||||
onSubmit () {
|
||||
const {name, body} = this.state;
|
||||
this.props.onSubmit({name, body});
|
||||
this.setState({body: '', name: ''});
|
||||
}
|
||||
|
||||
render (props, {name, body}) {
|
||||
return (
|
||||
<div>
|
||||
<div class={`${styles.textareaContainer} mdl-textfield mdl-js-textfield`}>
|
||||
<input type='text' value={name} onInput={this.linkState('name')} class='mdl-textfield__input' id='name' />
|
||||
<label class='mdl-textfield__label' for='name'>Your name</label>
|
||||
</div>
|
||||
<div class={`${styles.textareaContainer} mdl-textfield mdl-js-textfield`}>
|
||||
<textarea value={body} onInput={this.linkState('body')} class='mdl-textfield__input' type='text' rows='5' id='comment' />
|
||||
<label class='mdl-textfield__label' for='comment'>Write your comment</label>
|
||||
</div>
|
||||
<Button onClick={this.onSubmit} raised>Post</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,27 @@
|
||||
.root {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.coreLabels {
|
||||
> *:not(:last-child) {
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.slot {
|
||||
&:not(:empty) {
|
||||
padding-left: 3px;
|
||||
}
|
||||
> *:not(:last-child) {
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.replyLabel {
|
||||
background-color: #3D73D5;
|
||||
}
|
||||
|
||||
.premodLabel {
|
||||
background-color: #063B9A;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Label from 'coral-ui/components/Label';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import FlagLabel from 'coral-ui/components/FlagLabel';
|
||||
import cn from 'classnames';
|
||||
import styles from './CommentLabels.css';
|
||||
|
||||
function isUserFlagged(actions) {
|
||||
return actions.some((action) => action.__typename === 'FlagAction' && action.user);
|
||||
}
|
||||
|
||||
function hasSuspectedWords(actions) {
|
||||
return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'Matched suspect word filter');
|
||||
}
|
||||
|
||||
function hasHistoryFlag(actions) {
|
||||
return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'TRUST');
|
||||
}
|
||||
|
||||
const CommentLabels = ({comment, comment: {className, status, actions, hasParent}}) => {
|
||||
return (
|
||||
<div className={cn(className, styles.root)}>
|
||||
<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>}
|
||||
{hasSuspectedWords(actions) && <FlagLabel iconName="sms_failed">Suspect</FlagLabel>}
|
||||
{hasHistoryFlag(actions) && <FlagLabel iconName="sentiment_very_dissatisfied">History</FlagLabel>}
|
||||
</div>
|
||||
<Slot className={styles.slot} fill="adminCommentLabels" queryData={{comment}} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CommentLabels.propTypes = {
|
||||
comment: PropTypes.shape({
|
||||
className: PropTypes.string,
|
||||
status: PropTypes.string,
|
||||
actions: PropTypes.array,
|
||||
hasParent: PropTypes.bool,
|
||||
}),
|
||||
};
|
||||
|
||||
export default CommentLabels;
|
||||
@@ -1,30 +0,0 @@
|
||||
.commentType {
|
||||
display: inline-block;
|
||||
color: white;
|
||||
background: grey;
|
||||
box-sizing: border-box;
|
||||
padding: 2px 5px;
|
||||
font-size: 12px;
|
||||
height: 24px;
|
||||
letter-spacing: 0.4px;
|
||||
line-height: 22px;
|
||||
|
||||
> i {
|
||||
font-size: 14px;
|
||||
vertical-align: text-top;
|
||||
margin: 0;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&.premod {
|
||||
background: #063B9A;
|
||||
}
|
||||
|
||||
&.flagged {
|
||||
background: #d03235;
|
||||
}
|
||||
|
||||
&.no-type {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from './CommentType.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
import cn from 'classnames';
|
||||
|
||||
const CommentType = (props) => {
|
||||
const typeData = getTypeData(props.type);
|
||||
|
||||
return (
|
||||
<span className={cn(styles.commentType, styles[typeData.className], props.className)}>
|
||||
<Icon name={typeData.icon}/>{typeData.text}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const getTypeData = (type) => {
|
||||
switch (type) {
|
||||
case 'premod':
|
||||
return {icon: 'query_builder', text: 'Pre-Mod', className: 'premod'};
|
||||
case 'flagged':
|
||||
return {icon: 'flag', text: 'Flagged', className: 'flagged'};
|
||||
default:
|
||||
return {icon: 'flag', className: 'no-type'};
|
||||
}
|
||||
};
|
||||
|
||||
CommentType.propTypes = {
|
||||
type: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default CommentType;
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
vertical-align: middle;
|
||||
padding: 1px 5px;
|
||||
border-radius: 2px;
|
||||
margin-left: 2px;
|
||||
margin-left: 5px;
|
||||
line-height: 18px;
|
||||
box-sizing: border-box;
|
||||
height: 18px;
|
||||
+6
-5
@@ -1,9 +1,10 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from './CommentCount.css';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './CountBadge.css';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const CommentCount = ({count}) => {
|
||||
const CountBadge = ({count}) => {
|
||||
let number = count;
|
||||
|
||||
// shorten large counts to abbreviations
|
||||
@@ -20,8 +21,8 @@ const CommentCount = ({count}) => {
|
||||
);
|
||||
};
|
||||
|
||||
CommentCount.propTypes = {
|
||||
CountBadge.propTypes = {
|
||||
count: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default CommentCount;
|
||||
export default CountBadge;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-4
@@ -1,12 +1,13 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Navigation, Drawer} from 'react-mdl';
|
||||
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';
|
||||
|
||||
const CoralDrawer = ({handleLogout, auth}) => (
|
||||
<Drawer className={styles.header}>
|
||||
const CoralDrawer = ({handleLogout, auth = {}}) => (
|
||||
<Drawer className={styles.drawer}>
|
||||
{ auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
|
||||
<div>
|
||||
<Navigation className={styles.nav}>
|
||||
@@ -56,7 +57,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,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Card} from 'coral-ui';
|
||||
|
||||
const EmptyCard = (props) => (
|
||||
|
||||
@@ -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,96 +0,0 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
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;
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from 'react';
|
||||
import Linkify from 'react-linkify';
|
||||
const linkify = new Linkify();
|
||||
import {matchLinks} from '../utils';
|
||||
|
||||
export default ({text, children}) => {
|
||||
const hasLinks = !!linkify.getMatches(text);
|
||||
const hasLinks = !!matchLinks(text);
|
||||
|
||||
if (!hasLinks) {
|
||||
return null;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Button} from 'coral-ui';
|
||||
import styles from './LoadMore.css';
|
||||
import cn from 'classnames';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'components/Modal';
|
||||
import styles from './ModerationKeysModal.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
@@ -9,6 +10,8 @@ const shortcuts = [
|
||||
shortcuts: {
|
||||
'j': 'modqueue.next_comment',
|
||||
'k': 'modqueue.prev_comment',
|
||||
'ctrl+f': 'modqueue.toggle_search',
|
||||
't': 'modqueue.next_queue',
|
||||
's': 'modqueue.singleview',
|
||||
'?': 'modqueue.thismenu'
|
||||
}
|
||||
|
||||
@@ -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,227 +0,0 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
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,30 @@
|
||||
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}) => {
|
||||
const text = active ? t('modqueue.rejected') : t('modqueue.reject');
|
||||
return (
|
||||
<button
|
||||
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active})}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon name={'close'} className={styles.icon} />
|
||||
{!minimal && text}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
RejectButton.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
minimal: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default RejectButton;
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Badge} from 'coral-ui';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const ReplyBadge = () => (
|
||||
<Badge icon="reply">
|
||||
{t('modqueue.reply')}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
export default ReplyBadge;
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Dialog} from 'coral-ui';
|
||||
import {RadioGroup, Radio} from 'react-mdl';
|
||||
import styles from './SuspendUserDialog.css';
|
||||
|
||||
@@ -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,26 +20,66 @@ 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 = (info) => {
|
||||
this.props.rejectComment(info).then(() => {
|
||||
rejectThenReload = async (info) => {
|
||||
try {
|
||||
await this.props.rejectComment(info);
|
||||
this.props.data.refetch();
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
console.error(err);
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
}
|
||||
|
||||
acceptThenReload = (info) => {
|
||||
this.props.acceptComment(info).then(() => {
|
||||
acceptThenReload = async (info) => {
|
||||
try {
|
||||
await this.props.acceptComment(info);
|
||||
this.props.data.refetch();
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
showAll = () => {
|
||||
@@ -60,6 +102,7 @@ export default class UserDetail extends React.Component {
|
||||
|
||||
renderLoaded() {
|
||||
const {
|
||||
data,
|
||||
root,
|
||||
root: {
|
||||
user,
|
||||
@@ -69,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;
|
||||
@@ -94,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>
|
||||
@@ -110,73 +150,74 @@ 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
|
||||
fill="userProfile"
|
||||
data={this.props.data}
|
||||
queryData={root, user}
|
||||
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}
|
||||
selected={false}
|
||||
suspectWords={suspectWords}
|
||||
bannedWords={bannedWords}
|
||||
actions={actionsMap[status]}
|
||||
acceptComment={this.acceptThenReload}
|
||||
rejectComment={this.rejectThenReload}
|
||||
selected={selected}
|
||||
@@ -196,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;
|
||||
@@ -55,7 +56,7 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.badgeBar {
|
||||
.labels {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
@@ -1,41 +1,42 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Link} from 'react-router';
|
||||
|
||||
import {Icon} from 'coral-ui';
|
||||
import FlagBox from './FlagBox';
|
||||
import ReplyBadge from './ReplyBadge';
|
||||
import CommentDetails from './CommentDetails';
|
||||
import styles from './UserDetailComment.css';
|
||||
import CommentType from './CommentType';
|
||||
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 {getCommentType} from 'coral-admin/src/utils/comment';
|
||||
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');
|
||||
const commentType = getCommentType(comment);
|
||||
|
||||
return (
|
||||
<li
|
||||
tabIndex={0}
|
||||
@@ -58,9 +59,8 @@ class UserDetailComment extends React.Component {
|
||||
: null
|
||||
}
|
||||
|
||||
<div className={styles.badgeBar}>
|
||||
{comment.hasParent && <ReplyBadge/>}
|
||||
<CommentType type={commentType}/>
|
||||
<div className={styles.labels}>
|
||||
<CommentLabels comment={comment} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.story}>
|
||||
@@ -69,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}
|
||||
/>
|
||||
{' '}
|
||||
@@ -83,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}>
|
||||
@@ -91,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>
|
||||
);
|
||||
}
|
||||
@@ -137,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,105 +1,118 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
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';
|
||||
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') && (
|
||||
<Link
|
||||
id='moderateNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/moderate"
|
||||
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}>
|
||||
<IndexLink
|
||||
id='dashboardNav'
|
||||
className={cn('talk-admin-nav-dashboard', styles.navLink)}
|
||||
to="/admin/dashboard"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.moderate')}
|
||||
</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') && (
|
||||
{t('configure.dashboard')}
|
||||
</IndexLink>
|
||||
{
|
||||
can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
<Link
|
||||
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 />}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
<Link
|
||||
id='configureNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/configure"
|
||||
id='storiesNav'
|
||||
className={cn('talk-admin-nav-stories', styles.navLink)}
|
||||
to="/admin/stories"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.configure')}
|
||||
{t('configure.stories')}
|
||||
</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">
|
||||
View latest version
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://coralproject.net/contribute.html#other-ideas-and-bug-reports" target="_blank">
|
||||
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>
|
||||
|
||||
<Link
|
||||
id='communityNav'
|
||||
className={cn('talk-admin-nav-community', styles.navLink)}
|
||||
to="/admin/community"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.community')}
|
||||
{root.flaggedUsernamesCount !== 0 && <Indicator />}
|
||||
</Link>
|
||||
|
||||
{
|
||||
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={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>
|
||||
</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,21 +1,28 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
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>
|
||||
@@ -23,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';
|
||||
|
||||
@@ -6,15 +6,22 @@ import {hideBanUserDialog} from '../actions/banUserDialog';
|
||||
import {withSetUserStatus, withSetCommentStatus} from 'coral-framework/graphql/mutations';
|
||||
import {compose} from 'react-apollo';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {getErrorMessages} from 'coral-framework/utils';
|
||||
import {notify} from 'coral-framework/actions/notification';
|
||||
|
||||
class BanUserDialogContainer extends Component {
|
||||
|
||||
banUser = async () => {
|
||||
const {userId, commentId, commentStatus, setUserStatus, setCommentStatus, hideBanUserDialog} = this.props;
|
||||
await setUserStatus({userId, status: 'BANNED'});
|
||||
hideBanUserDialog();
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
await setCommentStatus({commentId, status: 'REJECTED'});
|
||||
const {userId, commentId, commentStatus, setUserStatus, setCommentStatus, hideBanUserDialog, notify} = this.props;
|
||||
try {
|
||||
await setUserStatus({userId, status: 'BANNED'});
|
||||
hideBanUserDialog();
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
await setCommentStatus({commentId, status: 'REJECTED'});
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
notify('error', getErrorMessages(err));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +57,7 @@ const mapStateToProps = ({banUserDialog: {open, userId, username, commentId, com
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
...bindActionCreators({
|
||||
hideBanUserDialog,
|
||||
notify,
|
||||
}, dispatch),
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -0,0 +1,33 @@
|
||||
import {gql} from 'react-apollo';
|
||||
import CommentLabels from '../components/CommentLabels';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
import {getSlotFragmentSpreads} from 'coral-framework/utils';
|
||||
|
||||
const slots = [
|
||||
'adminCommentLabels',
|
||||
];
|
||||
|
||||
export default withFragments({
|
||||
root: gql`
|
||||
fragment CoralAdmin_CommentLabels_root on RootQuery {
|
||||
__typename
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
}
|
||||
`,
|
||||
comment: gql`
|
||||
fragment CoralAdmin_CommentLabels_comment on Comment {
|
||||
hasParent
|
||||
status
|
||||
actions {
|
||||
__typename
|
||||
... on FlagAction {
|
||||
reason
|
||||
}
|
||||
user {
|
||||
id
|
||||
}
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'comment')}
|
||||
}
|
||||
`
|
||||
})(CommentLabels);
|
||||
@@ -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: 5000
|
||||
}
|
||||
})(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);
|
||||
|
||||
@@ -17,21 +17,13 @@ class SuspendUserDialogContainer extends Component {
|
||||
const {userId, username, commentStatus, commentId, hideSuspendUserDialog, setCommentStatus, suspendUser, notify} = this.props;
|
||||
hideSuspendUserDialog();
|
||||
try {
|
||||
const result = await suspendUser({id: userId, message, until});
|
||||
if (result.data.suspendUser.errors) {
|
||||
throw result.data.suspendUser.errors;
|
||||
}
|
||||
await suspendUser({id: userId, message, until});
|
||||
notify(
|
||||
'success',
|
||||
t('suspenduser.notify_suspend_until', username, timeago(until)),
|
||||
);
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
return setCommentStatus({commentId, status: 'REJECTED'})
|
||||
.then((result) => {
|
||||
if (result.data.setCommentStatus.errors) {
|
||||
throw result.data.setCommentStatus.errors;
|
||||
}
|
||||
});
|
||||
await setCommentStatus({commentId, status: 'REJECTED'});
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
|
||||
@@ -11,10 +11,12 @@ 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 {
|
||||
@@ -36,22 +38,21 @@ class UserDetailContainer extends React.Component {
|
||||
isLoadingMore = false;
|
||||
|
||||
// status can be 'ACCEPTED' or 'REJECTED'
|
||||
bulkSetCommentStatus = (status) => {
|
||||
bulkSetCommentStatus = async (status) => {
|
||||
const changes = this.props.selectedCommentIds.map((commentId) => {
|
||||
return this.props.setCommentStatus({commentId, status});
|
||||
});
|
||||
|
||||
Promise.all(changes).then(() => {
|
||||
this.props.clearUserDetailSelections(); // un-select everything
|
||||
});
|
||||
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}) => {
|
||||
@@ -115,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}
|
||||
@@ -147,7 +149,7 @@ 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,
|
||||
@@ -155,13 +157,16 @@ export const withUserDetailQuery = withQuery(gql`
|
||||
}) {
|
||||
...CoralAdmin_Moderation_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,
|
||||
@@ -172,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) => ({
|
||||
@@ -183,6 +186,8 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
toggleSelectCommentInUserDetail,
|
||||
viewUserDetail,
|
||||
hideUserDetail,
|
||||
toggleSelectAllCommentInUserDetail,
|
||||
notify
|
||||
}, dispatch)
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
import {gql} from 'react-apollo';
|
||||
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
|
||||
@@ -15,26 +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,21 @@
|
||||
import update from 'immutability-helper';
|
||||
import mapValues from 'lodash/mapValues';
|
||||
|
||||
// Map nested object leaves. Array objects are considered leaves.
|
||||
function mapLeaves(o, mapper) {
|
||||
return mapValues(o, (val) => {
|
||||
if (typeof val === 'object' && !Array.isArray(val)) {
|
||||
return mapLeaves(val, mapper);
|
||||
}
|
||||
return mapper(val);
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
mutations: {
|
||||
SetUserStatus: ({variables: {status, userId}}) => ({
|
||||
updateQueries: {
|
||||
TalkAdmin_FlaggedAccounts: (prev) => {
|
||||
TalkAdmin_Community: (prev) => {
|
||||
if (status !== 'APPROVED') {
|
||||
return prev;
|
||||
}
|
||||
@@ -19,7 +30,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 +40,16 @@ export default {
|
||||
}
|
||||
}
|
||||
}),
|
||||
UpdateSettings: ({variables: {input}}) => ({
|
||||
updateQueries: {
|
||||
TalkAdmin_Configure: (prev) => {
|
||||
const updated = update(prev, {
|
||||
settings: mapLeaves(input, (leaf) => ({$set: leaf})),
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -12,23 +12,24 @@ import {toast} from 'react-toastify';
|
||||
import {createNotificationService} from './services/notification';
|
||||
import {hideShortcutsNote} from './actions/moderation';
|
||||
|
||||
function hidrateStore({store, storage}) {
|
||||
smoothscroll.polyfill();
|
||||
|
||||
function init({store, storage}) {
|
||||
if (storage && storage.getItem('coral:shortcutsNote') === 'hide') {
|
||||
store.dispatch(hideShortcutsNote());
|
||||
}
|
||||
}
|
||||
|
||||
smoothscroll.polyfill();
|
||||
async function main() {
|
||||
const notification = createNotificationService(toast);
|
||||
const context = await createContext({reducers, graphqlExtension, pluginsConfig, notification, init});
|
||||
|
||||
const notification = createNotificationService(toast);
|
||||
const context = createContext({reducers, graphqlExtension, pluginsConfig, notification});
|
||||
render(
|
||||
<TalkProvider {...context}>
|
||||
<App />
|
||||
</TalkProvider>
|
||||
, document.querySelector('#root')
|
||||
);
|
||||
}
|
||||
|
||||
// hidrate Store with external data.
|
||||
hidrateStore(context);
|
||||
|
||||
render(
|
||||
<TalkProvider {...context}>
|
||||
<App />
|
||||
</TalkProvider>
|
||||
, document.querySelector('#root')
|
||||
);
|
||||
main();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// this is initialized here because
|
||||
// currently you have to reload the dashboard to get new stats
|
||||
// cleaner updates are planned in the future.
|
||||
const DASHBOARD_WINDOW_MINUTES = 5;
|
||||
let then = new Date();
|
||||
then.setMinutes(then.getMinutes() - DASHBOARD_WINDOW_MINUTES);
|
||||
|
||||
const initialState = {
|
||||
windowStart: then.toISOString(),
|
||||
windowEnd: new Date().toISOString(),
|
||||
};
|
||||
|
||||
export default function dashboard (state = initialState, _action) {
|
||||
return state;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import auth from './auth';
|
||||
import assets from './assets';
|
||||
import settings from './settings';
|
||||
import stories from './stories';
|
||||
import dashboard from './dashboard';
|
||||
import configure from './configure';
|
||||
import community from './community';
|
||||
import moderation from './moderation';
|
||||
import install from './install';
|
||||
@@ -12,10 +13,11 @@ import userDetail from './userDetail';
|
||||
export default {
|
||||
auth,
|
||||
banUserDialog,
|
||||
dashboard,
|
||||
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: ['NONE', 'ACCEPTED', 'REJECTED', 'PREMOD'],
|
||||
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,104 +1,57 @@
|
||||
import React, {Component} from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Community.css';
|
||||
import People from '../containers/People';
|
||||
import CommunityMenu from './CommunityMenu';
|
||||
import People from './People';
|
||||
import FlaggedAccounts from '../containers/FlaggedAccounts';
|
||||
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 (
|
||||
<div>
|
||||
<FlaggedAccounts />
|
||||
<FlaggedAccounts
|
||||
data={this.props.data}
|
||||
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>
|
||||
<CommunityMenu />
|
||||
<div>
|
||||
{ tab }
|
||||
<div className="talk-admin-community">
|
||||
<CommunityMenu flaggedUsernamesCount={flaggedUsernamesCount} />
|
||||
<div className={styles.container}>
|
||||
{this.renderTab()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Community.propTypes = {
|
||||
route: PropTypes.object,
|
||||
community: PropTypes.object,
|
||||
rejectUsername: PropTypes.func.isRequired,
|
||||
hideRejectUsernameDialog: PropTypes.func.isRequired,
|
||||
data: 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;
|
||||
@@ -1,18 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
import styles from './CommunityMenu.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {Link} from 'react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
import CountBadge from '../../../components/CountBadge';
|
||||
|
||||
const CommunityMenu = () => {
|
||||
const CommunityMenu = ({flaggedUsernamesCount = 0}) => {
|
||||
const flaggedPath = '/admin/community/flagged';
|
||||
const peoplePath = '/admin/community/people';
|
||||
|
||||
return (
|
||||
<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}>
|
||||
{t('community.flaggedaccounts')}
|
||||
<CountBadge count={flaggedUsernamesCount} />
|
||||
</Link>
|
||||
<Link to={peoplePath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
|
||||
{t('community.people')}
|
||||
@@ -23,4 +26,8 @@ const CommunityMenu = () => {
|
||||
);
|
||||
};
|
||||
|
||||
CommunityMenu.propTypes = {
|
||||
flaggedUsernamesCount: PropTypes.number,
|
||||
};
|
||||
|
||||
export default CommunityMenu;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
padding-bottom: 200px;
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.mainFlaggedContent {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user