mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff72d79748 | ||
|
|
0645735d2b | ||
|
|
f1a0febd6c | ||
|
|
16d0b39ebc | ||
|
|
66c7430ff1 | ||
|
|
ac06f0d13b | ||
|
|
ca10062498 | ||
|
|
c9381c6367 | ||
|
|
13853ef87e | ||
|
|
fb2f36d0ee | ||
|
|
1a2fa73941 | ||
|
|
d3931db285 | ||
|
|
093202f9e2 | ||
|
|
60be682a5f | ||
|
|
41c9ae32b1 | ||
|
|
4445479237 | ||
|
|
aab44587a6 | ||
|
|
0af28edbd9 | ||
|
|
9db7571fe2 | ||
|
|
683adceec4 | ||
|
|
aa6058e9d1 | ||
|
|
a4c4032b0d | ||
|
|
2ea0d51613 | ||
|
|
f2564f4eda | ||
|
|
73040b1314 | ||
|
|
c3c106c7b0 | ||
|
|
8faf69ce48 | ||
|
|
23f17db0c9 | ||
|
|
1385a0b961 | ||
|
|
7fd01e5845 | ||
|
|
340052cdf0 | ||
|
|
a085e4b6f9 | ||
|
|
1575b15e36 | ||
|
|
7da9126a76 | ||
|
|
9453d207f5 | ||
|
|
869c760a8b | ||
|
|
b283595c97 | ||
|
|
d672af63f9 | ||
|
|
b4ad78fd65 | ||
|
|
713de46c2a | ||
|
|
fc1e51ed62 | ||
|
|
4eef6f4218 | ||
|
|
c8cfd7c0f8 | ||
|
|
0e4cbf5f44 | ||
|
|
448b988281 | ||
|
|
7a688495f2 | ||
|
|
b9cf7084eb | ||
|
|
b99161f0cb | ||
|
|
6faa4862de | ||
|
|
eff3f1dfd8 | ||
|
|
73aa77d7da | ||
|
|
5580ab385f | ||
|
|
40eb0e97ec | ||
|
|
b7e1ce0205 | ||
|
|
bfde9bf8c7 | ||
|
|
54f9b28086 | ||
|
|
03a5eb7f9b | ||
|
|
43e60e6544 | ||
|
|
ebd16666e6 | ||
|
|
f2129082a9 | ||
|
|
1098c068f2 | ||
|
|
f0c95d0044 | ||
|
|
74226aa6fc | ||
|
|
b6ed5b792b | ||
|
|
d31969592c | ||
|
|
794487b1a9 | ||
|
|
39e45c30ba | ||
|
|
ed734ebb08 | ||
|
|
9f059e0c4a | ||
|
|
7d369f2d35 | ||
|
|
5fb5240f89 | ||
|
|
976931135c | ||
|
|
1e3d47eb87 | ||
|
|
2d804d783b | ||
|
|
3404578ecc | ||
|
|
48c5355a5b | ||
|
|
e3a300c643 | ||
|
|
a6653152e4 | ||
|
|
441422542c | ||
|
|
1d1822c086 | ||
|
|
1c2ff7d2ae | ||
|
|
9319bb4e1f | ||
|
|
6611f8b636 | ||
|
|
738542ab99 | ||
|
|
cd4ae47958 | ||
|
|
2ec66007d9 | ||
|
|
ea34c936d0 | ||
|
|
ef1fd9c159 | ||
|
|
c7ccf03132 | ||
|
|
8015098305 | ||
|
|
c85a2703f9 | ||
|
|
9aec6b026c | ||
|
|
b2ca9dba40 | ||
|
|
9186c73fff | ||
|
|
86d55fa2de | ||
|
|
12c4cfbbf6 | ||
|
|
c875a8d9ed | ||
|
|
4d082165aa | ||
|
|
08c01981ac | ||
|
|
cd01aae76c | ||
|
|
53367eeaac | ||
|
|
32962aa1e8 | ||
|
|
049357a09c | ||
|
|
5e7fe7cf5a | ||
|
|
6243fcba91 | ||
|
|
805825e5d2 | ||
|
|
a40e721a68 | ||
|
|
1dd8d0a706 | ||
|
|
da1a8bb539 | ||
|
|
18add4cdea | ||
|
|
de5de26c82 | ||
|
|
934e4b4754 | ||
|
|
049658508c | ||
|
|
f0ead66f68 | ||
|
|
14f1343930 | ||
|
|
b21845b963 | ||
|
|
0ff20bed5e | ||
|
|
0bb2d4cbca | ||
|
|
927a34a579 | ||
|
|
75f2f05534 | ||
|
|
2998db46ad | ||
|
|
8163b52301 | ||
|
|
19b425dc1a | ||
|
|
be81ced2f5 | ||
|
|
f6e5f24722 | ||
|
|
2c1dc9c118 | ||
|
|
657bf388cf | ||
|
|
b3412a46cf | ||
|
|
c6ee54c2f5 | ||
|
|
174a80c377 | ||
|
|
17a05059d9 | ||
|
|
8c53c25a89 | ||
|
|
1928a7b02e | ||
|
|
5173af2a27 | ||
|
|
9077f31872 | ||
|
|
51e8035533 | ||
|
|
94786c2711 | ||
|
|
456abaf862 | ||
|
|
fe9f92d3fc | ||
|
|
a49fbf2d05 | ||
|
|
51bb158446 | ||
|
|
d601bd1466 | ||
|
|
8eb8294987 | ||
|
|
99332323a8 | ||
|
|
1a23cc1fa7 | ||
|
|
798f55ed79 | ||
|
|
6897c0e18d | ||
|
|
9d7b9a91cd | ||
|
|
51a62b8c26 | ||
|
|
f11a51ebfc | ||
|
|
dbdf38af73 | ||
|
|
abc1664ef7 | ||
|
|
c16514502c | ||
|
|
9d3e3ce45d | ||
|
|
f6bfef3fb5 | ||
|
|
9b0173e6bf | ||
|
|
43219eed4b | ||
|
|
9300359ed1 | ||
|
|
80c3367259 | ||
|
|
ef731f719d | ||
|
|
9c09509a08 | ||
|
|
313310a097 | ||
|
|
fa42c028df | ||
|
|
905e43e745 | ||
|
|
6cc0c60ade | ||
|
|
ca44588095 | ||
|
|
9a49044030 | ||
|
|
02963dc5ac | ||
|
|
5ad90e00cb | ||
|
|
1dfc4b83c7 | ||
|
|
d522a72d42 | ||
|
|
d1a22f9dc8 | ||
|
|
5ac3910687 | ||
|
|
d007c2295e | ||
|
|
7ebcc4c793 | ||
|
|
582d45bb21 | ||
|
|
6ef5c3a42f | ||
|
|
00883a3739 | ||
|
|
d7fb0056be | ||
|
|
ad55d715f6 | ||
|
|
0a855cd3d4 | ||
|
|
c02d2b9b4e | ||
|
|
68255bec70 | ||
|
|
28edd98ff1 | ||
|
|
d1b5409e44 | ||
|
|
df5e863cc4 | ||
|
|
acc83e27ce | ||
|
|
ca13fde87d | ||
|
|
f69a9c826d | ||
|
|
76ef0f7d52 | ||
|
|
8f54c1d3c3 | ||
|
|
83379b58a7 | ||
|
|
b0b5807c8b | ||
|
|
c98d227411 | ||
|
|
83819df922 | ||
|
|
db5bf9a676 | ||
|
|
ba31dbbdf9 | ||
|
|
8f633fbe78 | ||
|
|
886222260d | ||
|
|
2c3589cdac | ||
|
|
ed953b5e7e | ||
|
|
79d1f059c2 | ||
|
|
dd0601a801 | ||
|
|
9f7d485871 | ||
|
|
ec816636e6 | ||
|
|
08f579225b | ||
|
|
46987b6303 | ||
|
|
c2d07ec79f | ||
|
|
a98f5ef980 | ||
|
|
d6515e23f8 | ||
|
|
282db6847a | ||
|
|
a3db05845d | ||
|
|
c8fc419cec | ||
|
|
677c8a0828 | ||
|
|
08146fd979 | ||
|
|
ab92fe3fe8 | ||
|
|
95435f0793 | ||
|
|
7eed540671 | ||
|
|
374951bd33 | ||
|
|
42a694f56e | ||
|
|
1c997aea31 | ||
|
|
44830f6cb1 | ||
|
|
22d263c6d2 | ||
|
|
78ba82cc05 | ||
|
|
a6b9cc57ac | ||
|
|
cd9cb56844 | ||
|
|
d331bdee18 | ||
|
|
d6f6fbcd9c | ||
|
|
1ed5df71da | ||
|
|
8b1b61f695 | ||
|
|
f1a3a5ca28 | ||
|
|
8ef44dc6f5 | ||
|
|
362f29f77e | ||
|
|
08d342ea2e | ||
|
|
99777d6b49 | ||
|
|
6f6da1fa25 | ||
|
|
2fac304163 | ||
|
|
5ca7c092db | ||
|
|
cfae6b5374 | ||
|
|
3668a2cffa | ||
|
|
92681f52aa | ||
|
|
847ded5bb4 | ||
|
|
76c959f790 | ||
|
|
4659392ad3 | ||
|
|
e9cec73966 | ||
|
|
bac0ee9c3b | ||
|
|
5ae293af89 | ||
|
|
cce464b82a | ||
|
|
5d32c4ec03 | ||
|
|
50d4e2bb9b | ||
|
|
debdd3eae4 | ||
|
|
286d9baddf | ||
|
|
9c06a63afd | ||
|
|
f38e72c4df | ||
|
|
e2fac4a2dc | ||
|
|
ba0513fd6c | ||
|
|
8e29f0094c | ||
|
|
f60c7e3ca6 | ||
|
|
b3c034d89d | ||
|
|
be2657eb30 | ||
|
|
fd32d2aa24 | ||
|
|
809b39ae2a | ||
|
|
6197981480 | ||
|
|
eb58dd52f8 | ||
|
|
f5c011f44b | ||
|
|
396ccbd6c2 | ||
|
|
4fd61b3731 | ||
|
|
cbd795dc95 | ||
|
|
65a6b484a9 | ||
|
|
45c193b9bf | ||
|
|
ed037f8461 | ||
|
|
7bb0c73ee4 | ||
|
|
f13654b297 | ||
|
|
4a165c0972 | ||
|
|
bbf034a595 | ||
|
|
9f6ddd3801 | ||
|
|
34184d07eb | ||
|
|
057d083341 | ||
|
|
c5f107a168 | ||
|
|
17a801d6d2 | ||
|
|
888a61f895 | ||
|
|
9edf1d7c41 | ||
|
|
b802fdd5eb | ||
|
|
1a7b3fe903 | ||
|
|
cadbbd0b5c | ||
|
|
b6101b880d | ||
|
|
afb9fdcdc1 | ||
|
|
53abb65da1 | ||
|
|
7b5192d40c | ||
|
|
8fdea8734a | ||
|
|
588fbb0eb9 | ||
|
|
a28024dfed | ||
|
|
343eb3e8b6 | ||
|
|
80319f9aca | ||
|
|
33e1e5e729 | ||
|
|
ea2b2bc2e3 | ||
|
|
6ca2f21bc2 | ||
|
|
0d9b795740 | ||
|
|
73482d68ac | ||
|
|
e837dfc3c1 | ||
|
|
2cd9a71d62 | ||
|
|
ad73fcd94b | ||
|
|
66145d6fd5 | ||
|
|
7d246bba76 | ||
|
|
0ce0d57a96 | ||
|
|
792bc713f2 | ||
|
|
399f5ef7ba | ||
|
|
14c0945687 | ||
|
|
52f2f48385 | ||
|
|
e52605e26b | ||
|
|
ae4d6e2424 | ||
|
|
968d20e744 | ||
|
|
49d2416172 | ||
|
|
aac7cae6d6 | ||
|
|
f4fd843efe | ||
|
|
c4de69218c | ||
|
|
94ae029a4a | ||
|
|
1dc1f91b72 | ||
|
|
c98216cf3c | ||
|
|
2c9f8586d3 | ||
|
|
4ce5d97e3b | ||
|
|
3ea2632187 | ||
|
|
c9b709ad43 | ||
|
|
dcd7145510 | ||
|
|
d3896ed97d | ||
|
|
47a3cba5f5 | ||
|
|
0f1cabe126 | ||
|
|
5f62083c2f | ||
|
|
3cabe50a7b | ||
|
|
c0efc33044 | ||
|
|
9344c1bc80 | ||
|
|
1bfe0fbac1 | ||
|
|
b3d704be76 | ||
|
|
3c100160fb | ||
|
|
49c7d2eeda | ||
|
|
cff9b29a36 | ||
|
|
5a1f93975c | ||
|
|
cd6da751a6 | ||
|
|
41610ccf57 | ||
|
|
607a59ad63 | ||
|
|
c0fea4821b | ||
|
|
8432df15a1 | ||
|
|
61486caa9e | ||
|
|
7f45f7e2e2 | ||
|
|
9e511817a8 | ||
|
|
4015e19040 | ||
|
|
c1fcc82b62 | ||
|
|
893c8d32b2 | ||
|
|
2dfadebd42 | ||
|
|
a2d5af1372 | ||
|
|
7a5416a807 | ||
|
|
8c2c632f52 | ||
|
|
fabec2c89b | ||
|
|
7f18603fd5 | ||
|
|
3a69e0cf96 | ||
|
|
7ad3d247b5 | ||
|
|
130dc578b3 | ||
|
|
fd7114ab2a | ||
|
|
5e4bdf1f0c | ||
|
|
71e1b69afe | ||
|
|
6fa18f6c7c | ||
|
|
bda9b55562 | ||
|
|
d843320c4a | ||
|
|
8a88228d9c | ||
|
|
0a9664d464 | ||
|
|
b3749b9332 | ||
|
|
aecb7de019 | ||
|
|
ade3cf6252 | ||
|
|
1837ae4a0f | ||
|
|
334074a7ca | ||
|
|
f93c4adc97 | ||
|
|
bb54009567 | ||
|
|
c126d217ad | ||
|
|
8b2dc3a46d | ||
|
|
d0eca26d5b | ||
|
|
b742923897 | ||
|
|
1781b926d9 | ||
|
|
9222ee57a5 | ||
|
|
67cca7d85d | ||
|
|
7b97a8fca2 | ||
|
|
6bff2de371 | ||
|
|
05283b08f7 | ||
|
|
838687a3ae | ||
|
|
7b0213299a | ||
|
|
f4dbc7a24d | ||
|
|
161e8b4117 | ||
|
|
6f3535984c | ||
|
|
e3557c1f7a | ||
|
|
692b6af09d | ||
|
|
25932e10d6 | ||
|
|
ec13a001a2 | ||
|
|
702a616e6c | ||
|
|
a3dd208fbd | ||
|
|
55c3406990 | ||
|
|
197fbc33e0 | ||
|
|
008a4fd7d5 | ||
|
|
8c5665095f | ||
|
|
647b2ea0c8 | ||
|
|
e61f88b98d | ||
|
|
b024421bbb | ||
|
|
ce95467145 | ||
|
|
1564b380b1 | ||
|
|
593b8959c5 | ||
|
|
3fda3b4b7c | ||
|
|
7e680038d6 | ||
|
|
71b8bb5d60 | ||
|
|
cfbafdeb5c | ||
|
|
c19d4a433a | ||
|
|
13cc80bd35 | ||
|
|
1eea2daeae | ||
|
|
5b29e7d351 | ||
|
|
3af39d99e4 | ||
|
|
1abf988366 | ||
|
|
205f943ab5 | ||
|
|
879683da4f | ||
|
|
974e884432 | ||
|
|
846319f1a9 | ||
|
|
e5237ae805 | ||
|
|
b9d88251e6 | ||
|
|
0d720626d4 | ||
|
|
50b2c6aa88 | ||
|
|
382019d907 | ||
|
|
97103739c2 | ||
|
|
9cf13691ad | ||
|
|
e6107173fb | ||
|
|
1d9ec7a779 | ||
|
|
0c345fe913 | ||
|
|
b6f06f1e89 | ||
|
|
aea3a7449e | ||
|
|
4db73ae73a | ||
|
|
2af7acd3db | ||
|
|
da9847b88f | ||
|
|
5b9f9f1b0d | ||
|
|
5427f295f6 | ||
|
|
620273769f | ||
|
|
dff3d79569 | ||
|
|
0904453e41 | ||
|
|
90eafb557c | ||
|
|
bcb294c625 | ||
|
|
b070a7cc52 | ||
|
|
e48d89a607 | ||
|
|
cb57e5c1e1 | ||
|
|
bf363621d3 | ||
|
|
4dbd1e0bb3 | ||
|
|
cbda970437 | ||
|
|
eecd814e53 | ||
|
|
fb4b6330da | ||
|
|
eb24b5c027 | ||
|
|
3346f5de77 | ||
|
|
1a69cea9f4 | ||
|
|
d763f0b8f6 | ||
|
|
6bb98a00dc | ||
|
|
2edb8e8d0a | ||
|
|
c3e162dd56 | ||
|
|
3cdd061afd | ||
|
|
aaaf37b8db | ||
|
|
7af5f04e45 | ||
|
|
65a166b860 | ||
|
|
eb4bcb71c4 | ||
|
|
6a04b66d84 | ||
|
|
30ae9a82c1 | ||
|
|
ec06f2a392 | ||
|
|
f461f0884b | ||
|
|
d55c329f76 | ||
|
|
0f63e076a6 | ||
|
|
1012b6b28b | ||
|
|
421352c75c | ||
|
|
a170c107e2 | ||
|
|
104617d644 | ||
|
|
5b1773ba26 | ||
|
|
cd0aeca87c | ||
|
|
5c65702032 | ||
|
|
66445aae59 | ||
|
|
23156f3d5e | ||
|
|
76bb6df084 | ||
|
|
54c99c7fcc | ||
|
|
80fc5e86ff | ||
|
|
eddd63ae1a | ||
|
|
e55ed489f2 | ||
|
|
95fb33e41b | ||
|
|
404c900de7 | ||
|
|
dc773f486b | ||
|
|
db679fb6ca | ||
|
|
a2458e0dd0 | ||
|
|
f22507e313 | ||
|
|
8cfc0caad3 | ||
|
|
7d8b36e00c | ||
|
|
50b920b799 | ||
|
|
b4f5121382 | ||
|
|
b9828bcf11 | ||
|
|
0ad0577ba3 | ||
|
|
46d6c3a8c6 | ||
|
|
b40ed1d217 | ||
|
|
ab3ceb151a | ||
|
|
d393c6fc4d | ||
|
|
e6c6391958 | ||
|
|
f2857a8085 | ||
|
|
30a5a08769 | ||
|
|
16a884d778 | ||
|
|
f403281978 | ||
|
|
c1d547e1b1 | ||
|
|
d94003475c | ||
|
|
dd8a11e241 | ||
|
|
7699e440e4 | ||
|
|
0ad2da9baa | ||
|
|
c287b842f3 | ||
|
|
b6d2092048 | ||
|
|
ae9f5d4e8e | ||
|
|
eab0b40e2c | ||
|
|
0aee78ac5f | ||
|
|
56cb8962bd | ||
|
|
5747fd84ad | ||
|
|
d3666a9388 | ||
|
|
d836e8203f | ||
|
|
458927a6e3 | ||
|
|
0267782386 | ||
|
|
32bdf1c1af | ||
|
|
a977ce4ac9 | ||
|
|
18f6121797 | ||
|
|
59531f32dc | ||
|
|
03ae85f5ac | ||
|
|
8ff27d5e8c | ||
|
|
7ecf51a159 | ||
|
|
f4513912d4 | ||
|
|
a7f7e6fa9e | ||
|
|
73464361e4 | ||
|
|
56c6633ef6 | ||
|
|
143d23bf9d | ||
|
|
111b1b744f | ||
|
|
29f8762ed7 | ||
|
|
959e5beb3c | ||
|
|
7417a972b6 | ||
|
|
3da62f3471 | ||
|
|
cc668ef439 | ||
|
|
18fa98d8ea | ||
|
|
a245c2e7ea | ||
|
|
a1745b26f7 | ||
|
|
35ca970650 | ||
|
|
6e8ab4f141 | ||
|
|
e48c2b9fd5 | ||
|
|
8c20376c95 | ||
|
|
93813adf97 | ||
|
|
ee81a3d280 | ||
|
|
41fab3e11c | ||
|
|
dae4eb123c | ||
|
|
6b6e59791a | ||
|
|
ef97ef7217 | ||
|
|
d407ac0c4a | ||
|
|
82b580d3a3 | ||
|
|
c2a9e0290c | ||
|
|
01609a51f5 | ||
|
|
ccff6ed63b | ||
|
|
d9a1d739dc | ||
|
|
05088a7744 | ||
|
|
2387b46ba1 | ||
|
|
375c8789a4 | ||
|
|
7a13b64fbe | ||
|
|
b8a12aef2f | ||
|
|
8feb67f2ea | ||
|
|
773eb34c3e | ||
|
|
5835845092 | ||
|
|
7627d37e22 | ||
|
|
6674b657ab | ||
|
|
9c3d9e5943 | ||
|
|
636bc9efa4 | ||
|
|
6f66ff6ccf | ||
|
|
bdf5b0c6a1 | ||
|
|
ef86938a0e | ||
|
|
01f7aa52a0 | ||
|
|
6c73e5b818 | ||
|
|
3ac392e358 | ||
|
|
18d58e4ea8 | ||
|
|
2d343029a1 | ||
|
|
56d9fee486 | ||
|
|
cf98232109 | ||
|
|
01c19a9995 | ||
|
|
f142150b3e | ||
|
|
b783ee8c0b | ||
|
|
2f4dfa080b | ||
|
|
54b0ce27fb | ||
|
|
0e8d410c98 | ||
|
|
d3158f5c7e | ||
|
|
f580608a31 | ||
|
|
fefd4d281a | ||
|
|
ed539d2ee1 | ||
|
|
25852d68bd | ||
|
|
795389c0c8 | ||
|
|
66f2d41033 | ||
|
|
01a0d0d587 | ||
|
|
7262a9ab50 | ||
|
|
189d27c13f | ||
|
|
eb7f2abd97 | ||
|
|
250a8ec2f1 | ||
|
|
c5a6f8a0b3 | ||
|
|
b7768ccaf6 | ||
|
|
e68d5c8a14 | ||
|
|
f7899bc670 | ||
|
|
5f74c827f5 | ||
|
|
09ddd82800 | ||
|
|
87aa1af654 | ||
|
|
c3ffb13a79 | ||
|
|
ba8466bacf | ||
|
|
595e19231e | ||
|
|
4f76163541 | ||
|
|
6f06ac760c | ||
|
|
25947cb278 | ||
|
|
b0d76f2f6a | ||
|
|
6d35c648d6 | ||
|
|
b4e7dd54cf | ||
|
|
6411c1cf0d | ||
|
|
0f16d0a7a3 | ||
|
|
1a29f374a5 | ||
|
|
63400a455f | ||
|
|
5911f3e28e | ||
|
|
040447833c | ||
|
|
b7d8fb950b | ||
|
|
c60ed59a75 | ||
|
|
976f92cb11 | ||
|
|
ded8367b07 | ||
|
|
1071e86f96 | ||
|
|
da68b222b7 | ||
|
|
42ca5743aa | ||
|
|
c2b14fca10 | ||
|
|
9e2b1c5af3 | ||
|
|
8f0d05da2e | ||
|
|
3d3a46070c | ||
|
|
fbc551a76e | ||
|
|
8563ddffd3 | ||
|
|
3915ffc4e0 | ||
|
|
7955b28a75 | ||
|
|
ac9899406a | ||
|
|
13e1b288b3 | ||
|
|
8c8f5d8fee | ||
|
|
35cf694b60 | ||
|
|
fa4dbfe9d0 | ||
|
|
0ea08ccc9c | ||
|
|
fd5bbe0234 | ||
|
|
b43f657c8d | ||
|
|
9456c79bad | ||
|
|
0cd18a458d | ||
|
|
fdcd1404ef | ||
|
|
741d5000dd | ||
|
|
8f413852d9 | ||
|
|
420fc6daa7 | ||
|
|
ad69086888 | ||
|
|
f18c51dc68 | ||
|
|
8e1822af21 | ||
|
|
5d3e8f3fb6 | ||
|
|
71288f543e | ||
|
|
a1890a0bfe | ||
|
|
1f17d71a76 | ||
|
|
fc8b9c3547 | ||
|
|
8ebb90c160 | ||
|
|
b0c230a1df | ||
|
|
be15734c77 | ||
|
|
4478ced205 | ||
|
|
769437c8fb | ||
|
|
5fde3235ad | ||
|
|
2a8975711a | ||
|
|
da56cd5b18 | ||
|
|
90424b4766 | ||
|
|
cf23bdba12 | ||
|
|
0f983109e1 | ||
|
|
8f7edde93f | ||
|
|
1946d023cd | ||
|
|
1e5f5848b2 | ||
|
|
3cbf5fef07 | ||
|
|
4f276ed944 | ||
|
|
e69afc4e76 | ||
|
|
c4b46c28fe | ||
|
|
dac823f9c4 | ||
|
|
65deb1cfbe | ||
|
|
828f200eb6 | ||
|
|
fe5c95c27a | ||
|
|
c7fad00b66 | ||
|
|
5165d06d64 | ||
|
|
e15275666e | ||
|
|
1dc8df4189 | ||
|
|
ffe83452cd | ||
|
|
3c3603ee08 | ||
|
|
f6e94b0cc2 | ||
|
|
c3f92e8c8d | ||
|
|
84a121a1d9 | ||
|
|
f55f1341e1 | ||
|
|
cccac6a8bc | ||
|
|
e775a9f092 | ||
|
|
76237f42da | ||
|
|
6e942a1121 |
+28
-30
@@ -1,4 +1,3 @@
|
||||
|
||||
# job_environment will setup the environment for any job being executed.
|
||||
job_environment: &job_environment
|
||||
NODE_ENV: test
|
||||
@@ -41,9 +40,9 @@ integration_job: &integration_job
|
||||
- attach_workspace:
|
||||
at: ~/coralproject/talk
|
||||
- <<: *create_indexes
|
||||
- run:
|
||||
name: Setup the database with defaults
|
||||
command: ./bin/cli setup --defaults
|
||||
# - run:
|
||||
# name: Setup the database with defaults
|
||||
# command: ./bin/cli setup --defaults
|
||||
- run:
|
||||
name: Run the integration tests
|
||||
command: bash .circleci/e2e.sh
|
||||
@@ -54,7 +53,6 @@ integration_job: &integration_job
|
||||
when: always
|
||||
path: /tmp/circleci-test-results
|
||||
|
||||
|
||||
version: 2
|
||||
jobs:
|
||||
# npm_dependencies will install the dependencies used by all other steps.
|
||||
@@ -223,20 +221,17 @@ jobs:
|
||||
# only execute on a deploy related job.
|
||||
filter_deploy: &filter_deploy
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- next
|
||||
tags:
|
||||
only: /v[0-9]+(\.[0-9]+)*/
|
||||
branches:
|
||||
ignore: /.*/
|
||||
|
||||
# filter_develop will add the filters for a development related commit.
|
||||
filter_develop: &filter_develop
|
||||
filters:
|
||||
branches:
|
||||
ignore:
|
||||
- master
|
||||
- next
|
||||
- /release\/.*/
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
@@ -258,10 +253,11 @@ workflows:
|
||||
<<: *filter_develop
|
||||
requires:
|
||||
- npm_dependencies
|
||||
- test_integration_chrome_local:
|
||||
<<: *filter_develop
|
||||
requires:
|
||||
- build_assets
|
||||
# TODO: uncomment when we revisit browserstack
|
||||
# - test_integration_chrome_local:
|
||||
# <<: *filter_develop
|
||||
# requires:
|
||||
# - build_assets
|
||||
# TODO: uncomment when more reliable
|
||||
# - test_integration_firefox_local:
|
||||
# <<: *filter_develop
|
||||
@@ -283,18 +279,19 @@ workflows:
|
||||
<<: *filter_deploy
|
||||
requires:
|
||||
- npm_dependencies
|
||||
- test_integration_chrome:
|
||||
<<: *filter_deploy
|
||||
requires:
|
||||
- build_assets
|
||||
- test_integration_firefox:
|
||||
<<: *filter_deploy
|
||||
requires:
|
||||
- build_assets
|
||||
- test_integration_edge:
|
||||
<<: *filter_deploy
|
||||
requires:
|
||||
- build_assets
|
||||
# TODO: uncomment when we revisit browserstack
|
||||
# - test_integration_chrome:
|
||||
# <<: *filter_deploy
|
||||
# requires:
|
||||
# - build_assets
|
||||
# - test_integration_firefox:
|
||||
# <<: *filter_deploy
|
||||
# requires:
|
||||
# - build_assets
|
||||
# - test_integration_edge:
|
||||
# <<: *filter_deploy
|
||||
# requires:
|
||||
# - build_assets
|
||||
# TODO: uncomment when more reliable
|
||||
# - test_integration_ie:
|
||||
# <<: *filter_deploy
|
||||
@@ -309,9 +306,10 @@ workflows:
|
||||
requires:
|
||||
- lint
|
||||
- test_unit
|
||||
- test_integration_chrome
|
||||
- test_integration_firefox
|
||||
- test_integration_edge
|
||||
# TODO: uncomment when we revisit browserstack
|
||||
# - test_integration_chrome
|
||||
# - test_integration_firefox
|
||||
# - test_integration_edge
|
||||
# TODO: uncomment when more reliable
|
||||
# - test_integration_ie
|
||||
# - test_integration_safari
|
||||
|
||||
@@ -2,5 +2,10 @@
|
||||
"env": {
|
||||
"jest": true
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "15.0"
|
||||
}
|
||||
},
|
||||
"extends": "@coralproject/eslint-config-talk"
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ plugins/*
|
||||
!plugins/talk-plugin-moderation-actions
|
||||
!plugins/talk-plugin-notifications
|
||||
!plugins/talk-plugin-notifications-category-featured
|
||||
!plugins/talk-plugin-notifications-category-moderation-actions
|
||||
!plugins/talk-plugin-notifications-category-reply
|
||||
!plugins/talk-plugin-notifications-category-staff
|
||||
!plugins/talk-plugin-notifications-digest-daily
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
overrides:
|
||||
- files: "bin/cli*"
|
||||
options:
|
||||
parser: babylon
|
||||
+4
-75
@@ -1,78 +1,7 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
# Vox Media Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
## Introduction
|
||||
|
||||
We expect everyone contributing to The Coral Project to follow this code of conduct. That means the team, contractors we employ, contributors, as well as anyone posting to our public or internal-facing channels.
|
||||
We created it not because we anticipate any unacceptable behavior, but because we believe that articulating our values and obligations to one another reinforces the already exceptional level of respect among the team, and because having a code provides us with clear avenues to correct our culture should it ever stray from that course.
|
||||
This code of conduct governs the environment of the Vox Product team. We created it not because we anticipate bad behavior, but because we believe that articulating our values and obligations to one another reinforces the already exceptional level of respect among the team and because having a code provides us with clear avenues to correct our culture should it ever stray from that course. We make this code public in the hopes of contributing to the ongoing conversation about inclusion in the tech, design, and media communities and encourage other teams to fork it and make it their own. To our team, we commit to enforce and evolve this code as our team grows.
|
||||
|
||||
We commit to enforce and evolve this code over the duration of the project.
|
||||
|
||||
## Expected behavior
|
||||
|
||||
* Be supportive of each other.
|
||||
* Be collaborative. Involve others in brainstorms, sketching sessions, code reviews, planning documents, and the like. It’s not only okay to ask for help or feedback often, it’s unacceptable not to do so.
|
||||
* Be generous and kind in both giving and accepting critique. Critique is a natural and important part of our culture. Good critiques are kind, respectful, clear, and constructive, focused on goals and requirements rather than personal preferences. You are expected to give and receive criticism with grace.
|
||||
* Be humane. Be polite and friendly in all forms of communication, especially remote communication, where opportunities for misunderstanding are greater. Use sarcasm carefully. Tone is hard to decipher online; make judicious use of emoji to aid in communication.
|
||||
* Be considerate.
|
||||
* Be tolerant.
|
||||
* Respect people’s boundaries.
|
||||
* Do not make it personal.
|
||||
* Use welcoming and inclusive language.
|
||||
* Offer to help if you see someone struggling or otherwise in need of assistance (taking care not to be patronizing or disrespectful).
|
||||
* If someone approaches you looking for help, be generous with your time; if you’re under a deadline, direct them to someone else who may be of assistance.
|
||||
* Go out of your way to include people in jokes or memes, recognizing that we want to build an environment free of cliques.
|
||||
* Show empathy towards other community members
|
||||
|
||||
|
||||
## Unacceptable behavior
|
||||
|
||||
We are committed to providing a welcoming and safe environment for people of all races, gender identities, gender expressions, sexual orientations, physical abilities, physical appearances, socioeconomic backgrounds, nationalities, ages, religions, and beliefs.
|
||||
We expect that you will refrain from demeaning, discriminatory, or harassing behavior and speech.
|
||||
|
||||
Harassment includes, but is not limited to: deliberate intimidation; stalking; unwanted photography or recording; sustained or willful disruption of talks or other events; inappropriate physical contact; use of sexual or discriminatory imagery, comments, or jokes; and unwelcome sexual attention.
|
||||
Furthermore, any behavior or language which is unwelcoming—whether or not it rises to the level of harassment—is also strongly discouraged. Much exclusionary behavior takes the form of microaggressions—subtle put-downs which may be unconsciously delivered. Regardless of intent, microaggressions can have a significant negative impact on victims and have no place on our team.
|
||||
|
||||
Other inappropriate behavior:
|
||||
|
||||
* Threats
|
||||
* Slurs
|
||||
* Pornography
|
||||
* Spam
|
||||
* Bullying
|
||||
* Copyright infringement
|
||||
* 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).
|
||||
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
These guidelines are ambitious, and we’re not always going to succeed in meeting them. When something goes wrong—whether it’s a microaggression or an instance of harassment — there are a number of things you can do to address the situation. Depending on your comfort level and the severity of the situation, here are some suggestions:
|
||||
|
||||
* Address it directly. If you’re comfortable bringing up the incident with the person who instigated it, pull them aside to discuss how it affected you. Be sure to approach these conversations in a forgiving spirit: an angry or tense conversation will not do either of you any good. If you’re unsure how to go about that, try discussing with your manager or with the people and culture team first—they might have some advice about how to make this conversation happen.
|
||||
|
||||
If you’re too frustrated to have a direct conversation, there are a number of alternate routes you can take.
|
||||
|
||||
* Talk to a peer or mentor. Your colleagues are likely to have personal and professional experience on which to draw that could be of use to you. If you have someone you’re comfortable approaching, reach out and discuss the situation with them. They may be able to advise on how they would handle it, or direct you to someone who can. The flip side of this, of course, is that you should also be available when your colleagues reach out to you.
|
||||
|
||||
* 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).
|
||||
|
||||
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.
|
||||
Read the rest here: http://code-of-conduct.voxmedia.com/
|
||||
|
||||
+3
-23
@@ -18,7 +18,7 @@ There are at least three ways to contribute to Talk:
|
||||
|
||||
Conversation surrounding contributions begins in [issues](https://github.com/coralproject/talk/issues).
|
||||
|
||||
### When should I Create an Issue?
|
||||
### When should I create an issue?
|
||||
|
||||
File an issue as soon as you have an idea of something you'd like to contribute. We would love to hear what you're thinking and help refine the idea to make it into the Talk ecosystem.
|
||||
|
||||
@@ -30,9 +30,9 @@ Please file issues if:
|
||||
|
||||
### What should I include?
|
||||
|
||||
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:
|
||||
Coral has adopted an iterative, agile development philosophy. All contributions that make it into the Talk repository should start with a user story in this form:
|
||||
|
||||
> As a [type of person] I'd like to be able to [do something] so that I can [get some result].
|
||||
> As a [type of Coral user] I'd like to [do something] so that I can [get some result/value].
|
||||
|
||||
This exercise does two things:
|
||||
|
||||
@@ -71,23 +71,3 @@ If you'd like to discuss a contribution, please [file an issue](https://github.c
|
||||
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 repository.
|
||||
|
||||
## I want to contribute but I'm not sure what to do!
|
||||
|
||||
If you want to contribute but don't have a clear idea of exactly what that may be, here are some resources that may help:
|
||||
|
||||
### Product Roadmap
|
||||
|
||||
Please visit our product roadmap here: https://www.pivotaltracker.com/n/projects/1863625. If you'd like to take on any of our scheduled tasks we'd be forever grateful!
|
||||
|
||||
### Discussion Forum
|
||||
|
||||
If you'd like to discuss what we're up to, please visit or [community](https://community.coralproject.net/) or [contact us](https://coralproject.net/contact).
|
||||
|
||||
### Integrations
|
||||
|
||||
Have a favorite analytics engine? Data science service? CMS? Auth platform? Deployment platform or pipeline? Pet project? Consider building a plugin to integrate them!
|
||||
|
||||
### 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.
|
||||
@@ -4,12 +4,15 @@ FROM coralproject/talk:latest
|
||||
ONBUILD ARG TALK_ADDTL_COMMENTS_ON_LOAD_MORE=10
|
||||
ONBUILD ARG TALK_ASSET_COMMENTS_LOAD_DEPTH=10
|
||||
ONBUILD ARG TALK_REPLY_COMMENTS_LOAD_DEPTH=3
|
||||
ONBUILD ARG TALK_ADDTL_REPLIES_ON_LOAD_MORE=999999
|
||||
ONBUILD ARG TALK_THREADING_LEVEL=3
|
||||
ONBUILD ARG TALK_DEFAULT_STREAM_TAB=all
|
||||
ONBUILD ARG TALK_DISABLE_EMBED_POLYFILL=FALSE
|
||||
ONBUILD ARG TALK_DEFAULT_LANG=en
|
||||
ONBUILD ARG TALK_WHITELISTED_LANGUAGES
|
||||
ONBUILD ARG TALK_PLUGINS_JSON
|
||||
ONBUILD ARG TALK_WEBPACK_SOURCE_MAP
|
||||
ONBUILD ARG TALK_DEFAULT_LAZY_RENDER
|
||||
|
||||
# Bundle app source
|
||||
ONBUILD COPY . /usr/src/app
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright 2018 Mozilla Foundation
|
||||
Copyright 2019 Vox Media, Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
# Talk · [](https://circleci.com/gh/coralproject/talk) · [](https://nodesecurity.io/orgs/coralproject/projects/7bd7d26c-47ed-4a5f-8c4a-b919bf1c2946) · [](CONTRIBUTING.md#pull-requests)
|
||||
# Talk · [](https://circleci.com/gh/coralproject/talk) · [](CONTRIBUTING.md#pull-requests)
|
||||
|
||||
Online comments are broken. Our open-source commenting platform, Talk, rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here](https://coralproject.net/products/talk.html).
|
||||
---
|
||||
|
||||
Built with <3 by The Coral Project & Mozilla.
|
||||
> :construction: **NOTE:** We're currently building v5 of this platform, check it out in our [next branch](https://github.com/coralproject/talk/tree/next) :construction_worker_man: :construction_worker_woman:
|
||||
|
||||
## Try Talk!
|
||||
---
|
||||
|
||||
You're just one click away from trying Talk - all you need is a Heroku account and a few minutes of your time.
|
||||
Online comments are broken. Our open-source commenting platform, Talk, rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here](https://coralproject.net/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)
|
||||
Built with <3 by The Coral Project, a part of [Vox Media](https://www.voxmedia.com/).
|
||||
|
||||
## Technical Documentation
|
||||
## Getting Started
|
||||
|
||||
From getting up and running, to advanced configuration, to how to scale Talk, our [Talk Technical Docs](https://docs.coralproject.net/talk/) have everything you need to know.
|
||||
Check out our Quickstart and Install guides to get started with Talk in our [Technical Docs](https://docs.coralproject.net/talk/).
|
||||
|
||||
## Product Guide
|
||||
|
||||
@@ -20,13 +20,19 @@ Learn more about Talk, including a deep dive into features for commenters and mo
|
||||
|
||||
## Pre-Launch Guide
|
||||
|
||||
You’ve installed Talk on your server, and you’re preparing to launch it on your site. The real community work starts now, before you go live. You have a unique opportunity pre-launch to set your community up for success. Read our [Talk Community Guide](https://blog.coralproject.net/youve-installed-talk-now-what/).
|
||||
You’ve installed Talk on your server, and you’re preparing to launch it on your site. The real community work starts now, before you go live. You have a unique opportunity pre-launch to set your community up for success. Read our [Talk Community Guide](https://coralproject.net/blog/youve-installed-talk-now-what/).
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
For advanced configuration and usage of Talk, check out our [Configuration](https://docs.coralproject.net/talk/advanced-configuration/) and [Integration](https://docs.coralproject.net/talk/integrating/authentication/) how-tos. This covers topics in which you will need dev support to fully customize and integrate Talk, such as SSO/authentication, creating and managing assets and articles, styling Talk with custom CSS, and setting up Notifications and SMTP support.
|
||||
|
||||
## Versions & Upgrading
|
||||
|
||||
Check our Releases page for the latest recommended release version. [Releases](https://github.com/coralproject/talk/releases) All future even-numbered versions are considered stable LTS versions. We recommend the latest verified release for use in production environments.
|
||||
|
||||
## More Resources
|
||||
|
||||
- [Talk Product Roadmap](https://www.pivotaltracker.com/n/projects/1863625)
|
||||
- [Our Blog](https://blog.coralproject.net/)
|
||||
- [Community Forums](https://community.coralproject.net/)
|
||||
- [Our Blog](https://coralproject.net/blog)
|
||||
- [Community Guides for Journalism](https://guides.coralproject.net/)
|
||||
- [More About Us](https://coralproject.net/)
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
"value": "",
|
||||
"required": true
|
||||
},
|
||||
"MAILGUN_SMTP_PASSWORD": {
|
||||
"value": "",
|
||||
"required": true
|
||||
},
|
||||
"NODE_ENV": "production",
|
||||
"REWRITE_ENV": "TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:MAILGUN_SMTP_SERVER,TALK_SMTP_PORT:MAILGUN_SMTP_PORT,TALK_SMTP_USERNAME:MAILGUN_SMTP_LOGIN,TALK_SMTP_PASSWORD:MAILGUN_SMTP_PASSWORD",
|
||||
"NPM_CONFIG_PRODUCTION": "false"
|
||||
|
||||
@@ -219,6 +219,28 @@ async function rewrite(search, replace, options) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* debugAssetURL will scrape the given URL and print it's scraped values.
|
||||
*/
|
||||
async function debugAssetURL(url) {
|
||||
try {
|
||||
const meta = await scraper.scrape(url);
|
||||
|
||||
const table = new Table({ head: ['Property', 'Value'] });
|
||||
|
||||
for (const [property, value] of Object.entries(meta)) {
|
||||
table.push([property, value]);
|
||||
}
|
||||
|
||||
console.log(table.toString());
|
||||
|
||||
util.shutdown(0);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
@@ -234,6 +256,13 @@ program
|
||||
.description('list all the assets in the database')
|
||||
.action(listAssets);
|
||||
|
||||
program
|
||||
.command('debug [url]')
|
||||
.description(
|
||||
'prints the scraped metadata that would be added to the given asset'
|
||||
)
|
||||
.action(debugAssetURL);
|
||||
|
||||
program
|
||||
.command('refresh [age]')
|
||||
.description('queues the assets that exceed the age requested')
|
||||
|
||||
+37
-24
@@ -143,9 +143,9 @@ async function reconcileRemotePlugins({ dryRun, upgradeRemote }) {
|
||||
|
||||
if (fetchable.length > 0) {
|
||||
console.log(
|
||||
`$ yarn add --ignore-scripts --ignore-workspace-root-check ${fetchable.map(
|
||||
({ name, version }) => `${name}@${version}`.cyan
|
||||
)}`
|
||||
`$ yarn add --ignore-scripts --ignore-workspace-root-check ${fetchable
|
||||
.map(({ name, version }) => `${name}@${version}`.cyan)
|
||||
.join(' ')}`
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
@@ -162,7 +162,7 @@ async function reconcileRemotePlugins({ dryRun, upgradeRemote }) {
|
||||
|
||||
if (output.status) {
|
||||
throw new Error(
|
||||
'Could not install external plugins, errors occured during install'
|
||||
'Could not install external plugins, errors occurred during install'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ async function reconcileRemotePlugins({ dryRun, upgradeRemote }) {
|
||||
|
||||
if (output.status) {
|
||||
throw new Error(
|
||||
'Could not install external plugins, errors occured during install'
|
||||
'Could not install external plugins, errors occurred during install'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -329,28 +329,41 @@ async function createSeedPlugin() {
|
||||
if (answers.addPluginsJson) {
|
||||
const pluginsJson = path.resolve(__dirname, '..', 'plugins.json');
|
||||
|
||||
fs
|
||||
.readJson(pluginsJson)
|
||||
.then(j => {
|
||||
// This is a client-side plugin, let's push this.
|
||||
if (answers.client) {
|
||||
j.client.push(answers.pluginName);
|
||||
let j;
|
||||
try {
|
||||
j = await fs.readJson(pluginsJson);
|
||||
} catch (err) {
|
||||
// Fallback to plugins.default.json if the plugins.json file does not
|
||||
// exist.
|
||||
const defaultPluginsJson = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'plugins.default.json'
|
||||
);
|
||||
|
||||
const output = JSON.stringify(j, null, 2);
|
||||
fs.writeFileSync(pluginsJson, output);
|
||||
}
|
||||
try {
|
||||
j = await fs.readJson(defaultPluginsJson);
|
||||
} catch (err) {
|
||||
// Fallback to an empty one if that also, doesn't exist.
|
||||
j = { client: [], server: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// This is a server-side plugin, let's push this.
|
||||
if (answers.server) {
|
||||
j.server.push(answers.pluginName);
|
||||
// This is a client-side plugin, let's push this.
|
||||
if (answers.client) {
|
||||
j.client.push(answers.pluginName);
|
||||
|
||||
const output = JSON.stringify(j, null, 2);
|
||||
fs.writeFileSync(pluginsJson, output);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
const output = JSON.stringify(j, null, 2);
|
||||
fs.writeFileSync(pluginsJson, output);
|
||||
}
|
||||
|
||||
// This is a server-side plugin, let's push this.
|
||||
if (answers.server) {
|
||||
j.server.push(answers.pluginName);
|
||||
|
||||
const output = JSON.stringify(j, null, 2);
|
||||
fs.writeFileSync(pluginsJson, output);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
|
||||
+17
-5
@@ -1,8 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const throng = require('throng');
|
||||
const { CONCURRENCY } = require('../config');
|
||||
const { logger } = require('../services/logging');
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const serve = require('../serve');
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
@@ -22,8 +24,18 @@ program
|
||||
)
|
||||
.parse(process.argv);
|
||||
|
||||
// Start serving.
|
||||
serve(program).catch(err => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
throng({
|
||||
workers: CONCURRENCY,
|
||||
start: i => {
|
||||
logger.info({ workerID: i }, 'started worker');
|
||||
|
||||
// Load in the serve.
|
||||
const serve = require('../serve');
|
||||
|
||||
// Start serving.
|
||||
serve(program).catch(err => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
+37
-31
@@ -45,11 +45,10 @@ const performSetup = async () => {
|
||||
} catch (err) {
|
||||
// If the error is `not init`, then we're good, otherwise, it's something
|
||||
// else.
|
||||
if (err instanceof ErrSettingsNotInit) {
|
||||
if (!err instanceof ErrSettingsNotInit) {
|
||||
throw err;
|
||||
return;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (program.defaults) {
|
||||
@@ -138,7 +137,7 @@ const performSetup = async () => {
|
||||
|
||||
console.log("\nWe'll ask you some questions about your first admin user.\n");
|
||||
|
||||
let user = await inquirer.prompt([
|
||||
let { username, email } = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'username',
|
||||
@@ -161,39 +160,46 @@ const performSetup = async () => {
|
||||
return 'Email is required';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
type: 'password',
|
||||
filter: password => {
|
||||
return UsersService.isValidPassword(password).catch(err => {
|
||||
throw err.message;
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'confirmPassword',
|
||||
message: 'Confirm Password',
|
||||
type: 'password',
|
||||
filter: (confirmPassword, { password }) => {
|
||||
if (password !== confirmPassword) {
|
||||
return Promise.reject(new Error('Passwords do not match'));
|
||||
}
|
||||
|
||||
return UsersService.isValidPassword(confirmPassword).catch(err => {
|
||||
throw err.message;
|
||||
});
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
let password = '';
|
||||
while (!password) {
|
||||
answers = await inquirer.prompt([
|
||||
{
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
type: 'password',
|
||||
filter: password => {
|
||||
try {
|
||||
UsersService.isValidPassword(password);
|
||||
} catch (err) {
|
||||
throw err.message;
|
||||
}
|
||||
|
||||
return password;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'confirmPassword',
|
||||
message: 'Confirm Password',
|
||||
type: 'password',
|
||||
},
|
||||
]);
|
||||
|
||||
if (answers.password !== answers.confirmPassword) {
|
||||
console.error('Passwords do not match');
|
||||
} else {
|
||||
password = answers.password;
|
||||
}
|
||||
}
|
||||
|
||||
const ctx = Context.forSystem();
|
||||
let { user: newUser } = await SetupService.setup(ctx, {
|
||||
settings: settings.toObject(),
|
||||
user: {
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
password: user.password,
|
||||
email,
|
||||
username,
|
||||
password,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+4
-1
@@ -49,7 +49,10 @@ async function revokeToken(tokenID) {
|
||||
|
||||
async function createToken(userID, tokenName) {
|
||||
try {
|
||||
let { pat: { id }, jwt } = await TokensService.create(userID, tokenName);
|
||||
let {
|
||||
pat: { id },
|
||||
jwt,
|
||||
} = await TokensService.create(userID, tokenName);
|
||||
|
||||
console.log(`Created Token[${id}] for User[${userID}] = ${jwt}`);
|
||||
|
||||
|
||||
+2
-1
@@ -105,7 +105,8 @@ function printUserAsTable(user) {
|
||||
Suspension: user.suspended
|
||||
? `Until ${user.status.suspension.until}`
|
||||
: false,
|
||||
}
|
||||
},
|
||||
{ 'Always premod comments': user.alwaysPremod }
|
||||
);
|
||||
|
||||
console.log(table.toString());
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import {
|
||||
SHOW_ALWAYS_PREMOD_USER_DIALOG,
|
||||
HIDE_ALWAYS_PREMOD_USER_DIALOG,
|
||||
} from '../constants/alwaysPremodUserDialog.js';
|
||||
|
||||
export const showAlwaysPremodUserDialog = ({ userId, username }) => ({
|
||||
type: SHOW_ALWAYS_PREMOD_USER_DIALOG,
|
||||
userId,
|
||||
username,
|
||||
});
|
||||
|
||||
export const hideAlwaysPremodUserDialog = () => ({
|
||||
type: HIDE_ALWAYS_PREMOD_USER_DIALOG,
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import queryString from 'query-string';
|
||||
import queryString from 'querystringify';
|
||||
|
||||
import {
|
||||
FETCH_USERS_REQUEST,
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
SET_SEARCH_VALUE,
|
||||
SHOW_BANUSER_DIALOG,
|
||||
HIDE_BANUSER_DIALOG,
|
||||
SHOW_ALWAYS_PREMOD_USER_DIALOG,
|
||||
HIDE_ALWAYS_PREMOD_USER_DIALOG,
|
||||
SHOW_REJECT_USERNAME_DIALOG,
|
||||
HIDE_REJECT_USERNAME_DIALOG,
|
||||
SET_INDICATOR_TRACK,
|
||||
@@ -61,6 +63,15 @@ export const setSearchValue = value => ({
|
||||
export const showBanUserDialog = user => ({ type: SHOW_BANUSER_DIALOG, user });
|
||||
export const hideBanUserDialog = () => ({ type: HIDE_BANUSER_DIALOG });
|
||||
|
||||
// Always premod User Dialog
|
||||
export const showAlwaysPremodUserDialog = user => ({
|
||||
type: SHOW_ALWAYS_PREMOD_USER_DIALOG,
|
||||
user,
|
||||
});
|
||||
export const hideAlwaysPremodUserDialog = () => ({
|
||||
type: HIDE_ALWAYS_PREMOD_USER_DIALOG,
|
||||
});
|
||||
|
||||
// Reject Username Dialog
|
||||
export const showRejectUsernameDialog = user => ({
|
||||
type: SHOW_REJECT_USERNAME_DIALOG,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import {
|
||||
SHOW_REJECT_USERNAME_DIALOG,
|
||||
HIDE_REJECT_USERNAME_DIALOG,
|
||||
} from '../constants/rejectUsernameDialog';
|
||||
|
||||
export const showRejectUsernameDialog = ({ userId, username }) => ({
|
||||
type: SHOW_REJECT_USERNAME_DIALOG,
|
||||
userId,
|
||||
username,
|
||||
});
|
||||
|
||||
export const hideRejectUsernameDialog = () => ({
|
||||
type: HIDE_REJECT_USERNAME_DIALOG,
|
||||
});
|
||||
@@ -1,16 +1,18 @@
|
||||
import queryString from 'query-string';
|
||||
import queryString from 'querystringify';
|
||||
|
||||
import {
|
||||
FETCH_ASSETS_REQUEST,
|
||||
FETCH_ASSETS_SUCCESS,
|
||||
FETCH_ASSETS_FAILURE,
|
||||
LOAD_MORE_ASSETS_REQUEST,
|
||||
LOAD_MORE_ASSETS_SUCCESS,
|
||||
LOAD_MORE_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/stories';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
@@ -21,17 +23,14 @@ 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 = (query = {}) => (dispatch, _, { rest }) => {
|
||||
export const fetchAssets = (query = {}) => (dispatch, _, { rest2 }) => {
|
||||
dispatch({ type: FETCH_ASSETS_REQUEST });
|
||||
return rest(`/assets?${queryString.stringify(query)}`)
|
||||
.then(({ result, page, count, limit, totalPages }) =>
|
||||
return rest2(`/stories?${queryString.stringify(query)}`)
|
||||
.then(({ edges, pageInfo }) =>
|
||||
dispatch({
|
||||
type: FETCH_ASSETS_SUCCESS,
|
||||
assets: result,
|
||||
page,
|
||||
count,
|
||||
limit,
|
||||
totalPages,
|
||||
edges,
|
||||
pageInfo,
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
@@ -43,12 +42,31 @@ export const fetchAssets = (query = {}) => (dispatch, _, { rest }) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const loadMoreAssets = (query = {}) => (dispatch, _l, { rest2 }) => {
|
||||
dispatch({ type: LOAD_MORE_ASSETS_REQUEST });
|
||||
return rest2(`/stories?${queryString.stringify(query)}`)
|
||||
.then(({ edges, pageInfo }) =>
|
||||
dispatch({
|
||||
type: LOAD_MORE_ASSETS_SUCCESS,
|
||||
edges,
|
||||
pageInfo,
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch({ type: LOAD_MORE_ASSETS_FAILURE, error: errorMessage });
|
||||
});
|
||||
};
|
||||
|
||||
// Update an asset state
|
||||
// Get comments to fill each of the three lists on the mod queue
|
||||
export const updateAssetState = (id, closedAt) => (dispatch, _, { rest }) => {
|
||||
dispatch({ type: UPDATE_ASSET_STATE_REQUEST, id, closedAt });
|
||||
dispatch({ type: UPDATE_ASSET_STATE_REQUEST });
|
||||
return rest(`/assets/${id}/status`, { method: 'PUT', body: { closedAt } })
|
||||
.then(() => dispatch({ type: UPDATE_ASSET_STATE_SUCCESS }))
|
||||
.then(() => dispatch({ type: UPDATE_ASSET_STATE_SUCCESS, id, closedAt }))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
@@ -58,10 +76,6 @@ 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,
|
||||
|
||||
@@ -76,7 +76,7 @@ ActionsMenu.propTypes = {
|
||||
icon: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
label: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
|
||||
buttonClassNames: PropTypes.string,
|
||||
};
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ const ActionsMenuItem = props => (
|
||||
|
||||
ActionsMenuItem.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.string,
|
||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
|
||||
};
|
||||
|
||||
export default ActionsMenuItem;
|
||||
|
||||
@@ -5,6 +5,7 @@ import styles from './NotFound.css';
|
||||
import { Button, TextField, Alert, Success } from 'coral-ui';
|
||||
import Recaptcha from 'react-recaptcha';
|
||||
import cn from 'classnames';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class AdminLogin extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -41,13 +42,13 @@ class AdminLogin extends React.Component {
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
id="email"
|
||||
label="Email Address"
|
||||
label={t('login.email_address')}
|
||||
value={this.state.email}
|
||||
onChange={e => this.setState({ email: e.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
id="password"
|
||||
label="Password"
|
||||
label={t('login.password')}
|
||||
value={this.state.password}
|
||||
onChange={e => this.setState({ password: e.target.value })}
|
||||
type="password"
|
||||
@@ -60,10 +61,10 @@ class AdminLogin extends React.Component {
|
||||
full
|
||||
onClick={this.handleSignIn}
|
||||
>
|
||||
Sign In
|
||||
{t('login.sign_in_button')}
|
||||
</Button>
|
||||
<p className={styles.forgotPasswordCTA}>
|
||||
Forgot your password?{' '}
|
||||
{t('login.forgot_password')}{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.forgotPasswordLink}
|
||||
@@ -72,7 +73,7 @@ class AdminLogin extends React.Component {
|
||||
this.setState({ requestPassword: true });
|
||||
}}
|
||||
>
|
||||
Request a new one.
|
||||
{t('login.request_password')}
|
||||
</a>
|
||||
</p>
|
||||
{loginMaxExceeded && (
|
||||
@@ -95,14 +96,14 @@ class AdminLogin extends React.Component {
|
||||
>
|
||||
{this.props.passwordRequestSuccess}{' '}
|
||||
<a className={styles.signInLink} href="#">
|
||||
Sign in
|
||||
{t('login.sign_in')}
|
||||
</a>
|
||||
<Success />
|
||||
</p>
|
||||
) : (
|
||||
<form onSubmit={this.handleRequestPassword}>
|
||||
<TextField
|
||||
label="Email Address"
|
||||
label={t('login.email_address')}
|
||||
value={this.state.email}
|
||||
onChange={e => this.setState({ email: e.target.value })}
|
||||
/>
|
||||
@@ -112,17 +113,15 @@ class AdminLogin extends React.Component {
|
||||
full
|
||||
onClick={this.handleRequestPassword}
|
||||
>
|
||||
Reset Password
|
||||
{t('login.reset_password')}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
return (
|
||||
<Layout fixedDrawer restricted={true}>
|
||||
<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>
|
||||
<h1 className={styles.loginHeader}>{t('login.team_sign_in')}</h1>
|
||||
<p className={styles.loginCTA}>{t('login.sign_in_message')}</p>
|
||||
{this.state.requestPassword ? requestPasswordForm : signInForm}
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
.dialog {
|
||||
border: none;
|
||||
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
|
||||
width: 400px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.header {
|
||||
color: black;
|
||||
font-size: 1.5em;
|
||||
font-weight: 500;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.subheader {
|
||||
color: black;
|
||||
font-size: 1.3em;
|
||||
font-weight: 500;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 6px;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Dialog } from 'coral-ui';
|
||||
import styles from './AlwaysPremodUserDialog.css';
|
||||
|
||||
import Button from 'coral-ui/components/Button';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class AlwaysPremodUserDialog extends React.Component {
|
||||
handlePerform = () => {
|
||||
this.props.onPerform();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { open, onCancel, username, info } = this.props;
|
||||
return (
|
||||
<Dialog
|
||||
className={cn(styles.dialog, 'talk-always-premod-user-dialog')}
|
||||
id="alwaysPremodUserDialog"
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
title={t('alwayspremoddialog.always_premod_user')}
|
||||
>
|
||||
<span className={styles.close} onClick={onCancel}>
|
||||
×
|
||||
</span>
|
||||
<section>
|
||||
<h2 className={styles.header}>
|
||||
{t('alwayspremoddialog.always_premod_user')}
|
||||
</h2>
|
||||
<h3 className={styles.subheader}>
|
||||
{t('alwayspremoddialog.are_you_sure', username)}
|
||||
</h3>
|
||||
<p className={styles.description}>{info}</p>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
className={cn('talk-always-premod-user-dialog-button-cancel')}
|
||||
cStyle="white"
|
||||
onClick={onCancel}
|
||||
raised
|
||||
>
|
||||
{t('alwayspremoddialog.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className={cn('talk-always-premod-user-dialog-button-confirm')}
|
||||
cStyle="black"
|
||||
onClick={this.handlePerform}
|
||||
raised
|
||||
>
|
||||
{t('alwayspremoddialog.yes_always_premod_user')}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AlwaysPremodUserDialog.propTypes = {
|
||||
open: PropTypes.bool,
|
||||
onPerform: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
username: PropTypes.string,
|
||||
info: PropTypes.string,
|
||||
};
|
||||
|
||||
export default AlwaysPremodUserDialog;
|
||||
@@ -19,7 +19,9 @@ class BanUserDialog extends React.Component {
|
||||
}
|
||||
|
||||
handleMessageChange = e => {
|
||||
const { target: { value: message } } = e;
|
||||
const {
|
||||
target: { value: message },
|
||||
} = e;
|
||||
this.setState({ message });
|
||||
};
|
||||
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './CountBadge.css';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { humanizeNumber } from 'coral-framework/helpers/numbers';
|
||||
|
||||
const CountBadge = ({ count }) => {
|
||||
let number = count;
|
||||
|
||||
// shorten large counts to abbreviations
|
||||
if (number / 1e9 > 1) {
|
||||
number = `${(number / 1e9).toFixed(1)}${t('modqueue.billion')}`;
|
||||
} else if (number / 1e6 > 1) {
|
||||
number = `${(number / 1e6).toFixed(1)}${t('modqueue.million')}`;
|
||||
} else if (number / 1e3 > 1) {
|
||||
number = `${(number / 1e3).toFixed(1)}${t('modqueue.thousand')}`;
|
||||
}
|
||||
number = humanizeNumber(number);
|
||||
|
||||
return <span className={styles.count}>{number}</span>;
|
||||
};
|
||||
|
||||
@@ -7,9 +7,9 @@ import t from 'coral-framework/services/i18n';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import cn from 'classnames';
|
||||
|
||||
const CoralDrawer = ({ handleLogout, currentUser }) => (
|
||||
<Drawer className={cn('talk-admin-drawer-nav', styles.drawer)}>
|
||||
{currentUser && can(currentUser, 'ACCESS_ADMIN') ? (
|
||||
const CoralDrawer = ({ handleLogout, currentUser }) =>
|
||||
currentUser && can(currentUser, 'ACCESS_ADMIN') ? (
|
||||
<Drawer className={cn('talk-admin-drawer-nav', styles.drawer)}>
|
||||
<div>
|
||||
<Navigation className={styles.nav}>
|
||||
{can(currentUser, 'MODERATE_COMMENTS') && (
|
||||
@@ -48,9 +48,8 @@ const CoralDrawer = ({ handleLogout, currentUser }) => (
|
||||
<span>{`v${process.env.VERSION}`}</span>
|
||||
</Navigation>
|
||||
</div>
|
||||
) : null}
|
||||
</Drawer>
|
||||
);
|
||||
</Drawer>
|
||||
) : null;
|
||||
|
||||
CoralDrawer.propTypes = {
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import React from 'react';
|
||||
import styles from './Forbidden.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const Forbidden = () => (
|
||||
<div className={styles.container}>
|
||||
<p className={styles.copy}>
|
||||
This page is for team use only. Please contact an administrator if you
|
||||
want to join this team.
|
||||
</p>
|
||||
<p className={styles.copy}>{t('error.PAGE_NOT_AVAILABLE_ROLE')}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class ForgotPassword extends React.Component {
|
||||
href="#"
|
||||
onClick={this.handleSignInLink}
|
||||
>
|
||||
Sign in
|
||||
{t('login.sign_in')}
|
||||
</a>
|
||||
<Success />
|
||||
</div>
|
||||
@@ -43,21 +43,21 @@ class ForgotPassword extends React.Component {
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
label="Email Address"
|
||||
label={t('login.email_address')}
|
||||
value={email}
|
||||
onChange={this.handleEmailChange}
|
||||
/>
|
||||
<Button type="submit" cStyle="black" full>
|
||||
Reset Password
|
||||
{t('login.reset_password_send_button')}
|
||||
</Button>
|
||||
<p className={styles.cta}>
|
||||
Go back to{' '}
|
||||
{t('login.go_back')}{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.signInLink}
|
||||
onClick={this.handleSignInLink}
|
||||
>
|
||||
Sign In
|
||||
{t('login.sign_in')}
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
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);
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2),
|
||||
0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.header > div {
|
||||
@@ -41,7 +42,7 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 170px;
|
||||
width: 200px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -85,7 +86,7 @@
|
||||
background-color: transparent;
|
||||
transition: background-color 200ms;
|
||||
font-weight: 100;
|
||||
letter-spacing: .8;
|
||||
letter-spacing: 0.8;
|
||||
|
||||
&:hover {
|
||||
background-color: #404040;
|
||||
|
||||
@@ -77,13 +77,22 @@ const CoralHeader = ({
|
||||
<MenuItem onClick={() => showShortcuts(true)}>
|
||||
{t('configure.shortcuts')}
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a
|
||||
href="https://docs.coralproject.net/talk/how-talk-works/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('configure.product_guide_link')}
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a
|
||||
href="https://github.com/coralproject/talk/releases"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
View latest version
|
||||
{t('configure.view_last_version')}
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
@@ -92,7 +101,7 @@ const CoralHeader = ({
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Report a bug or give feedback
|
||||
{t('configure.report_bug_or_feedback')}
|
||||
</a>
|
||||
</MenuItem>
|
||||
{currentUser && (
|
||||
|
||||
@@ -27,7 +27,9 @@ class KarmaTooltip extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { thresholds: { unreliable } } = this.props;
|
||||
const {
|
||||
thresholds: { unreliable },
|
||||
} = this.props;
|
||||
const { menuVisible } = this.state;
|
||||
|
||||
return (
|
||||
@@ -68,6 +70,7 @@ class KarmaTooltip extends React.Component {
|
||||
className={styles.link}
|
||||
href={t('user_detail.karma_docs_link')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('user_detail.learn_more')}
|
||||
</a>
|
||||
|
||||
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
|
||||
import styles from './Login.css';
|
||||
import Layout from 'coral-admin/src/components/Layout';
|
||||
import cn from 'classnames';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class LoginContainer extends Component {
|
||||
renderForm() {
|
||||
@@ -19,8 +20,8 @@ class LoginContainer extends Component {
|
||||
return (
|
||||
<Layout fixedDrawer restricted={true}>
|
||||
<div className={cn(styles.layout, 'talk-admin-login')}>
|
||||
<h1 className={styles.header}>Team sign in</h1>
|
||||
<p className={styles.cta}>Sign in to interact with your community.</p>
|
||||
<h1 className={styles.header}>{t('login.team_sign_in')}</h1>
|
||||
<p className={styles.cta}>{t('login.sign_in_message')}</p>
|
||||
{this.renderForm()}
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
.dialog {
|
||||
border: none;
|
||||
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
|
||||
width: 400px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.header {
|
||||
color: black;
|
||||
font-size: 1.5em;
|
||||
font-weight: 500;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.close {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
font-size: 24px;
|
||||
line-height: 14px;
|
||||
color: #363636;
|
||||
&:hover {
|
||||
color: #6b6b6b;
|
||||
}
|
||||
}
|
||||
|
||||
.legend {
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.radioGroup {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
label.radioGroup {
|
||||
|
||||
&:global(.is-checked) > :global(.mdl-radio__outer-circle),
|
||||
> :global(.mdl-radio__outer-circle) {
|
||||
border-color: #212121;
|
||||
}
|
||||
|
||||
> :global(.mdl-radio__inner-circle) {
|
||||
background: #212121;
|
||||
}
|
||||
|
||||
> :global(.mdl-radio__label) {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.messageInput {
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.perform {
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 6px;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Dialog, BareButton } from 'coral-ui';
|
||||
import styles from './RejectUsernameDialog.css';
|
||||
import cn from 'classnames';
|
||||
import { RadioGroup, Radio } from 'react-mdl';
|
||||
import Button from 'coral-ui/components/Button';
|
||||
import { username as flagReason } from 'coral-framework/graphql/flagReasons';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const initialState = { reason: flagReason.offensive, message: '' };
|
||||
|
||||
class RejectUsernameDialog extends React.Component {
|
||||
state = initialState;
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
if (this.props.open && !next.open) {
|
||||
this.setState(initialState);
|
||||
}
|
||||
}
|
||||
|
||||
handleReasonChange = event => {
|
||||
this.setState({ reason: event.target.value });
|
||||
};
|
||||
|
||||
handleMessageChange = event => {
|
||||
this.setState({ message: event.target.value });
|
||||
};
|
||||
|
||||
handlePerform = () => {
|
||||
this.props.onPerform({
|
||||
reason: this.state.reason,
|
||||
message: this.state.message,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { open, onCancel } = this.props;
|
||||
const { reason, message } = this.state;
|
||||
return (
|
||||
<Dialog
|
||||
className={cn(styles.dialog, 'talk-admin-reject-username-dialog')}
|
||||
id="rejectUsernameDialog"
|
||||
onCancel={onCancel}
|
||||
open={open}
|
||||
>
|
||||
<div className={styles.close}>
|
||||
<BareButton
|
||||
aria-label="Close"
|
||||
onClick={onCancel}
|
||||
className={styles.closeButton}
|
||||
>
|
||||
×
|
||||
</BareButton>
|
||||
</div>
|
||||
<section className="talk-admin-reject-username-dialog-section">
|
||||
<h1 className={styles.header}>
|
||||
{t('reject_username_dialog.title')}: {this.props.username}
|
||||
</h1>
|
||||
<p className={styles.description}>
|
||||
{t('reject_username_dialog.description')}
|
||||
</p>
|
||||
<fieldset>
|
||||
<legend className={styles.legend}>
|
||||
{t('reject_username_dialog.reason')}
|
||||
</legend>
|
||||
<RadioGroup
|
||||
name="reason"
|
||||
value={reason}
|
||||
childContainer="div"
|
||||
onChange={this.handleReasonChange}
|
||||
className={styles.radioGroup}
|
||||
>
|
||||
<Radio value={flagReason.offensive}>
|
||||
{t('flag_reasons.username.offensive')}
|
||||
</Radio>
|
||||
<Radio value={flagReason.nolike}>
|
||||
{t('flag_reasons.username.nolike')}
|
||||
</Radio>
|
||||
<Radio value={flagReason.impersonating}>
|
||||
{t('flag_reasons.username.impersonating')}
|
||||
</Radio>
|
||||
<Radio value={flagReason.spam}>
|
||||
{t('flag_reasons.username.spam')}
|
||||
</Radio>
|
||||
<Radio value={flagReason.other}>
|
||||
{t('flag_reasons.username.other')}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
{reason === flagReason.other && (
|
||||
<fieldset>
|
||||
<legend className={styles.legend}>
|
||||
{t('reject_username_dialog.message')}
|
||||
</legend>
|
||||
<textarea
|
||||
rows={5}
|
||||
className={styles.messageInput}
|
||||
value={message}
|
||||
onChange={this.handleMessageChange}
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
</fieldset>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
cStyle="white"
|
||||
className={styles.cancel}
|
||||
onClick={onCancel}
|
||||
raised
|
||||
>
|
||||
{t('reject_username_dialog.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
cStyle="black"
|
||||
className={cn(
|
||||
styles.perform,
|
||||
'talk-admin-reject-username-dialog-continue'
|
||||
)}
|
||||
onClick={this.handlePerform}
|
||||
raised
|
||||
>
|
||||
{t('reject_username_dialog.reject_username')}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RejectUsernameDialog.propTypes = {
|
||||
open: PropTypes.bool.isRequired,
|
||||
onPerform: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
username: PropTypes.string,
|
||||
};
|
||||
|
||||
export default RejectUsernameDialog;
|
||||
@@ -5,6 +5,7 @@ import { Button, TextField, Alert } from 'coral-ui';
|
||||
import cn from 'classnames';
|
||||
import Recaptcha from 'coral-framework/components/Recaptcha';
|
||||
import External from './External';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class SignIn extends React.Component {
|
||||
recaptcha = null;
|
||||
@@ -40,13 +41,13 @@ class SignIn extends React.Component {
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
id="email"
|
||||
label="Email Address"
|
||||
label={t('login.email_address')}
|
||||
value={email}
|
||||
onChange={this.handleEmailChange}
|
||||
/>
|
||||
<TextField
|
||||
id="password"
|
||||
label="Password"
|
||||
label={t('login.password')}
|
||||
value={password}
|
||||
onChange={this.handlePasswordChange}
|
||||
type="password"
|
||||
@@ -68,17 +69,17 @@ class SignIn extends React.Component {
|
||||
cStyle="black"
|
||||
full
|
||||
>
|
||||
Sign In
|
||||
{t('login.sign_in')}
|
||||
</Button>
|
||||
<p className={styles.forgotPasswordCTA}>
|
||||
{/* TODO: translate */}
|
||||
Forgot your password?{' '}
|
||||
{t('login.forgot_password')}{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.forgotPasswordLink}
|
||||
onClick={this.handleForgotPasswordLink}
|
||||
>
|
||||
Request a new one.
|
||||
{t('login.request_password')}
|
||||
</a>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
@@ -137,6 +137,12 @@
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.actionsMenuAlwaysPremod {
|
||||
background-color: #F0B50B;
|
||||
border-color: #F0B50B;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.actionsMenuSuspended {
|
||||
background-color: #F29336;
|
||||
border-color: #F29336;
|
||||
|
||||
@@ -6,7 +6,16 @@ import styles from './UserDetail.css';
|
||||
import UserHistory from './UserHistory';
|
||||
import { Slot } from 'coral-framework/components';
|
||||
import UserDetailCommentList from '../components/UserDetailCommentList';
|
||||
import { isSuspended, isBanned, getKarma } from 'coral-framework/utils/user';
|
||||
|
||||
import {
|
||||
isSuspended,
|
||||
isUsernameRejected,
|
||||
isUsernameChanged,
|
||||
isBanned,
|
||||
isAlwaysPremod,
|
||||
getKarma,
|
||||
} from 'coral-framework/utils/user';
|
||||
|
||||
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
import {
|
||||
@@ -23,6 +32,26 @@ import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
import UserInfoTooltip from './UserInfoTooltip';
|
||||
import KarmaTooltip from './KarmaTooltip';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { humanizeNumber } from 'coral-framework/helpers/numbers';
|
||||
|
||||
const filterOutLocalProfiles = ({ provider }) => provider !== 'local';
|
||||
|
||||
/**
|
||||
* getUserStatusArray
|
||||
* returns an array of active status(es)
|
||||
* i.e if suspension is active, it returns suspension
|
||||
*/
|
||||
|
||||
function getUserStatusArray(user) {
|
||||
const statusMap = {
|
||||
suspended: isSuspended,
|
||||
banned: isBanned,
|
||||
alwaysPremod: isAlwaysPremod,
|
||||
usernameRejected: isUsernameRejected,
|
||||
usernameChanged: isUsernameChanged,
|
||||
};
|
||||
return Object.keys(statusMap).filter(k => statusMap[k](user));
|
||||
}
|
||||
|
||||
class UserDetail extends React.Component {
|
||||
changeTab = tab => {
|
||||
@@ -41,6 +70,18 @@ class UserDetail extends React.Component {
|
||||
username: this.props.root.user.username,
|
||||
});
|
||||
|
||||
showAlwaysPremodUserDialog = () =>
|
||||
this.props.showAlwaysPremodUserDialog({
|
||||
userId: this.props.root.user.id,
|
||||
username: this.props.root.user.username,
|
||||
});
|
||||
|
||||
showRejectUsernameDialog = () =>
|
||||
this.props.showRejectUsernameDialog({
|
||||
userId: this.props.root.user.id,
|
||||
username: this.props.root.user.username,
|
||||
});
|
||||
|
||||
renderLoading() {
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.props.hideUserDetail}>
|
||||
@@ -61,18 +102,48 @@ class UserDetail extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
getActionMenuLabel() {
|
||||
const { root: { user } } = this.props;
|
||||
getActionMenuLabel(user) {
|
||||
const userStatusArr = getUserStatusArray(user);
|
||||
const count = userStatusArr.length;
|
||||
|
||||
if (isBanned(user)) {
|
||||
return 'Banned';
|
||||
} else if (isSuspended(user)) {
|
||||
return 'Suspended';
|
||||
if (count > 1) {
|
||||
return `Status (${count})`;
|
||||
} else {
|
||||
const activeStatus = userStatusArr[0];
|
||||
switch (activeStatus) {
|
||||
case 'suspended':
|
||||
return t('user_detail.suspended');
|
||||
case 'banned':
|
||||
return t('user_detail.banned');
|
||||
case 'alwaysPremod':
|
||||
return t('user_detail.always_premoded');
|
||||
case 'usernameRejected':
|
||||
return (
|
||||
<span>
|
||||
{t('user_detail.username')}
|
||||
{` `}
|
||||
<Icon name="cancel" />
|
||||
</span>
|
||||
);
|
||||
case 'usernameChanged':
|
||||
return (
|
||||
<span>
|
||||
{t('user_detail.username')}
|
||||
{` `}
|
||||
<Icon name="access_time" />
|
||||
</span>
|
||||
);
|
||||
default:
|
||||
return activeStatus;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
goToReportedUsernames = () => {
|
||||
const { router } = this.props;
|
||||
router.push('/admin/community/flagged');
|
||||
};
|
||||
|
||||
renderLoaded() {
|
||||
const {
|
||||
root,
|
||||
@@ -92,6 +163,7 @@ class UserDetail extends React.Component {
|
||||
toggleSelectAll,
|
||||
unbanUser,
|
||||
unsuspendUser,
|
||||
removeAlwaysPremodUser,
|
||||
modal,
|
||||
acceptComment,
|
||||
rejectComment,
|
||||
@@ -100,7 +172,7 @@ class UserDetail extends React.Component {
|
||||
} = this.props;
|
||||
|
||||
// if totalComments is 0, you're dividing by zero
|
||||
let rejectedPercent = rejectedComments / totalComments * 100;
|
||||
let rejectedPercent = (rejectedComments / totalComments) * 100;
|
||||
|
||||
if (rejectedPercent === Infinity || isNaN(rejectedPercent)) {
|
||||
rejectedPercent = 0;
|
||||
@@ -108,6 +180,9 @@ class UserDetail extends React.Component {
|
||||
|
||||
const banned = isBanned(user);
|
||||
const suspended = isSuspended(user);
|
||||
const alwaysPremod = isAlwaysPremod(user);
|
||||
const usernameRejected = isUsernameRejected(user);
|
||||
const usernameChanged = isUsernameChanged(user);
|
||||
|
||||
const slotPassthrough = {
|
||||
root,
|
||||
@@ -137,10 +212,11 @@ class UserDetail extends React.Component {
|
||||
{
|
||||
[styles.actionsMenuSuspended]: suspended,
|
||||
[styles.actionsMenuBanned]: banned,
|
||||
[styles.actionsMenuAlwaysPremod]: alwaysPremod,
|
||||
},
|
||||
'talk-admin-user-detail-actions-button'
|
||||
)}
|
||||
label={this.getActionMenuLabel()}
|
||||
label={this.getActionMenuLabel(user)}
|
||||
>
|
||||
{suspended ? (
|
||||
<ActionsMenuItem onClick={() => unsuspendUser({ id: user.id })}>
|
||||
@@ -167,6 +243,42 @@ class UserDetail extends React.Component {
|
||||
{t('user_detail.ban')}
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
|
||||
{alwaysPremod ? (
|
||||
<ActionsMenuItem
|
||||
onClick={() => removeAlwaysPremodUser({ id: user.id })}
|
||||
>
|
||||
{t('user_detail.remove_always_premod')}
|
||||
</ActionsMenuItem>
|
||||
) : (
|
||||
<ActionsMenuItem
|
||||
disabled={me.id === user.id}
|
||||
onClick={this.showAlwaysPremodUserDialog}
|
||||
>
|
||||
{t('user_detail.always_premod')}
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
|
||||
{usernameChanged && (
|
||||
<ActionsMenuItem onClick={this.goToReportedUsernames}>
|
||||
{t('user_detail.username_needs_approval')}
|
||||
{` `}
|
||||
<Icon name="launch" />
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
|
||||
{usernameRejected && !usernameChanged ? (
|
||||
<ActionsMenuItem disabled>
|
||||
{t('user_detail.username_rejected')}
|
||||
</ActionsMenuItem>
|
||||
) : (
|
||||
<ActionsMenuItem
|
||||
onClick={this.showRejectUsernameDialog}
|
||||
disabled={me.id === user.id || usernameChanged}
|
||||
>
|
||||
{t('user_detail.reject_username')}
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
</ActionsMenu>
|
||||
)}
|
||||
|
||||
@@ -180,6 +292,18 @@ class UserDetail extends React.Component {
|
||||
|
||||
<div>
|
||||
<ul className={styles.userDetailList}>
|
||||
<li className={styles.userDetailItem}>
|
||||
<Icon name="perm_identity" />
|
||||
<span className={styles.userDetailItem}>
|
||||
{t('user_detail.id')}:
|
||||
</span>
|
||||
{user.id}{' '}
|
||||
<ButtonCopyToClipboard
|
||||
className={styles.copyButton}
|
||||
icon="content_copy"
|
||||
copyText={user.id}
|
||||
/>
|
||||
</li>
|
||||
<li className={styles.userDetailItem}>
|
||||
<Icon name="assignment_ind" />
|
||||
<span className={styles.userDetailItem}>
|
||||
@@ -201,20 +325,22 @@ class UserDetail extends React.Component {
|
||||
/>
|
||||
</li>
|
||||
|
||||
{user.profiles.map(({ provider, id }) => (
|
||||
<li key={id} className={styles.userDetailItem}>
|
||||
<Icon name="device_hub" />
|
||||
<span className={styles.userDetailItem}>
|
||||
{capitalize(provider)} {t('user_detail.id')}:
|
||||
</span>
|
||||
{id}{' '}
|
||||
<ButtonCopyToClipboard
|
||||
className={styles.copyButton}
|
||||
icon="content_copy"
|
||||
copyText={id}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
{user.profiles
|
||||
.filter(filterOutLocalProfiles)
|
||||
.map(({ provider, id }) => (
|
||||
<li key={id} className={styles.userDetailItem}>
|
||||
<Icon name="device_hub" />
|
||||
<span className={styles.userDetailItem}>
|
||||
{capitalize(provider)} {t('user_detail.id')}:
|
||||
</span>
|
||||
{id}{' '}
|
||||
<ButtonCopyToClipboard
|
||||
className={styles.copyButton}
|
||||
icon="content_copy"
|
||||
copyText={id}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<ul className={styles.stats}>
|
||||
@@ -243,7 +369,7 @@ class UserDetail extends React.Component {
|
||||
styles[getKarma(user.reliable.commenter)]
|
||||
)}
|
||||
>
|
||||
{user.reliable.commenterKarma}
|
||||
{humanizeNumber(user.reliable.commenterKarma)}
|
||||
</span>
|
||||
</div>
|
||||
<KarmaTooltip thresholds={karmaThresholds.comment} />
|
||||
@@ -358,6 +484,7 @@ class UserDetail extends React.Component {
|
||||
}
|
||||
|
||||
UserDetail.propTypes = {
|
||||
router: PropTypes.object.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
hideUserDetail: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
@@ -374,11 +501,15 @@ UserDetail.propTypes = {
|
||||
selectedCommentIds: PropTypes.array.isRequired,
|
||||
viewUserDetail: PropTypes.any.isRequired,
|
||||
loadMore: PropTypes.any.isRequired,
|
||||
showRejectUsernameDialog: PropTypes.func,
|
||||
showSuspendUserDialog: PropTypes.func,
|
||||
showBanUserDialog: PropTypes.func,
|
||||
showAlwaysPremodUserDialog: PropTypes.func,
|
||||
unbanUser: PropTypes.func.isRequired,
|
||||
unsuspendUser: PropTypes.func.isRequired,
|
||||
removeAlwaysPremodUser: PropTypes.func.isRequired,
|
||||
modal: PropTypes.bool,
|
||||
rejectUsername: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default UserDetail;
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
color: #063b9a;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
letter-spacing: .5px;
|
||||
letter-spacing: 0.5px;
|
||||
margin-left: 10px;
|
||||
font-size: 13px;
|
||||
margin-left: 5px;
|
||||
@@ -109,7 +109,7 @@
|
||||
}
|
||||
|
||||
.external {
|
||||
font-size: .7em;
|
||||
font-size: 0.7em;
|
||||
text-decoration: none;
|
||||
color: #063b9a;
|
||||
cursor: pointer;
|
||||
@@ -119,7 +119,7 @@
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
opacity: .9;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
> i {
|
||||
@@ -139,3 +139,25 @@
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.bodyHistoryToggle {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
line-height: 1px;
|
||||
font-weight: 300;
|
||||
text-decoration: underline;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.editedCommentBody {
|
||||
padding: 0 5px 5px 5px;
|
||||
}
|
||||
|
||||
.editedComment {
|
||||
margin-top: 6px;
|
||||
font-weight: 300;
|
||||
font-size: 16px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
@@ -14,9 +14,13 @@ import ApproveButton from './ApproveButton';
|
||||
import RejectButton from 'coral-admin/src/components/RejectButton';
|
||||
import CommentDeletedTombstone from './CommentDeletedTombstone';
|
||||
|
||||
import t, { timeago } from 'coral-framework/services/i18n';
|
||||
import { buildCommentURL } from 'coral-framework/utils/url';
|
||||
import TimeAgo from 'coral-framework/components/TimeAgo';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class UserDetailComment extends React.Component {
|
||||
state = { showingEditHistory: false };
|
||||
|
||||
approve = () =>
|
||||
this.props.comment.status === 'ACCEPTED'
|
||||
? null
|
||||
@@ -27,6 +31,29 @@ class UserDetailComment extends React.Component {
|
||||
? null
|
||||
: this.props.rejectComment({ commentId: this.props.comment.id });
|
||||
|
||||
getBodyHistory = () => {
|
||||
const bodyHistory = [];
|
||||
const comment = this.props.comment;
|
||||
for (let i = 0; i < comment.body_history.length - 1; i++) {
|
||||
bodyHistory.push(
|
||||
<div key={i} className={styles.editedComment}>
|
||||
<div>
|
||||
<TimeAgo className={styles.created} datetime={comment.created_at} />
|
||||
</div>
|
||||
<div className={styles.editedCommentBody}>
|
||||
{comment.body_history[i].body}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return bodyHistory;
|
||||
};
|
||||
|
||||
toggleEditHistory = () => {
|
||||
this.setState({ showingEditHistory: !this.state.showingEditHistory });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
comment,
|
||||
@@ -73,14 +100,20 @@ class UserDetailComment extends React.Component {
|
||||
checked={selected}
|
||||
onChange={e => toggleSelect(e.target.value, e.target.checked)}
|
||||
/>
|
||||
<span className={styles.created}>
|
||||
{timeago(comment.created_at)}
|
||||
</span>
|
||||
<TimeAgo className={styles.created} datetime={comment.created_at} />
|
||||
{comment.editing && comment.editing.edited ? (
|
||||
<span>
|
||||
<span className={styles.editedMarker}>
|
||||
({t('comment.edited')})
|
||||
</span>
|
||||
<span
|
||||
className={styles.bodyHistoryToggle}
|
||||
onClick={this.toggleEditHistory}
|
||||
>
|
||||
{this.state.showingEditHistory
|
||||
? t('comment.hide_edit_history')
|
||||
: t('comment.show_edit_history')}
|
||||
</span>
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
@@ -112,8 +145,9 @@ class UserDetailComment extends React.Component {
|
||||
/>
|
||||
<a
|
||||
className={styles.external}
|
||||
href={`${comment.asset.url}?commentId=${comment.id}`}
|
||||
href={buildCommentURL(comment.asset.url, comment.id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon name="open_in_new" /> {t('comment.view_context')}
|
||||
</a>
|
||||
@@ -122,7 +156,7 @@ class UserDetailComment extends React.Component {
|
||||
<IfHasLink text={comment.body}>
|
||||
<span className={styles.hasLinks}>
|
||||
{/* TODO: translate string */}
|
||||
<Icon name="error_outline" /> Contains Link
|
||||
<Icon name="error_outline" /> {t('common.contains_link')}
|
||||
</span>
|
||||
</IfHasLink>
|
||||
<div className={styles.actions}>
|
||||
@@ -141,6 +175,14 @@ class UserDetailComment extends React.Component {
|
||||
</div>
|
||||
</CommentAnimatedEdit>
|
||||
</div>
|
||||
|
||||
{this.state.showingEditHistory ? (
|
||||
<div className={styles.container}>
|
||||
{t('comment.edit_history')}
|
||||
{this.getBodyHistory()}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<CommentDetails root={root} comment={comment} />
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,10 @@ import ApproveButton from './ApproveButton';
|
||||
const UserDetailCommentList = props => {
|
||||
const {
|
||||
root,
|
||||
root: { user, comments: { nodes, hasNextPage } },
|
||||
root: {
|
||||
user,
|
||||
comments: { nodes, hasNextPage },
|
||||
},
|
||||
acceptComment,
|
||||
rejectComment,
|
||||
selectedCommentIds,
|
||||
|
||||
@@ -48,6 +48,10 @@ const buildActionResponse = (typename, created_at, until, status) => {
|
||||
return status
|
||||
? t('user_history.user_banned')
|
||||
: t('user_history.ban_removed');
|
||||
case 'AlwaysPremodStatusHistory':
|
||||
return status
|
||||
? t('user_history.user_always_premoded')
|
||||
: t('user_history.always_premod_removed');
|
||||
case 'SuspensionStatusHistory':
|
||||
return until
|
||||
? t('user_history.suspended', readableDuration(created_at, until))
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const SHOW_ALWAYS_PREMOD_USER_DIALOG = 'SHOW_ALWAYS_PREMOD_USER_DIALOG';
|
||||
export const HIDE_ALWAYS_PREMOD_USER_DIALOG = 'HIDE_ALWAYS_PREMOD_USER_DIALOG';
|
||||
@@ -14,6 +14,9 @@ export const FETCH_FLAGGED_COMMENTERS_FAILURE = `${prefix}_FETCH_FLAGGED_COMMENT
|
||||
export const SHOW_BANUSER_DIALOG = `${prefix}_SHOW_BANUSER_DIALOG`;
|
||||
export const HIDE_BANUSER_DIALOG = `${prefix}_HIDE_BANUSER_DIALOG`;
|
||||
|
||||
export const SHOW_ALWAYS_PREMOD_USER_DIALOG = `${prefix}_SHOW_ALWAYS_PREMOD_USER_DIALOG`;
|
||||
export const HIDE_ALWAYS_PREMOD_USER_DIALOG = `${prefix}_HIDE_ALWAYS_PREMOD_USER_DIALOG`;
|
||||
|
||||
export const SHOW_REJECT_USERNAME_DIALOG = `${prefix}_SHOW_REJECT_USERNAME_DIALOG`;
|
||||
export const HIDE_REJECT_USERNAME_DIALOG = `${prefix}_HIDE_REJECT_USERNAME_DIALOG`;
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const SHOW_REJECT_USERNAME_DIALOG = 'SHOW_REJECT_USERNAME_DIALOG';
|
||||
export const HIDE_REJECT_USERNAME_DIALOG = 'HIDE_REJECT_USERNAME_DIALOG';
|
||||
@@ -4,12 +4,14 @@ 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 LOAD_MORE_ASSETS_REQUEST = `${prefix}_LOAD_MORE_ASSETS_REQUEST`;
|
||||
export const LOAD_MORE_ASSETS_SUCCESS = `${prefix}_LOAD_MORE_ASSETS_SUCCESS`;
|
||||
export const LOAD_MORE_ASSETS_FAILURE = `${prefix}_LOAD_MORE_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`;
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import AlwaysPremodUserDialog from '../components/AlwaysPremodUserDialog';
|
||||
import { hideAlwaysPremodUserDialog } from '../actions/alwaysPremodUserDialog';
|
||||
import { withAlwaysPremodUser } from 'coral-framework/graphql/mutations';
|
||||
import { compose } from 'react-apollo';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class AlwaysPremodUserDialogContainer extends Component {
|
||||
alwaysPremodUser = async () => {
|
||||
const { userId, alwaysPremodUser, hideAlwaysPremodUserDialog } = this.props;
|
||||
await alwaysPremodUser({ id: userId });
|
||||
hideAlwaysPremodUserDialog();
|
||||
};
|
||||
|
||||
getInfo() {
|
||||
let note = t('alwayspremoddialog.note_always_premod_user');
|
||||
return t('alwayspremoddialog.note', note);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AlwaysPremodUserDialog
|
||||
open={this.props.open}
|
||||
onPerform={this.alwaysPremodUser}
|
||||
onCancel={this.props.hideAlwaysPremodUserDialog}
|
||||
username={this.props.username}
|
||||
info={this.getInfo()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AlwaysPremodUserDialogContainer.propTypes = {
|
||||
alwaysPremodUser: PropTypes.func.isRequired,
|
||||
hideAlwaysPremodUserDialog: PropTypes.func,
|
||||
open: PropTypes.bool,
|
||||
username: PropTypes.string,
|
||||
};
|
||||
|
||||
const mapStateToProps = ({
|
||||
alwaysPremodUserDialog: { open, userId, username },
|
||||
}) => ({
|
||||
open,
|
||||
userId,
|
||||
username,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
...bindActionCreators(
|
||||
{
|
||||
hideAlwaysPremodUserDialog,
|
||||
},
|
||||
dispatch
|
||||
),
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withAlwaysPremodUser
|
||||
)(AlwaysPremodUserDialogContainer);
|
||||
@@ -77,7 +77,10 @@ const mapDispatchToProps = dispatch => ({
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withBanUser,
|
||||
withSetCommentStatus
|
||||
)(BanUserDialogContainer);
|
||||
|
||||
@@ -5,7 +5,9 @@ import Layout from '../components/Layout';
|
||||
import Login from '../containers/Login';
|
||||
import { FullLoading } from '../components/FullLoading';
|
||||
import BanUserDialog from './BanUserDialog';
|
||||
import AlwaysPremodUserDialog from './AlwaysPremodUserDialog';
|
||||
import SuspendUserDialog from './SuspendUserDialog';
|
||||
import RejectUsernameDialog from './RejectUsernameDialog';
|
||||
import { toggleModal as toggleShortcutModal } from '../actions/moderation';
|
||||
import { logout } from 'coral-framework/actions/auth';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
@@ -39,8 +41,10 @@ class LayoutContainer extends React.Component {
|
||||
toggleShortcutModal={toggleShortcutModal}
|
||||
currentUser={this.props.currentUser}
|
||||
>
|
||||
<AlwaysPremodUserDialog />
|
||||
<BanUserDialog />
|
||||
<SuspendUserDialog />
|
||||
<RejectUsernameDialog />
|
||||
<UserDetail />
|
||||
{children}
|
||||
</Layout>
|
||||
@@ -79,4 +83,7 @@ const mapDispatchToProps = dispatch =>
|
||||
dispatch
|
||||
);
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(LayoutContainer);
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import RejectUsernameDialog from '../components/RejectUsernameDialog';
|
||||
import { hideRejectUsernameDialog } from '../actions/rejectUsernameDialog';
|
||||
import {
|
||||
withRejectUsername,
|
||||
withPostFlag,
|
||||
} from 'coral-framework/graphql/mutations';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
import { compose } from 'react-apollo';
|
||||
import { getErrorMessages } from 'coral-framework/utils';
|
||||
|
||||
class RejectUsernameDialogContainer extends Component {
|
||||
rejectUsername = async ({ reason, message }) => {
|
||||
const {
|
||||
postFlag,
|
||||
rejectUsername,
|
||||
hideRejectUsernameDialog,
|
||||
userId,
|
||||
} = this.props;
|
||||
|
||||
// First flag the user.
|
||||
try {
|
||||
await postFlag({
|
||||
item_id: userId,
|
||||
item_type: 'USERS',
|
||||
reason,
|
||||
message,
|
||||
});
|
||||
} catch (error) {
|
||||
// Ignore already exists error, otherwise show error.
|
||||
if (
|
||||
error.errors &&
|
||||
(error.errors.length !== 1 ||
|
||||
error.errors[0].translation_key !== 'ALREADY_EXISTS')
|
||||
) {
|
||||
notify('error', getErrorMessages(error));
|
||||
}
|
||||
}
|
||||
|
||||
await rejectUsername(userId);
|
||||
hideRejectUsernameDialog();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<RejectUsernameDialog
|
||||
open={this.props.open}
|
||||
onPerform={this.rejectUsername}
|
||||
onCancel={this.props.hideRejectUsernameDialog}
|
||||
username={this.props.username}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RejectUsernameDialogContainer.propTypes = {
|
||||
rejectUsername: PropTypes.func.isRequired,
|
||||
hideRejectUsernameDialog: PropTypes.func,
|
||||
open: PropTypes.bool,
|
||||
userId: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
};
|
||||
|
||||
const mapStateToProps = ({
|
||||
rejectUsernameDialog: { open, userId, username },
|
||||
}) => ({
|
||||
open,
|
||||
userId,
|
||||
username,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
...bindActionCreators(
|
||||
{
|
||||
hideRejectUsernameDialog,
|
||||
notify,
|
||||
},
|
||||
dispatch
|
||||
),
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withRejectUsername,
|
||||
withPostFlag({ notifyOnError: false })
|
||||
)(RejectUsernameDialogContainer);
|
||||
@@ -55,4 +55,7 @@ SignInContainer.propTypes = {
|
||||
requireRecaptcha: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default compose(withSignIn, withPopupAuthHandler)(SignInContainer);
|
||||
export default compose(
|
||||
withSignIn,
|
||||
withPopupAuthHandler
|
||||
)(SignInContainer);
|
||||
|
||||
@@ -86,7 +86,10 @@ const mapDispatchToProps = dispatch => ({
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withSuspendUser,
|
||||
withSetCommentStatus,
|
||||
withOrganizationName
|
||||
|
||||
@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import UserDetail from '../components/UserDetail';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import { withRouter } from 'react-router';
|
||||
import {
|
||||
getDefinitionName,
|
||||
getSlotFragmentSpreads,
|
||||
@@ -21,11 +22,16 @@ import {
|
||||
withSetCommentStatus,
|
||||
withUnbanUser,
|
||||
withUnsuspendUser,
|
||||
withRemoveAlwaysPremodUser,
|
||||
withRejectUsername,
|
||||
withPostFlag,
|
||||
} from 'coral-framework/graphql/mutations';
|
||||
import UserDetailComment from './UserDetailComment';
|
||||
import update from 'immutability-helper';
|
||||
import { showBanUserDialog } from 'actions/banUserDialog';
|
||||
import { showAlwaysPremodUserDialog } from 'actions/alwaysPremodUserDialog';
|
||||
import { showSuspendUserDialog } from 'actions/suspendUserDialog';
|
||||
import { showRejectUsernameDialog } from 'actions/rejectUsernameDialog';
|
||||
|
||||
const commentConnectionFragment = gql`
|
||||
fragment CoralAdmin_UserDetail_CommentConnection on CommentConnection {
|
||||
@@ -131,6 +137,7 @@ class UserDetailContainer extends React.Component {
|
||||
loading={loading}
|
||||
error={this.props.data && this.props.data.error}
|
||||
loadMore={this.loadMore}
|
||||
rejectUsername={this.props.rejectUsername}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
@@ -148,6 +155,8 @@ UserDetailContainer.propTypes = {
|
||||
selectedCommentIds: PropTypes.array,
|
||||
unbanUser: PropTypes.func.isRequired,
|
||||
unsuspendUser: PropTypes.func.isRequired,
|
||||
removeAlwaysPremodUser: PropTypes.func.isRequired,
|
||||
rejectUsername: PropTypes.func.isRequired,
|
||||
userId: PropTypes.string,
|
||||
};
|
||||
|
||||
@@ -212,6 +221,17 @@ export const withUserDetailQuery = withQuery(
|
||||
created_at
|
||||
}
|
||||
}
|
||||
alwaysPremod {
|
||||
status
|
||||
history {
|
||||
status
|
||||
assigned_by {
|
||||
id
|
||||
username
|
||||
}
|
||||
created_at
|
||||
}
|
||||
},
|
||||
username {
|
||||
status
|
||||
history {
|
||||
@@ -274,7 +294,9 @@ const mapDispatchToProps = dispatch => ({
|
||||
...bindActionCreators(
|
||||
{
|
||||
showBanUserDialog,
|
||||
showAlwaysPremodUserDialog,
|
||||
showSuspendUserDialog,
|
||||
showRejectUsernameDialog,
|
||||
changeTab,
|
||||
clearUserDetailSelections,
|
||||
toggleSelectCommentInUserDetail,
|
||||
@@ -287,9 +309,16 @@ const mapDispatchToProps = dispatch => ({
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withUserDetailQuery,
|
||||
withSetCommentStatus,
|
||||
withUnbanUser,
|
||||
withUnsuspendUser
|
||||
withUnsuspendUser,
|
||||
withRemoveAlwaysPremodUser,
|
||||
withRejectUsername,
|
||||
withPostFlag,
|
||||
withRouter
|
||||
)(UserDetailContainer);
|
||||
|
||||
@@ -42,6 +42,10 @@ export default withFragments({
|
||||
status_history {
|
||||
type
|
||||
}
|
||||
body_history {
|
||||
body
|
||||
created_at
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'comment')}
|
||||
...${getDefinitionName(CommentLabels.fragments.comment)}
|
||||
...${getDefinitionName(CommentDetails.fragments.comment)}
|
||||
|
||||
@@ -10,6 +10,9 @@ const userStatusFragment = gql`
|
||||
banned {
|
||||
status
|
||||
}
|
||||
alwaysPremod {
|
||||
status
|
||||
}
|
||||
suspension {
|
||||
until
|
||||
}
|
||||
@@ -66,7 +69,11 @@ export default {
|
||||
});
|
||||
},
|
||||
}),
|
||||
SuspendUser: ({ variables: { input: { id, until } } }) => ({
|
||||
SuspendUser: ({
|
||||
variables: {
|
||||
input: { id, until },
|
||||
},
|
||||
}) => ({
|
||||
update: proxy => {
|
||||
const fragmentId = `User_${id}`;
|
||||
|
||||
@@ -92,7 +99,11 @@ export default {
|
||||
});
|
||||
},
|
||||
}),
|
||||
UnsuspendUser: ({ variables: { input: { id } } }) => ({
|
||||
UnsuspendUser: ({
|
||||
variables: {
|
||||
input: { id },
|
||||
},
|
||||
}) => ({
|
||||
update: proxy => {
|
||||
const fragmentId = `User_${id}`;
|
||||
const data = proxy.readFragment({
|
||||
@@ -117,7 +128,69 @@ export default {
|
||||
});
|
||||
},
|
||||
}),
|
||||
BanUser: ({ variables: { input: { id } } }) => ({
|
||||
AlwaysPremodUser: ({
|
||||
variables: {
|
||||
input: { id },
|
||||
},
|
||||
}) => ({
|
||||
update: proxy => {
|
||||
const fragmentId = `User_${id}`;
|
||||
const data = proxy.readFragment({
|
||||
fragment: userStatusFragment,
|
||||
id: fragmentId,
|
||||
});
|
||||
|
||||
const updated = update(data, {
|
||||
state: {
|
||||
status: {
|
||||
alwaysPremod: {
|
||||
status: { $set: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
proxy.writeFragment({
|
||||
fragment: userStatusFragment,
|
||||
id: fragmentId,
|
||||
data: updated,
|
||||
});
|
||||
},
|
||||
}),
|
||||
RemoveAlwaysPremodUser: ({
|
||||
variables: {
|
||||
input: { id },
|
||||
},
|
||||
}) => ({
|
||||
update: proxy => {
|
||||
const fragmentId = `User_${id}`;
|
||||
const data = proxy.readFragment({
|
||||
fragment: userStatusFragment,
|
||||
id: fragmentId,
|
||||
});
|
||||
|
||||
const updated = update(data, {
|
||||
state: {
|
||||
status: {
|
||||
alwaysPremod: {
|
||||
status: { $set: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
proxy.writeFragment({
|
||||
fragment: userStatusFragment,
|
||||
id: fragmentId,
|
||||
data: updated,
|
||||
});
|
||||
},
|
||||
}),
|
||||
BanUser: ({
|
||||
variables: {
|
||||
input: { id },
|
||||
},
|
||||
}) => ({
|
||||
update: proxy => {
|
||||
const fragmentId = `User_${id}`;
|
||||
const data = proxy.readFragment({
|
||||
@@ -142,7 +215,28 @@ export default {
|
||||
});
|
||||
},
|
||||
}),
|
||||
UnbanUser: ({ variables: { input: { id } } }) => ({
|
||||
SetUserAlwaysPremodStatus: ({ variables: { status, id } }) => ({
|
||||
updateQueries: {
|
||||
TalkAdmin_Community: prev => {
|
||||
if (!status) {
|
||||
return prev;
|
||||
}
|
||||
const updated = update(prev, {
|
||||
users: {
|
||||
nodes: {
|
||||
$apply: nodes => nodes.filter(node => node.id !== id),
|
||||
},
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
},
|
||||
},
|
||||
}),
|
||||
UnbanUser: ({
|
||||
variables: {
|
||||
input: { id },
|
||||
},
|
||||
}) => ({
|
||||
update: proxy => {
|
||||
const fragmentId = `User_${id}`;
|
||||
const data = proxy.readFragment({
|
||||
@@ -194,6 +288,12 @@ export default {
|
||||
},
|
||||
updateQueries: {
|
||||
TalkAdmin_Community_FlaggedAccounts: (prev, { mutationResult }) => {
|
||||
// No need to update, when user was not in the flagged users queue.
|
||||
// TODO: this should be more generic, e.g. looking at the history.
|
||||
if (!prev.flaggedUsers.nodes.find(node => node.id === id)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const decrement = {
|
||||
flaggedUsernamesCount: { $apply: count => count - 1 },
|
||||
};
|
||||
@@ -214,35 +314,6 @@ export default {
|
||||
return updated;
|
||||
},
|
||||
},
|
||||
update: proxy => {
|
||||
proxy.writeFragment({
|
||||
fragment: gql`
|
||||
fragment Talk_ApproveUsername on User {
|
||||
state {
|
||||
status {
|
||||
username {
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
id: `User_${id}`,
|
||||
data: {
|
||||
__typename: 'User',
|
||||
state: {
|
||||
__typename: 'UserState',
|
||||
status: {
|
||||
__typename: 'UserStatus',
|
||||
username: {
|
||||
__typename: 'UsernameStatus',
|
||||
status: 'APPROVED',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
RejectUsername: ({ variables: { id } }) => ({
|
||||
optimisticResponse: {
|
||||
@@ -254,6 +325,12 @@ export default {
|
||||
},
|
||||
updateQueries: {
|
||||
TalkAdmin_Community_FlaggedAccounts: (prev, { mutationResult }) => {
|
||||
// No need to update, when user was not in the flagged users queue.
|
||||
// TODO: this should be more generic, e.g. looking at the history.
|
||||
if (!prev.flaggedUsers.nodes.find(node => node.id === id)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const decrement = {
|
||||
flaggedUsernamesCount: { $apply: count => count - 1 },
|
||||
};
|
||||
@@ -274,35 +351,6 @@ export default {
|
||||
return updated;
|
||||
},
|
||||
},
|
||||
update: proxy => {
|
||||
proxy.writeFragment({
|
||||
fragment: gql`
|
||||
fragment Talk_RejectUsername on User {
|
||||
state {
|
||||
status {
|
||||
username {
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
id: `User_${id}`,
|
||||
data: {
|
||||
__typename: 'User',
|
||||
state: {
|
||||
__typename: 'UserState',
|
||||
status: {
|
||||
__typename: 'UserStatus',
|
||||
username: {
|
||||
__typename: 'UsernameStatus',
|
||||
status: 'REJECTED',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
UpdateSettings: ({ variables: { input } }) => ({
|
||||
updateQueries: {
|
||||
|
||||
@@ -14,6 +14,11 @@ import { hideShortcutsNote } from './actions/moderation';
|
||||
|
||||
smoothscroll.polyfill();
|
||||
|
||||
if (!NodeList.prototype.forEach) {
|
||||
// Polyfill IE11 missing forEach in NodeList.
|
||||
NodeList.prototype.forEach = Array.prototype.forEach;
|
||||
}
|
||||
|
||||
function init({ store, localStorage }) {
|
||||
const shouldHide = localStorage.getItem('coral:shortcutsNote') === 'hide';
|
||||
if (shouldHide) {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
SHOW_ALWAYS_PREMOD_USER_DIALOG,
|
||||
HIDE_ALWAYS_PREMOD_USER_DIALOG,
|
||||
} from '../constants/alwaysPremodUserDialog';
|
||||
|
||||
const initialState = {
|
||||
open: false,
|
||||
userId: null,
|
||||
username: '',
|
||||
};
|
||||
|
||||
export default function alwaysPremodUserDialog(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case SHOW_ALWAYS_PREMOD_USER_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
open: true,
|
||||
userId: action.userId,
|
||||
username: action.username,
|
||||
};
|
||||
case HIDE_ALWAYS_PREMOD_USER_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
open: false,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,17 @@ import moderation from './moderation';
|
||||
import install from './install';
|
||||
import banUserDialog from './banUserDialog';
|
||||
import suspendUserDialog from './suspendUserDialog';
|
||||
import alwaysPremodUserDialog from './alwaysPremodUserDialog';
|
||||
import rejectUsernameDialog from './rejectUsernameDialog';
|
||||
import userDetail from './userDetail';
|
||||
import ui from './ui';
|
||||
|
||||
export default {
|
||||
rejectUsernameDialog,
|
||||
banUserDialog,
|
||||
configure,
|
||||
suspendUserDialog,
|
||||
alwaysPremodUserDialog,
|
||||
userDetail,
|
||||
stories,
|
||||
community,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
SHOW_REJECT_USERNAME_DIALOG,
|
||||
HIDE_REJECT_USERNAME_DIALOG,
|
||||
} from '../constants/rejectUsernameDialog';
|
||||
|
||||
const initialState = {
|
||||
open: false,
|
||||
userId: null,
|
||||
username: '',
|
||||
};
|
||||
|
||||
export default function rejectUsernameDialog(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case SHOW_REJECT_USERNAME_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
open: true,
|
||||
userId: action.userId,
|
||||
username: action.username,
|
||||
};
|
||||
case HIDE_REJECT_USERNAME_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
open: false,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -3,56 +3,79 @@ import update from 'immutability-helper';
|
||||
|
||||
const initialState = {
|
||||
assets: {
|
||||
byId: {},
|
||||
ids: [],
|
||||
assets: [],
|
||||
edges: [],
|
||||
pageInfo: {},
|
||||
},
|
||||
searchValue: '',
|
||||
criteria: {
|
||||
asc: 'false',
|
||||
filter: 'all',
|
||||
},
|
||||
loading: true,
|
||||
loadingMore: false,
|
||||
};
|
||||
|
||||
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;
|
||||
}, {});
|
||||
|
||||
case actions.FETCH_ASSETS_REQUEST: {
|
||||
return update(state, {
|
||||
loading: { $set: true },
|
||||
});
|
||||
}
|
||||
case actions.FETCH_ASSETS_FAILURE: {
|
||||
return update(state, {
|
||||
loading: { $set: false },
|
||||
});
|
||||
}
|
||||
case actions.FETCH_ASSETS_SUCCESS: {
|
||||
return update(state, {
|
||||
loading: { $set: false },
|
||||
assets: {
|
||||
totalPages: { $set: action.totalPages },
|
||||
page: { $set: action.page },
|
||||
byId: { $set: assets },
|
||||
count: { $set: action.count },
|
||||
ids: { $set: Object.keys(assets) },
|
||||
edges: { $set: action.edges },
|
||||
pageInfo: { $set: action.pageInfo },
|
||||
},
|
||||
});
|
||||
}
|
||||
case actions.UPDATE_ASSET_STATE_REQUEST:
|
||||
case actions.LOAD_MORE_ASSETS_REQUEST: {
|
||||
return update(state, {
|
||||
loadingMore: { $set: true },
|
||||
});
|
||||
}
|
||||
case actions.LOAD_MORE_ASSETS_FAILURE: {
|
||||
return update(state, {
|
||||
loadingMore: { $set: false },
|
||||
});
|
||||
}
|
||||
case actions.LOAD_MORE_ASSETS_SUCCESS: {
|
||||
return update(state, {
|
||||
loadingMore: { $set: false },
|
||||
assets: {
|
||||
edges: { $push: action.edges },
|
||||
pageInfo: {
|
||||
endCursor: { $set: action.pageInfo.endCursor },
|
||||
hasNextPage: { $set: action.pageInfo.hasNextPage },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
case actions.UPDATE_ASSET_STATE_SUCCESS:
|
||||
const index = state.assets.edges.findIndex(
|
||||
({ node: { id } }) => id === action.id
|
||||
);
|
||||
if (index < 0) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return update(state, {
|
||||
assets: {
|
||||
byId: {
|
||||
[action.id]: {
|
||||
closedAt: { $set: action.closedAt },
|
||||
edges: {
|
||||
[index]: {
|
||||
node: {
|
||||
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,
|
||||
|
||||
@@ -6,6 +6,10 @@ import {
|
||||
SHOW_SUSPEND_USER_DIALOG,
|
||||
HIDE_SUSPEND_USER_DIALOG,
|
||||
} from '../constants/suspendUserDialog';
|
||||
import {
|
||||
SHOW_ALWAYS_PREMOD_USER_DIALOG,
|
||||
HIDE_ALWAYS_PREMOD_USER_DIALOG,
|
||||
} from '../constants/alwaysPremodUserDialog';
|
||||
|
||||
const initialState = {
|
||||
modal: false,
|
||||
@@ -23,6 +27,11 @@ export default function config(state = initialState, action) {
|
||||
...state,
|
||||
modal: true,
|
||||
};
|
||||
case SHOW_ALWAYS_PREMOD_USER_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
modal: true,
|
||||
};
|
||||
case HIDE_BAN_USER_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
@@ -33,6 +42,11 @@ export default function config(state = initialState, action) {
|
||||
...state,
|
||||
modal: false,
|
||||
};
|
||||
case HIDE_ALWAYS_PREMOD_USER_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
modal: false,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -78,9 +78,10 @@
|
||||
}
|
||||
|
||||
.flaggedByReason {
|
||||
font-size: 1tpx;
|
||||
font-size: 14px;
|
||||
margin: 0px;
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -34,6 +34,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
.filterHeader {
|
||||
margin-top: 30px;
|
||||
font-size: 16px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.filterDetail {
|
||||
margin-top: 15px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.radioGroup {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
width: calc(100% - 300px);
|
||||
padding: 34px 14px;
|
||||
@@ -115,6 +130,12 @@ th.header:nth-child(2), th.header:nth-child(3) {
|
||||
font-size: 0.98em;
|
||||
}
|
||||
|
||||
.actionsMenuAlwaysPremod {
|
||||
background-color: #F0B50B;
|
||||
border-color: #F0B50B;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.actionsMenuSuspended {
|
||||
background-color: #F29336;
|
||||
border-color: #F29336;
|
||||
|
||||
@@ -8,7 +8,12 @@ import LoadMore from '../../../components/LoadMore';
|
||||
import PropTypes from 'prop-types';
|
||||
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
|
||||
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
import { isSuspended, isBanned } from 'coral-framework/utils/user';
|
||||
import {
|
||||
isSuspended,
|
||||
isBanned,
|
||||
isAlwaysPremod,
|
||||
} from 'coral-framework/utils/user';
|
||||
import { RadioGroup, Radio } from 'react-mdl';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
ADMIN,
|
||||
@@ -42,6 +47,8 @@ class People extends React.Component {
|
||||
return 'Banned';
|
||||
} else if (isSuspended(user)) {
|
||||
return 'Suspended';
|
||||
} else if (isAlwaysPremod(user)) {
|
||||
return 'Always premoderated';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
@@ -54,6 +61,10 @@ class People extends React.Component {
|
||||
this.props.unbanUser(input);
|
||||
};
|
||||
|
||||
removeAlwaysPremodUser = input => {
|
||||
this.props.removeAlwaysPremodUser(input);
|
||||
};
|
||||
|
||||
showBanUserDialog = input => {
|
||||
this.props.showBanUserDialog(input);
|
||||
};
|
||||
@@ -62,13 +73,18 @@ class People extends React.Component {
|
||||
this.props.showSuspendUserDialog(input);
|
||||
};
|
||||
|
||||
showAlwaysPremodUserDialog = input => {
|
||||
this.props.showAlwaysPremodUserDialog(input);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
onSearchChange,
|
||||
onFilterChange,
|
||||
users = [],
|
||||
setUserRole,
|
||||
viewUserDetail,
|
||||
loadMore,
|
||||
filters,
|
||||
} = this.props;
|
||||
|
||||
const hasResults = !!users.nodes.length;
|
||||
@@ -87,11 +103,44 @@ class People extends React.Component {
|
||||
id="commenters-search"
|
||||
type="text"
|
||||
className={styles.searchBoxInput}
|
||||
defaultValue=""
|
||||
onChange={onSearchChange}
|
||||
value={filters.search}
|
||||
onChange={onFilterChange('search')}
|
||||
placeholder={t('streams.search')}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterHeader}>
|
||||
{t('community.filter_users')}
|
||||
</div>
|
||||
<div className={styles.filterDetail}>{t('community.status')}</div>
|
||||
<RadioGroup
|
||||
name="statusFilter"
|
||||
value={filters.status}
|
||||
childContainer="div"
|
||||
onChange={onFilterChange('status')}
|
||||
className={styles.radioGroup}
|
||||
>
|
||||
<Radio value="">{t('community.all')}</Radio>
|
||||
<Radio value="active">{t('community.active')}</Radio>
|
||||
<Radio value="suspended">{t('community.suspended')}</Radio>
|
||||
<Radio value="banned">{t('community.banned')}</Radio>
|
||||
<Radio value="alwaysPremod">{t('community.always_premod')}</Radio>
|
||||
</RadioGroup>
|
||||
<div className={styles.filterDetail}>
|
||||
{t('community.filter_role')}
|
||||
</div>
|
||||
<RadioGroup
|
||||
name="roleFilter"
|
||||
value={filters.role}
|
||||
childContainer="div"
|
||||
onChange={onFilterChange('role')}
|
||||
className={styles.radioGroup}
|
||||
>
|
||||
<Radio value="">{t('community.all')}</Radio>
|
||||
<Radio value={ADMIN}>{t('community.admin')}</Radio>
|
||||
<Radio value={STAFF}>{t('community.staff')}</Radio>
|
||||
<Radio value={MODERATOR}>{t('community.moderator')}</Radio>
|
||||
<Radio value={COMMENTER}>{t('community.commenter')}</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div className={styles.mainContent}>
|
||||
{hasResults ? (
|
||||
@@ -154,6 +203,9 @@ class People extends React.Component {
|
||||
user
|
||||
),
|
||||
[styles.actionsMenuBanned]: isBanned(user),
|
||||
[styles.actionsMenuAlwaysPremod]: isAlwaysPremod(
|
||||
user
|
||||
),
|
||||
},
|
||||
'talk-admin-user-detail-actions-button'
|
||||
)}
|
||||
@@ -176,7 +228,7 @@ class People extends React.Component {
|
||||
})
|
||||
}
|
||||
>
|
||||
Suspend User
|
||||
{t('modqueue.suspend')}
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
|
||||
@@ -195,7 +247,28 @@ class People extends React.Component {
|
||||
})
|
||||
}
|
||||
>
|
||||
Ban User
|
||||
{t('modqueue.ban_user_actions')}
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
|
||||
{isAlwaysPremod(user) ? (
|
||||
<ActionsMenuItem
|
||||
onClick={() =>
|
||||
this.removeAlwaysPremodUser({ id: user.id })
|
||||
}
|
||||
>
|
||||
Remove Always Premoderate
|
||||
</ActionsMenuItem>
|
||||
) : (
|
||||
<ActionsMenuItem
|
||||
onClick={() =>
|
||||
this.showAlwaysPremodUserDialog({
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('modqueue.always_premod_user_actions')}
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
</ActionsMenu>
|
||||
@@ -254,13 +327,16 @@ class People extends React.Component {
|
||||
|
||||
People.propTypes = {
|
||||
users: PropTypes.object.isRequired,
|
||||
onSearchChange: PropTypes.func.isRequired,
|
||||
filters: PropTypes.object.isRequired,
|
||||
onFilterChange: PropTypes.func.isRequired,
|
||||
setUserRole: PropTypes.func.isRequired,
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
unbanUser: PropTypes.func.isRequired,
|
||||
unsuspendUser: PropTypes.func.isRequired,
|
||||
removeAlwaysPremodUser: PropTypes.func.isRequired,
|
||||
showSuspendUserDialog: PropTypes.func,
|
||||
showBanUserDialog: PropTypes.func,
|
||||
showAlwaysPremodUserDialog: PropTypes.func,
|
||||
loadMore: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ class RejectUsernameDialog extends Component {
|
||||
<Dialog
|
||||
className={cn(
|
||||
styles.suspendDialog,
|
||||
'talk-admin-reject-username-dialog'
|
||||
'talk-admin-reject-reported-username-dialog'
|
||||
)}
|
||||
id="rejectUsernameDialog"
|
||||
open={open}
|
||||
@@ -85,7 +85,7 @@ class RejectUsernameDialog extends Component {
|
||||
<div
|
||||
className={cn(
|
||||
styles.container,
|
||||
`talk-admin-reject-username-dialog-step-${stage}`
|
||||
`talk-admin-reject-reported-username-dialog-step-${stage}`
|
||||
)}
|
||||
>
|
||||
<div className={styles.description}>
|
||||
@@ -101,7 +101,7 @@ class RejectUsernameDialog extends Component {
|
||||
<div className={styles.emailContainer}>
|
||||
<textarea
|
||||
rows={5}
|
||||
className={cn(styles.emailInput, 'talk-admin-reject-username-dialog-suspension-message')}
|
||||
className={cn(styles.emailInput, 'talk-admin-reject-reported-username-dialog-suspension-message')}
|
||||
value={this.state.email}
|
||||
onChange={this.onEmailChange}/>
|
||||
</div>
|
||||
@@ -110,7 +110,7 @@ class RejectUsernameDialog extends Component {
|
||||
<div
|
||||
className={cn(
|
||||
styles.modalButtons,
|
||||
'talk-admin-reject-username-dialog-buttons'
|
||||
'talk-admin-reject-reported-username-dialog-buttons'
|
||||
)}
|
||||
>
|
||||
{Object.keys(stages[stage].options).map((key, i) => (
|
||||
@@ -118,7 +118,7 @@ class RejectUsernameDialog extends Component {
|
||||
key={i}
|
||||
className={cn(
|
||||
'talk-admin-username-dialog-button',
|
||||
`talk-admin-reject-username-dialog-button-${key}`
|
||||
`talk-admin-reject-reported-username-dialog-button-${key}`
|
||||
)}
|
||||
onClick={this.onActionClick(stage, i)}
|
||||
>
|
||||
|
||||
@@ -46,7 +46,11 @@ class FlaggedAccountsContainer extends Component {
|
||||
document: USERNAME_FLAGGED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameFlagged: user } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { usernameFlagged: user },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return handleFlaggedAccountsChange(prev, user, () => {
|
||||
const msg = t(
|
||||
@@ -62,7 +66,11 @@ class FlaggedAccountsContainer extends Component {
|
||||
document: USERNAME_APPROVED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameApproved: user } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { usernameApproved: user },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return handleFlaggedAccountsChange(prev, user, () => {
|
||||
const msg = t(
|
||||
@@ -78,7 +86,11 @@ class FlaggedAccountsContainer extends Component {
|
||||
document: USERNAME_REJECTED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameRejected: user } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { usernameRejected: user },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return handleFlaggedAccountsChange(prev, user, () => {
|
||||
const msg = t(
|
||||
@@ -96,7 +108,9 @@ class FlaggedAccountsContainer extends Component {
|
||||
prev,
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { usernameChanged: { previousUsername, user } },
|
||||
data: {
|
||||
usernameChanged: { previousUsername, user },
|
||||
},
|
||||
},
|
||||
}
|
||||
) => {
|
||||
@@ -297,7 +311,10 @@ const mapDispatchToProps = dispatch =>
|
||||
);
|
||||
|
||||
export default compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withApproveUsername,
|
||||
withQuery(
|
||||
gql`
|
||||
|
||||
@@ -15,7 +15,11 @@ class IndicatorContainer extends Component {
|
||||
document: USERNAME_FLAGGED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameFlagged: user } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { usernameFlagged: user },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return handleIndicatorChange(prev, user);
|
||||
},
|
||||
@@ -24,7 +28,11 @@ class IndicatorContainer extends Component {
|
||||
document: USERNAME_APPROVED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameApproved: user } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { usernameApproved: user },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return handleIndicatorChange(prev, user);
|
||||
},
|
||||
@@ -33,7 +41,11 @@ class IndicatorContainer extends Component {
|
||||
document: USERNAME_REJECTED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameRejected: user } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { usernameRejected: user },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return handleIndicatorChange(prev, user);
|
||||
},
|
||||
@@ -42,7 +54,13 @@ class IndicatorContainer extends Component {
|
||||
document: USERNAME_CHANGED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameChanged: { user } } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: {
|
||||
usernameChanged: { user },
|
||||
},
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return handleIndicatorChange(prev, user);
|
||||
},
|
||||
@@ -98,9 +116,15 @@ const fields = `
|
||||
status {
|
||||
username {
|
||||
status
|
||||
history {
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
action_summaries {
|
||||
count
|
||||
}
|
||||
`;
|
||||
|
||||
const USERNAME_FLAGGED_SUBSCRIPTION = gql`
|
||||
|
||||
@@ -8,68 +8,76 @@ import {
|
||||
withUnbanUser,
|
||||
withUnsuspendUser,
|
||||
withSetUserRole,
|
||||
withRemoveAlwaysPremodUser,
|
||||
} from 'coral-framework/graphql/mutations';
|
||||
import { showBanUserDialog } from 'actions/banUserDialog';
|
||||
import { showSuspendUserDialog } from 'actions/suspendUserDialog';
|
||||
import { showAlwaysPremodUserDialog } from 'actions/alwaysPremodUserDialog';
|
||||
import { viewUserDetail } from '../../../actions/userDetail';
|
||||
import { appendNewNodes } from 'plugin-api/beta/client/utils';
|
||||
import update from 'immutability-helper';
|
||||
import { Spinner } from 'coral-ui';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
|
||||
class PeopleContainer extends React.Component {
|
||||
class PeopleContainer extends React.PureComponent {
|
||||
timer = null;
|
||||
|
||||
state = {
|
||||
searchValue: '',
|
||||
search: '',
|
||||
role: '',
|
||||
status: '',
|
||||
};
|
||||
|
||||
onSearchChange = e => {
|
||||
const { value } = e.target;
|
||||
this.setState({ searchValue: value }, () => {
|
||||
statusToQuery = {
|
||||
active: { suspended: false, banned: false },
|
||||
suspended: { suspended: true },
|
||||
banned: { banned: true },
|
||||
alwaysPremod: { alwaysPremod: true },
|
||||
};
|
||||
|
||||
onFilterChange = filter => e =>
|
||||
this.setState({ [filter]: e.target.value }, () => {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = setTimeout(() => {
|
||||
this.search(value);
|
||||
}, 350);
|
||||
this.timer = setTimeout(this.filter, 350);
|
||||
});
|
||||
|
||||
getFilterState = () => {
|
||||
const { role, status } = this.state;
|
||||
return {
|
||||
status: this.statusToQuery[status] || null,
|
||||
role: role || null,
|
||||
};
|
||||
};
|
||||
|
||||
search = async value => {
|
||||
return this.props.data.fetchMore({
|
||||
query: SEARCH_QUERY,
|
||||
filter = () =>
|
||||
this.props.data.fetchMore({
|
||||
query: FILTER_QUERY,
|
||||
variables: {
|
||||
value,
|
||||
state: this.getFilterState(),
|
||||
value: this.state.search,
|
||||
limit: 10,
|
||||
},
|
||||
updateQuery: (previous, { fetchMoreResult: { users } }) => {
|
||||
const updated = update(previous, {
|
||||
updateQuery: (previous, { fetchMoreResult: { users } }) =>
|
||||
update(previous, {
|
||||
users: {
|
||||
nodes: {
|
||||
$set: users.nodes,
|
||||
},
|
||||
nodes: { $set: users.nodes },
|
||||
hasNextPage: { $set: users.hasNextPage },
|
||||
endCursor: { $set: users.endCursor },
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
},
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
setUserRole = async (id, role) => {
|
||||
await this.props.setUserRole(id, role);
|
||||
};
|
||||
|
||||
loadMore = () => {
|
||||
return this.props.data.fetchMore({
|
||||
loadMore = () =>
|
||||
this.props.data.fetchMore({
|
||||
query: LOAD_MORE_QUERY,
|
||||
variables: {
|
||||
value: this.state.searchValue,
|
||||
limit: 5,
|
||||
cursor: this.props.root.users.endCursor,
|
||||
state: this.getFilterState(),
|
||||
value: this.state.search,
|
||||
limit: 5,
|
||||
},
|
||||
updateQuery: (previous, { fetchMoreResult: { users } }) => {
|
||||
const updated = update(previous, {
|
||||
updateQuery: (previous, { fetchMoreResult: { users } }) =>
|
||||
update(previous, {
|
||||
users: {
|
||||
nodes: {
|
||||
$apply: nodes => appendNewNodes(nodes, users.nodes),
|
||||
@@ -77,11 +85,8 @@ class PeopleContainer extends React.Component {
|
||||
hasNextPage: { $set: users.hasNextPage },
|
||||
endCursor: { $set: users.endCursor },
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
},
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.props.data.error) {
|
||||
@@ -98,17 +103,20 @@ class PeopleContainer extends React.Component {
|
||||
|
||||
return (
|
||||
<People
|
||||
onSearchChange={this.onSearchChange}
|
||||
onFilterChange={this.onFilterChange}
|
||||
viewUserDetail={this.props.viewUserDetail}
|
||||
setUserRole={this.setUserRole}
|
||||
setUserRole={this.props.setUserRole}
|
||||
showSuspendUserDialog={this.props.showSuspendUserDialog}
|
||||
showBanUserDialog={this.props.showBanUserDialog}
|
||||
showAlwaysPremodUserDialog={this.props.showAlwaysPremodUserDialog}
|
||||
unbanUser={this.props.unbanUser}
|
||||
unsuspendUser={this.props.unsuspendUser}
|
||||
removeAlwaysPremodUser={this.props.removeAlwaysPremodUser}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
users={this.props.root.users}
|
||||
loadMore={this.loadMore}
|
||||
filters={this.state}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -118,92 +126,112 @@ PeopleContainer.propTypes = {
|
||||
setUserRole: PropTypes.func.isRequired,
|
||||
unbanUser: PropTypes.func.isRequired,
|
||||
unsuspendUser: PropTypes.func.isRequired,
|
||||
removeAlwaysPremodUser: PropTypes.func.isRequired,
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
showSuspendUserDialog: PropTypes.func,
|
||||
showBanUserDialog: PropTypes.func,
|
||||
showAlwaysPremodUserDialog: PropTypes.func,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
};
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query TalkAdmin_Community_People_LoadMoreUsers(
|
||||
$limit: Int
|
||||
$cursor: Cursor
|
||||
$value: String
|
||||
$state: UserStateInput
|
||||
) {
|
||||
users(
|
||||
query: { value: $value, limit: $limit, cursor: $cursor, state: $state }
|
||||
) {
|
||||
hasNextPage
|
||||
endCursor
|
||||
nodes {
|
||||
__typename
|
||||
id
|
||||
username
|
||||
role
|
||||
created_at
|
||||
profiles {
|
||||
id
|
||||
provider
|
||||
}
|
||||
state {
|
||||
status {
|
||||
banned {
|
||||
status
|
||||
}
|
||||
alwaysPremod {
|
||||
status
|
||||
}
|
||||
suspension {
|
||||
until
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const FILTER_QUERY = gql`
|
||||
query TalkAdmin_Community_People_FilterUsers(
|
||||
$state: UserStateInput
|
||||
$value: String
|
||||
$limit: Int
|
||||
) {
|
||||
users(query: { state: $state, value: $value, limit: $limit }) {
|
||||
hasNextPage
|
||||
endCursor
|
||||
nodes {
|
||||
__typename
|
||||
id
|
||||
username
|
||||
role
|
||||
created_at
|
||||
profiles {
|
||||
id
|
||||
provider
|
||||
}
|
||||
state {
|
||||
status {
|
||||
banned {
|
||||
status
|
||||
}
|
||||
alwaysPremod {
|
||||
status
|
||||
}
|
||||
suspension {
|
||||
until
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
viewUserDetail,
|
||||
showSuspendUserDialog,
|
||||
showBanUserDialog,
|
||||
showAlwaysPremodUserDialog,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query TalkAdmin_Community_People_LoadMoreUsers(
|
||||
$limit: Int
|
||||
$cursor: Cursor
|
||||
$value: String
|
||||
) {
|
||||
users(query: { value: $value, limit: $limit, cursor: $cursor }) {
|
||||
hasNextPage
|
||||
endCursor
|
||||
nodes {
|
||||
__typename
|
||||
id
|
||||
username
|
||||
role
|
||||
created_at
|
||||
profiles {
|
||||
id
|
||||
provider
|
||||
}
|
||||
state {
|
||||
status {
|
||||
banned {
|
||||
status
|
||||
}
|
||||
suspension {
|
||||
until
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const SEARCH_QUERY = gql`
|
||||
query TalkAdmin_Community_People_SearchUsers($value: String, $limit: Int) {
|
||||
users(query: { value: $value, limit: $limit }) {
|
||||
hasNextPage
|
||||
endCursor
|
||||
nodes {
|
||||
__typename
|
||||
id
|
||||
username
|
||||
role
|
||||
created_at
|
||||
profiles {
|
||||
id
|
||||
provider
|
||||
}
|
||||
state {
|
||||
status {
|
||||
banned {
|
||||
status
|
||||
}
|
||||
suspension {
|
||||
until
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withSetUserRole,
|
||||
withUnsuspendUser,
|
||||
withUnbanUser,
|
||||
withRemoveAlwaysPremodUser,
|
||||
withQuery(
|
||||
gql`
|
||||
query TalkAdmin_Community_People {
|
||||
@@ -225,6 +253,9 @@ export default compose(
|
||||
banned {
|
||||
status
|
||||
}
|
||||
alwaysPremod {
|
||||
status
|
||||
}
|
||||
suspension {
|
||||
until
|
||||
}
|
||||
|
||||
@@ -19,6 +19,9 @@ const mapDispatchToProps = dispatch =>
|
||||
);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withRejectUsername
|
||||
)(RejectUsernameDialog);
|
||||
|
||||
@@ -106,6 +106,23 @@ export function handleFlaggedAccountsChange(root, user, notify) {
|
||||
}
|
||||
}
|
||||
|
||||
export const wasUsernameReported = user => {
|
||||
const previousStatus =
|
||||
user.state.status.username.history[
|
||||
user.state.status.username.history.length - 2
|
||||
];
|
||||
|
||||
// Check for correct previous status
|
||||
if (!['SET', 'CHANGES'].includes(previousStatus)) {
|
||||
return false;
|
||||
}
|
||||
// Check for flags
|
||||
if (user.action_summaries.every(as => as.count === 0)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Track indicator status
|
||||
* @param {Object} root current state of the store
|
||||
@@ -119,7 +136,9 @@ export function handleIndicatorChange(root, user) {
|
||||
return incrementFlaggedUserCount(root);
|
||||
case 'APPROVED':
|
||||
case 'REJECTED':
|
||||
return decrementFlaggedUserCount(root);
|
||||
if (wasUsernameReported(user)) {
|
||||
return decrementFlaggedUserCount(root);
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ class EmbedLink extends Component {
|
||||
`;
|
||||
|
||||
return (
|
||||
<ConfigureCard title={'Embed Comment Stream'}>
|
||||
<ConfigureCard title={t('configure.embed_comment_stream')}>
|
||||
<p>{t('configure.copy_and_paste')}</p>
|
||||
<textarea
|
||||
rows={5}
|
||||
@@ -53,7 +53,7 @@ class EmbedLink extends Component {
|
||||
{t('embedlink.copy')}
|
||||
</Button>
|
||||
<div className={styles.copiedText}>
|
||||
{this.state.copied && 'Copied!'}
|
||||
{this.state.copied && t('embedlink.copied')}
|
||||
</div>
|
||||
</div>
|
||||
</ConfigureCard>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SelectField, Option } from 'react-mdl-selectfield';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import styles from './StreamSettings.css';
|
||||
import { Textfield } from 'react-mdl';
|
||||
import { Icon, TextArea } from 'coral-ui';
|
||||
import { Icon } from 'coral-ui';
|
||||
import PropTypes from 'prop-types';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import MarkdownEditor from 'coral-framework/components/MarkdownEditor';
|
||||
@@ -66,8 +66,8 @@ class StreamSettings extends React.Component {
|
||||
this.props.updatePending({ updater });
|
||||
};
|
||||
|
||||
updateClosedMessage = event => {
|
||||
const updater = { closedMessage: { $set: event.target.value } };
|
||||
updateClosedMessage = value => {
|
||||
const updater = { closedMessage: { $set: value } };
|
||||
this.props.updatePending({ updater });
|
||||
};
|
||||
|
||||
@@ -155,9 +155,19 @@ class StreamSettings extends React.Component {
|
||||
<ConfigureCard
|
||||
checked={settings.infoBoxEnable}
|
||||
onCheckbox={this.updateInfoBoxEnable}
|
||||
title={t('configure.include_comment_stream')}
|
||||
title={t('configure.code_of_conduct_summary')}
|
||||
>
|
||||
<p>{t('configure.include_comment_stream_desc')}</p>
|
||||
<p>
|
||||
{t('configure.code_of_conduct_summary_desc')}
|
||||
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://guides.coralproject.net/create-a-code-of-conduct/"
|
||||
>
|
||||
Code of Conduct Guide.
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
className={cn(
|
||||
styles.configSettingInfoBox,
|
||||
@@ -178,7 +188,7 @@ class StreamSettings extends React.Component {
|
||||
>
|
||||
<p>{t('configure.closed_comments_desc')}</p>
|
||||
<div>
|
||||
<TextArea
|
||||
<MarkdownEditor
|
||||
className={styles.descriptionBox}
|
||||
onChange={this.updateClosedMessage}
|
||||
value={settings.closedMessage}
|
||||
@@ -238,12 +248,14 @@ class StreamSettings extends React.Component {
|
||||
onChange={this.updateClosedTimeout}
|
||||
value={getTimeoutAmount(settings.closedTimeout)}
|
||||
label={t('configure.closed_comments_label')}
|
||||
disabled={!settings.autoCloseStream}
|
||||
/>
|
||||
<div className={styles.configTimeoutSelect}>
|
||||
<SelectField
|
||||
label="comments closed time window"
|
||||
value={getTimeoutMeasure(settings.closedTimeout)}
|
||||
onChange={this.updateClosedTimeoutMeasure}
|
||||
readOnly={!settings.autoCloseStream}
|
||||
>
|
||||
<Option value={'hours'}>{t('configure.hours')}</Option>
|
||||
<Option value={'days'}>{t('configure.days')}</Option>
|
||||
|
||||
@@ -14,6 +14,11 @@ class TechSettings extends React.Component {
|
||||
this.props.updatePending({ updater });
|
||||
};
|
||||
|
||||
updateCustomAdminCssUrl = event => {
|
||||
const updater = { customAdminCssUrl: { $set: event.target.value } };
|
||||
this.props.updatePending({ updater });
|
||||
};
|
||||
|
||||
updateDomainlist = (listName, list) => {
|
||||
this.props.updatePending({
|
||||
updater: {
|
||||
@@ -50,6 +55,14 @@ class TechSettings extends React.Component {
|
||||
onChange={this.updateCustomCssUrl}
|
||||
/>
|
||||
</ConfigureCard>
|
||||
<ConfigureCard title={t('configure.custom_admin_css_url')}>
|
||||
<p>{t('configure.custom_admin_css_url_desc')}</p>
|
||||
<input
|
||||
className={styles.customCSSInput}
|
||||
value={settings.customAdminCssUrl}
|
||||
onChange={this.updateCustomAdminCssUrl}
|
||||
/>
|
||||
</ConfigureCard>
|
||||
<Slot fill="adminTechSettings" passthrough={slotPassthrough} />
|
||||
</ConfigurePage>
|
||||
);
|
||||
|
||||
@@ -6,19 +6,23 @@ import ConfigureCard from 'coral-framework/components/ConfigureCard';
|
||||
|
||||
const Wordlist = ({ suspectWords, bannedWords, onChangeWordlist }) => (
|
||||
<div>
|
||||
<ConfigureCard title={t('configure.banned_words_title')}>
|
||||
<ConfigureCard collapsible title={t('configure.banned_words_title')}>
|
||||
<p>{t('configure.banned_word_text')}</p>
|
||||
<TagsInput
|
||||
value={bannedWords}
|
||||
inputProps={{ placeholder: 'word or phrase' }}
|
||||
inputProps={{
|
||||
placeholder: t('configure.suspect_or_forbidden_words_placeholder'),
|
||||
}}
|
||||
onChange={tags => onChangeWordlist('banned', tags)}
|
||||
/>
|
||||
</ConfigureCard>
|
||||
<ConfigureCard title={t('configure.suspect_word_title')}>
|
||||
<ConfigureCard collapsible title={t('configure.suspect_word_title')}>
|
||||
<p>{t('configure.suspect_word_text')}</p>
|
||||
<TagsInput
|
||||
value={suspectWords}
|
||||
inputProps={{ placeholder: 'word or phrase' }}
|
||||
inputProps={{
|
||||
placeholder: t('configure.suspect_or_forbidden_words_placeholder'),
|
||||
}}
|
||||
onChange={tags => onChangeWordlist('suspect', tags)}
|
||||
/>
|
||||
</ConfigureCard>
|
||||
|
||||
@@ -174,7 +174,10 @@ const mapDispatchToProps = dispatch =>
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withUpdateSettings,
|
||||
withConfigureQuery,
|
||||
withMergedSettings('root.settings', 'pending', 'mergedSettings')
|
||||
|
||||
@@ -42,7 +42,10 @@ export default compose(
|
||||
}
|
||||
`,
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
|
||||
slotPassthrough: {
|
||||
root,
|
||||
|
||||
@@ -37,7 +37,10 @@ export default compose(
|
||||
}
|
||||
`,
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
|
||||
slotPassthrough: {
|
||||
root,
|
||||
|
||||
@@ -45,7 +45,10 @@ export default compose(
|
||||
}
|
||||
`,
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
|
||||
slotPassthrough: {
|
||||
root,
|
||||
|
||||
@@ -32,6 +32,7 @@ export default compose(
|
||||
settings: gql`
|
||||
fragment TalkAdmin_TechSettings_settings on Settings {
|
||||
customCssUrl
|
||||
customAdminCssUrl
|
||||
domains {
|
||||
whitelist
|
||||
}
|
||||
@@ -39,7 +40,10 @@ export default compose(
|
||||
}
|
||||
`,
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
|
||||
slotPassthrough: {
|
||||
root,
|
||||
|
||||
@@ -83,4 +83,7 @@ const mapDispatchToProps = dispatch =>
|
||||
dispatch
|
||||
);
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(InstallContainer);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(InstallContainer);
|
||||
|
||||
@@ -61,13 +61,20 @@
|
||||
line-height: 1.5;
|
||||
font-weight: 300;
|
||||
margin-bottom: 8px;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-top: 0px;
|
||||
flex: 1;
|
||||
color: black;
|
||||
max-width: 500px;
|
||||
/*
|
||||
IE11 fix – Next line was supposed to be:
|
||||
max-width: 500px;
|
||||
*/
|
||||
padding-right: 20px;
|
||||
/** IE11 fix end **/
|
||||
font-weight: 300;
|
||||
font-size: 16px;
|
||||
overflow-wrap: break-word;
|
||||
@@ -96,7 +103,7 @@
|
||||
color: #063b9a;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
letter-spacing: .5px;
|
||||
letter-spacing: 0.5px;
|
||||
margin-left: 10px;
|
||||
font-size: 13px;
|
||||
margin-left: 5px;
|
||||
@@ -104,14 +111,14 @@
|
||||
border-bottom: solid 1px;
|
||||
line-height: 16px;
|
||||
&:hover {
|
||||
opacity: .9;
|
||||
opacity: 0.9;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
color: #393B44;
|
||||
color: #393b44;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
@@ -120,12 +127,12 @@
|
||||
margin-left: -5px;
|
||||
transition: background-color 200ms ease;
|
||||
&:hover {
|
||||
background-color: #E0E0E0;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
.external {
|
||||
font-size: .7em;
|
||||
font-size: 0.7em;
|
||||
text-decoration: none;
|
||||
color: #063b9a;
|
||||
cursor: pointer;
|
||||
@@ -133,7 +140,7 @@
|
||||
white-space: nowrap;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
opacity: .9;
|
||||
opacity: 0.9;
|
||||
}
|
||||
i {
|
||||
font-size: 12px;
|
||||
@@ -188,4 +195,26 @@
|
||||
|
||||
.commentContentFooter {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.bodyHistoryToggle {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
line-height: 1px;
|
||||
font-weight: 300;
|
||||
text-decoration: underline;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.editedCommentBody {
|
||||
padding: 0 5px 5px 5px;
|
||||
}
|
||||
|
||||
.editedComment {
|
||||
margin-top: 6px;
|
||||
font-weight: 300;
|
||||
font-size: 16px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
@@ -14,12 +14,16 @@ import cn from 'classnames';
|
||||
import ApproveButton from 'coral-admin/src/components/ApproveButton';
|
||||
import RejectButton from 'coral-admin/src/components/RejectButton';
|
||||
import CommentDeletedTombstone from '../../../components/CommentDeletedTombstone';
|
||||
import TimeAgo from 'coral-framework/components/TimeAgo';
|
||||
|
||||
import t, { timeago } from 'coral-framework/services/i18n';
|
||||
import { buildCommentURL } from 'coral-framework/utils/url';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class Comment extends React.Component {
|
||||
ref = null;
|
||||
|
||||
state = { showingEditHistory: false };
|
||||
|
||||
handleRef = ref => (this.ref = ref);
|
||||
|
||||
handleFocusOrClick = () => {
|
||||
@@ -43,6 +47,30 @@ class Comment extends React.Component {
|
||||
? null
|
||||
: this.props.rejectComment({ commentId: this.props.comment.id });
|
||||
|
||||
getBodyHistory = () => {
|
||||
const bodyHistory = [];
|
||||
const comment = this.props.comment;
|
||||
for (let i = 0; i < comment.body_history.length - 1; i++) {
|
||||
bodyHistory.push(
|
||||
<div key={i} className={styles.editedComment}>
|
||||
<div>
|
||||
<TimeAgo className={styles.created} datetime={comment.created_at} />
|
||||
</div>
|
||||
<div className={styles.editedCommentBody}>
|
||||
{comment.body_history[i].body}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return bodyHistory;
|
||||
};
|
||||
|
||||
toggleEditHistory = () => {
|
||||
this.setState({ showingEditHistory: !this.state.showingEditHistory });
|
||||
this.props.clearHeightCache && this.props.clearHeightCache();
|
||||
};
|
||||
|
||||
componentDidUpdate(prev) {
|
||||
if (!prev.selected && this.props.selected) {
|
||||
this.ref.focus();
|
||||
@@ -126,14 +154,23 @@ class Comment extends React.Component {
|
||||
{comment.user.username}
|
||||
</span>
|
||||
|
||||
<span className={styles.created}>
|
||||
{timeago(comment.created_at)}
|
||||
</span>
|
||||
<TimeAgo
|
||||
className={styles.created}
|
||||
datetime={comment.created_at}
|
||||
/>
|
||||
{comment.editing && comment.editing.edited ? (
|
||||
<span>
|
||||
<span className={styles.editedMarker}>
|
||||
({t('comment.edited')})
|
||||
</span>
|
||||
<span
|
||||
className={styles.bodyHistoryToggle}
|
||||
onClick={this.toggleEditHistory}
|
||||
>
|
||||
{this.state.showingEditHistory
|
||||
? t('comment.hide_edit_history')
|
||||
: t('comment.show_edit_history')}
|
||||
</span>
|
||||
</span>
|
||||
) : null}
|
||||
<div className={styles.adminCommentInfoBar}>
|
||||
@@ -168,8 +205,9 @@ class Comment extends React.Component {
|
||||
<div className={styles.commentContentFooter}>
|
||||
<a
|
||||
className={styles.external}
|
||||
href={`${comment.asset.url}?commentId=${comment.id}`}
|
||||
href={buildCommentURL(comment.asset.url, comment.id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon name="open_in_new" /> {t('comment.view_context')}
|
||||
</a>
|
||||
@@ -179,8 +217,7 @@ class Comment extends React.Component {
|
||||
<div className={styles.sideActions}>
|
||||
<IfHasLink text={comment.body}>
|
||||
<span className={styles.hasLinks}>
|
||||
{/* TODO: translate string */}
|
||||
<Icon name="error_outline" /> Contains Link
|
||||
<Icon name="error_outline" /> {t('common.contains_link')}
|
||||
</span>
|
||||
</IfHasLink>
|
||||
<div className={`actions ${styles.actions}`}>
|
||||
@@ -198,6 +235,14 @@ class Comment extends React.Component {
|
||||
</div>
|
||||
</CommentAnimatedEdit>
|
||||
</div>
|
||||
|
||||
{this.state.showingEditHistory ? (
|
||||
<div className={styles.container}>
|
||||
{t('comment.edit_history')}
|
||||
{this.getBodyHistory()}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<CommentDetails
|
||||
root={root}
|
||||
comment={comment}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Story.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const formatDate = date => {
|
||||
const d = new Date(date);
|
||||
@@ -14,7 +15,9 @@ const Story = ({ author, title, createdAt, open, id, goToStory }) => {
|
||||
<div className={styles.meta}>
|
||||
<span className={styles.author}>By {author}</span>
|
||||
<span className={styles.createdAt}>{formatDate(createdAt)}</span>
|
||||
<span className={styles.status}>{open ? 'Open' : 'Closed'}</span>
|
||||
<span className={styles.status}>
|
||||
{open ? t('streams.open') : t('streams.closed')}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -3,9 +3,13 @@ import PropTypes from 'prop-types';
|
||||
import styles from './StorySearch.css';
|
||||
import { Button, Spinner, Icon } from 'coral-ui';
|
||||
import Story from './Story';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const StorySearch = props => {
|
||||
const { root: { assets }, data: { loading } } = props;
|
||||
const {
|
||||
root: { assets },
|
||||
data: { loading },
|
||||
} = props;
|
||||
|
||||
if (!props.moderation.storySearchVisible) {
|
||||
return null;
|
||||
@@ -33,14 +37,14 @@ const StorySearch = props => {
|
||||
onClick={props.search}
|
||||
raised
|
||||
>
|
||||
Search
|
||||
{t('streams.search')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.results}>
|
||||
{props.assetId && (
|
||||
<div className={styles.cta}>
|
||||
<a onClick={props.goToModerateAll}>
|
||||
Moderate comments on All Stories
|
||||
{t('moderate_all_streams')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
@@ -48,13 +52,15 @@ const StorySearch = props => {
|
||||
{props.moderation.storySearchString ? (
|
||||
<div className={styles.searchResults}>
|
||||
<Icon name="search" />
|
||||
<span className={styles.headlineRecent}>Search Results</span>
|
||||
<span className={styles.headlineRecent}>
|
||||
{t('streams.search_results')}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.searchResults}>
|
||||
<Icon name="access_time" />
|
||||
<span className={styles.headlineRecent}>
|
||||
Most Recent Stories
|
||||
{t('streams.most_recent_stories')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -82,7 +88,9 @@ const StorySearch = props => {
|
||||
)}
|
||||
|
||||
{assets.nodes.length === 0 && (
|
||||
<div className={styles.noResults}>No results</div>
|
||||
<div className={styles.noResults}>
|
||||
{t('streams.search_results')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -52,6 +52,10 @@ export default withFragments({
|
||||
status_history {
|
||||
type
|
||||
}
|
||||
body_history {
|
||||
body
|
||||
created_at
|
||||
}
|
||||
hasParent
|
||||
${getSlotFragmentSpreads(slots, 'comment')}
|
||||
...${getDefinitionName(CommentLabels.fragments.comment)}
|
||||
|
||||
@@ -22,7 +22,11 @@ class IndicatorContainer extends Component {
|
||||
document: COMMENT_ADDED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentAdded: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentAdded: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
@@ -31,7 +35,11 @@ class IndicatorContainer extends Component {
|
||||
document: COMMENT_FLAGGED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentFlagged: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentFlagged: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
@@ -40,7 +48,11 @@ class IndicatorContainer extends Component {
|
||||
document: COMMENT_EDITED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentEdited: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentEdited: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
@@ -49,7 +61,11 @@ class IndicatorContainer extends Component {
|
||||
document: COMMENT_ACCEPTED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentAccepted: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentAccepted: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
@@ -58,7 +74,11 @@ class IndicatorContainer extends Component {
|
||||
document: COMMENT_REJECTED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentRejected: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentRejected: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
@@ -67,7 +87,11 @@ class IndicatorContainer extends Component {
|
||||
document: COMMENT_RESET_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentReset: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentReset: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
|
||||
@@ -71,7 +71,9 @@ class ModerationContainer extends Component {
|
||||
};
|
||||
|
||||
get activeTab() {
|
||||
const { root: { asset, settings } } = this.props;
|
||||
const {
|
||||
root: { asset, settings },
|
||||
} = this.props;
|
||||
const id = getAssetId(this.props);
|
||||
const tab = getTab(this.props);
|
||||
|
||||
@@ -94,7 +96,11 @@ class ModerationContainer extends Component {
|
||||
variables,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentAdded: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentAdded: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
@@ -104,7 +110,11 @@ class ModerationContainer extends Component {
|
||||
variables,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentAccepted: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentAccepted: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
const user =
|
||||
comment.status_history[comment.status_history.length - 1]
|
||||
@@ -125,7 +135,11 @@ class ModerationContainer extends Component {
|
||||
variables,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentRejected: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentRejected: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
const user =
|
||||
comment.status_history[comment.status_history.length - 1]
|
||||
@@ -146,7 +160,11 @@ class ModerationContainer extends Component {
|
||||
variables,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentReset: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentReset: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
const user =
|
||||
comment.status_history[comment.status_history.length - 1]
|
||||
@@ -167,7 +185,11 @@ class ModerationContainer extends Component {
|
||||
variables,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentEdited: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentEdited: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
@@ -177,7 +199,11 @@ class ModerationContainer extends Component {
|
||||
variables,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentFlagged: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentFlagged: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
@@ -289,7 +315,11 @@ class ModerationContainer extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { root, root: { asset, settings }, data } = this.props;
|
||||
const {
|
||||
root,
|
||||
root: { asset, settings },
|
||||
data,
|
||||
} = this.props;
|
||||
const assetId = getAssetId(this.props);
|
||||
|
||||
if (assetId) {
|
||||
@@ -546,7 +576,10 @@ const mapDispatchToProps = dispatch => ({
|
||||
|
||||
export default compose(
|
||||
withQueueConfig(baseQueueConfig),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withSetCommentStatus,
|
||||
withModQueueQuery
|
||||
)(ModerationContainer);
|
||||
|
||||
@@ -112,4 +112,7 @@ export const withAssetSearchQuery = withQuery(
|
||||
}
|
||||
);
|
||||
|
||||
export default compose(withRouter, withAssetSearchQuery)(StorySearchContainer);
|
||||
export default compose(
|
||||
withRouter,
|
||||
withAssetSearchQuery
|
||||
)(StorySearchContainer);
|
||||
|
||||
@@ -77,7 +77,6 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -95,3 +94,11 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.loadMore {
|
||||
margin: 20px auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loadMoreSpinner {
|
||||
margin: 20px auto 50px;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@ import React, { Component } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Link } from 'react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Dropdown, Option, Paginate, Icon } from 'coral-ui';
|
||||
import { DataTable, TableHeader, RadioGroup, Radio } from 'react-mdl';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import styles from './Stories.css';
|
||||
import { Dropdown, Option, Icon, Spinner } from 'coral-ui';
|
||||
import EmptyCard from 'coral-admin/src/components/EmptyCard';
|
||||
import LoadMore from 'coral-admin/src/components/LoadMore';
|
||||
|
||||
import styles from './Stories.css';
|
||||
|
||||
class Stories extends Component {
|
||||
renderDate = date => {
|
||||
@@ -39,10 +42,11 @@ class Stories extends Component {
|
||||
filter,
|
||||
onSearchChange,
|
||||
onSettingChange,
|
||||
onPageChange,
|
||||
asc,
|
||||
onLoadMore,
|
||||
loading,
|
||||
loadingMore,
|
||||
} = this.props;
|
||||
const rows = assets.ids.map(id => assets.byId[id]);
|
||||
const rows = assets.edges.map(({ node }) => node);
|
||||
|
||||
return (
|
||||
<div className={cn('talk-admin-stories', styles.container)}>
|
||||
@@ -74,60 +78,58 @@ class Stories extends Component {
|
||||
<Radio value="open">{t('streams.open')}</Radio>
|
||||
<Radio value="closed">{t('streams.closed')}</Radio>
|
||||
</RadioGroup>
|
||||
<div className={styles.optionHeader}>{t('streams.sort_by')}</div>
|
||||
<RadioGroup
|
||||
name="sortBy"
|
||||
value={asc}
|
||||
childContainer="div"
|
||||
onChange={onSettingChange('asc')}
|
||||
className={styles.radioGroup}
|
||||
>
|
||||
<Radio value="false">{t('streams.newest')}</Radio>
|
||||
<Radio value="true">{t('streams.oldest')}</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
{rows.length ? (
|
||||
<div className={styles.mainContent}>
|
||||
<DataTable className={styles.streamsTable} rows={rows}>
|
||||
<TableHeader name="title" cellFormatter={this.renderTitle}>
|
||||
{t('streams.article')}
|
||||
</TableHeader>
|
||||
<TableHeader
|
||||
name="publication_date"
|
||||
cellFormatter={this.renderDate}
|
||||
>
|
||||
{t('streams.pubdate')}
|
||||
</TableHeader>
|
||||
<TableHeader
|
||||
name="closedAt"
|
||||
cellFormatter={this.renderStatus}
|
||||
className={styles.status}
|
||||
>
|
||||
{t('streams.status')}
|
||||
</TableHeader>
|
||||
</DataTable>
|
||||
<Paginate
|
||||
pageCount={assets.totalPages}
|
||||
page={assets.page - 1}
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyCard>{t('streams.empty_result')}</EmptyCard>
|
||||
)}
|
||||
<div className={styles.mainContent}>
|
||||
{loading ? (
|
||||
<Spinner />
|
||||
) : rows.length ? (
|
||||
<div>
|
||||
<DataTable className={styles.streamsTable} rows={rows}>
|
||||
<TableHeader name="title" cellFormatter={this.renderTitle}>
|
||||
{t('streams.article')}
|
||||
</TableHeader>
|
||||
<TableHeader
|
||||
name="publication_date"
|
||||
cellFormatter={this.renderDate}
|
||||
>
|
||||
{t('streams.pubdate')}
|
||||
</TableHeader>
|
||||
<TableHeader
|
||||
name="closedAt"
|
||||
cellFormatter={this.renderStatus}
|
||||
className={styles.status}
|
||||
>
|
||||
{t('streams.status')}
|
||||
</TableHeader>
|
||||
</DataTable>
|
||||
{loadingMore ? (
|
||||
<Spinner className={styles.loadMoreSpinner} />
|
||||
) : (
|
||||
<LoadMore
|
||||
showLoadMore={assets.pageInfo.hasNextPage}
|
||||
loadMore={onLoadMore}
|
||||
className={styles.loadMore}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyCard>{t('streams.empty_result')}</EmptyCard>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Stories.propTypes = {
|
||||
loading: PropTypes.bool,
|
||||
loadingMore: PropTypes.bool,
|
||||
assets: PropTypes.object,
|
||||
searchValue: PropTypes.string,
|
||||
asc: PropTypes.string,
|
||||
filter: PropTypes.string,
|
||||
onLoadMore: PropTypes.func.isRequired,
|
||||
onStatusChange: PropTypes.func.isRequired,
|
||||
onSearchChange: PropTypes.func.isRequired,
|
||||
onPageChange: PropTypes.func.isRequired,
|
||||
onSettingChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ import { bindActionCreators } from 'redux';
|
||||
import {
|
||||
fetchAssets,
|
||||
updateAssetState,
|
||||
setPage,
|
||||
setSearchValue,
|
||||
setCriteria,
|
||||
loadMoreAssets,
|
||||
} from 'coral-admin/src/actions/stories';
|
||||
import Stories from '../components/Stories';
|
||||
|
||||
@@ -35,13 +35,11 @@ class StoriesContainer extends Component {
|
||||
};
|
||||
|
||||
fetchAssets = query => {
|
||||
const { searchValue, asc, filter, limit } = this.props;
|
||||
const { searchValue: value, filter } = this.props;
|
||||
|
||||
this.props.fetchAssets({
|
||||
value: searchValue,
|
||||
asc,
|
||||
value,
|
||||
filter,
|
||||
limit,
|
||||
...query,
|
||||
});
|
||||
};
|
||||
@@ -50,31 +48,40 @@ class StoriesContainer extends Component {
|
||||
const { updateAssetState } = this.props;
|
||||
|
||||
try {
|
||||
updateAssetState(id, closeStream ? Date.now() : null);
|
||||
this.fetchAssets();
|
||||
await updateAssetState(id, closeStream ? Date.now() : null);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
onPageChange = ({ selected }) => {
|
||||
const page = selected + 1;
|
||||
this.props.setPage(page);
|
||||
this.fetchAssets({ page });
|
||||
onLoadMore = async () => {
|
||||
const {
|
||||
searchValue: value,
|
||||
filter,
|
||||
assets: {
|
||||
pageInfo: { endCursor: cursor },
|
||||
},
|
||||
loadMoreAssets,
|
||||
} = this.props;
|
||||
try {
|
||||
await loadMoreAssets({ cursor, value, filter });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Stories
|
||||
loading={this.props.loading}
|
||||
loadingMore={this.props.loadingMore}
|
||||
assets={this.props.assets}
|
||||
searchValue={this.props.searchValue}
|
||||
asc={this.props.asc}
|
||||
filter={this.props.filter}
|
||||
limit={this.props.limit}
|
||||
onPageChange={this.onPageChange}
|
||||
onStatusChange={this.onStatusChange}
|
||||
onSettingChange={this.onSettingChange}
|
||||
onSearchChange={this.onSearchChange}
|
||||
onLoadMore={this.onLoadMore}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -82,35 +89,38 @@ class StoriesContainer extends Component {
|
||||
|
||||
const mapStateToProps = ({ stories }) => ({
|
||||
assets: stories.assets,
|
||||
loading: stories.loading,
|
||||
loadingMore: stories.loadingMore,
|
||||
searchValue: stories.searchValue,
|
||||
asc: stories.criteria.asc,
|
||||
filter: stories.criteria.filter,
|
||||
limit: stories.criteria.limit,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
setPage,
|
||||
setCriteria,
|
||||
setSearchValue,
|
||||
fetchAssets,
|
||||
updateAssetState,
|
||||
loadMoreAssets,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
StoriesContainer.propTypes = {
|
||||
loading: PropTypes.bool,
|
||||
loadingMore: PropTypes.bool,
|
||||
assets: PropTypes.object,
|
||||
searchValue: PropTypes.string,
|
||||
asc: PropTypes.string,
|
||||
filter: PropTypes.string,
|
||||
limit: PropTypes.number,
|
||||
setPage: PropTypes.func.isRequired,
|
||||
setCriteria: PropTypes.func.isRequired,
|
||||
setSearchValue: PropTypes.func.isRequired,
|
||||
fetchAssets: PropTypes.func.isRequired,
|
||||
loadMoreAssets: PropTypes.func.isRequired,
|
||||
updateAssetState: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(StoriesContainer);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(StoriesContainer);
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import LinkifyIt from 'linkify-it';
|
||||
import tlds from 'tlds';
|
||||
|
||||
export function createLinkify() {
|
||||
const linkify = new LinkifyIt();
|
||||
linkify.tlds(tlds);
|
||||
return linkify;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { createPostMessage } from 'coral-framework/services/postMessage';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const staticConfig = getStaticConfiguration();
|
||||
const { STATIC_ORIGIN: origin } = staticConfig;
|
||||
const { BASE_ORIGIN: origin } = staticConfig;
|
||||
const postMessage = createPostMessage(origin);
|
||||
|
||||
// Get the auth element and parse it as JSON by decoding it.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as actions from '../constants/stream';
|
||||
import { buildUrl } from 'coral-framework/utils/url';
|
||||
import queryString from 'query-string';
|
||||
import queryString from 'querystringify';
|
||||
|
||||
export const setActiveReplyBox = id => ({
|
||||
type: actions.SET_ACTIVE_REPLY_BOX,
|
||||
@@ -14,12 +14,13 @@ export const setSort = ({ sortOrder, sortBy }) => ({
|
||||
});
|
||||
|
||||
export const viewAllComments = () => (dispatch, _, { pym }) => {
|
||||
const search = queryString.stringify({
|
||||
...queryString.parse(location.search),
|
||||
comment_id: undefined,
|
||||
});
|
||||
const query = queryString.parse(location.search);
|
||||
|
||||
// remove the comment_id url param
|
||||
delete query.comment_id;
|
||||
|
||||
const search = queryString.stringify(query);
|
||||
|
||||
const url = buildUrl({ ...location, search });
|
||||
|
||||
try {
|
||||
|
||||
@@ -5,6 +5,9 @@ const prefix = 'TALK_EMBED_STREAM';
|
||||
export const ADDTL_COMMENTS_ON_LOAD_MORE = parseInt(
|
||||
defaultTo(process.env.TALK_ADDTL_COMMENTS_ON_LOAD_MORE, '10')
|
||||
);
|
||||
export const ADDTL_REPLIES_ON_LOAD_MORE = parseInt(
|
||||
defaultTo(process.env.TALK_ADDTL_REPLIES_ON_LOAD_MORE, '999999')
|
||||
);
|
||||
export const ASSET_COMMENTS_LOAD_DEPTH = parseInt(
|
||||
defaultTo(process.env.TALK_ASSET_COMMENTS_LOAD_DEPTH, '10')
|
||||
);
|
||||
|
||||
@@ -41,7 +41,13 @@ class EmbedContainer extends React.Component {
|
||||
document: USER_BANNED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
_,
|
||||
{ subscriptionData: { data: { userBanned: { state } } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: {
|
||||
userBanned: { state },
|
||||
},
|
||||
},
|
||||
}
|
||||
) => {
|
||||
notify('info', t('your_account_has_been_banned'));
|
||||
props.updateStatus(state.status);
|
||||
@@ -51,7 +57,13 @@ class EmbedContainer extends React.Component {
|
||||
document: USER_SUSPENDED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
_,
|
||||
{ subscriptionData: { data: { userSuspended: { state } } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: {
|
||||
userSuspended: { state },
|
||||
},
|
||||
},
|
||||
}
|
||||
) => {
|
||||
notify('info', t('your_account_has_been_suspended'));
|
||||
props.updateStatus(state.status);
|
||||
@@ -61,9 +73,15 @@ class EmbedContainer extends React.Component {
|
||||
document: USERNAME_REJECTED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
_,
|
||||
{ subscriptionData: { data: { usernameRejected: { state } } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: {
|
||||
usernameRejected: { state },
|
||||
},
|
||||
},
|
||||
}
|
||||
) => {
|
||||
notify('info', t('your_username_has_been_rejected'));
|
||||
notify('info', t('your_username_has_been_rejected_not_in_line'));
|
||||
props.updateStatus(state.status);
|
||||
},
|
||||
},
|
||||
@@ -157,6 +175,9 @@ const USER_BANNED_SUBSCRIPTION = gql`
|
||||
banned {
|
||||
status
|
||||
}
|
||||
alwaysPremod {
|
||||
status
|
||||
}
|
||||
suspension {
|
||||
until
|
||||
}
|
||||
@@ -178,6 +199,9 @@ const USER_SUSPENDED_SUBSCRIPTION = gql`
|
||||
banned {
|
||||
status
|
||||
}
|
||||
alwaysPremod {
|
||||
status
|
||||
}
|
||||
suspension {
|
||||
until
|
||||
}
|
||||
@@ -199,6 +223,9 @@ const USERNAME_REJECTED_SUBSCRIPTION = gql`
|
||||
banned {
|
||||
status
|
||||
}
|
||||
alwaysPremod {
|
||||
status
|
||||
}
|
||||
suspension {
|
||||
until
|
||||
}
|
||||
@@ -235,6 +262,9 @@ const EMBED_QUERY = gql`
|
||||
banned {
|
||||
status
|
||||
}
|
||||
alwaysPremod {
|
||||
status
|
||||
}
|
||||
suspension {
|
||||
until
|
||||
}
|
||||
@@ -324,7 +354,10 @@ const mapDispatchToProps = dispatch =>
|
||||
|
||||
export default compose(
|
||||
withPopupAuthHandler,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
branch(props => !props.checkedInitialLogin, renderComponent(Spinner)),
|
||||
withEmbedQuery
|
||||
)(EmbedContainer);
|
||||
|
||||
@@ -126,7 +126,9 @@ export default {
|
||||
},
|
||||
mutations: {
|
||||
PostComment: ({
|
||||
variables: { input: { asset_id, body, parent_id, tags = [] } },
|
||||
variables: {
|
||||
input: { asset_id, body, parent_id, tags = [] },
|
||||
},
|
||||
state: { auth },
|
||||
}) => ({
|
||||
optimisticResponse: {
|
||||
@@ -193,7 +195,13 @@ export default {
|
||||
updateQueries: {
|
||||
CoralEmbedStream_Embed: (
|
||||
prev,
|
||||
{ mutationResult: { data: { createComment: { comment } } } }
|
||||
{
|
||||
mutationResult: {
|
||||
data: {
|
||||
createComment: { comment },
|
||||
},
|
||||
},
|
||||
}
|
||||
) => {
|
||||
if (
|
||||
(![ADMIN, MODERATOR].includes(prev.me.role) &&
|
||||
@@ -208,7 +216,13 @@ export default {
|
||||
},
|
||||
CoralEmbedStream_Profile: (
|
||||
prev,
|
||||
{ mutationResult: { data: { createComment: { comment } } } }
|
||||
{
|
||||
mutationResult: {
|
||||
data: {
|
||||
createComment: { comment },
|
||||
},
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return update(prev, {
|
||||
me: {
|
||||
@@ -224,7 +238,13 @@ export default {
|
||||
updateQueries: {
|
||||
CoralEmbedStream_Embed: (
|
||||
prev,
|
||||
{ mutationResult: { data: { editComment: { comment } } } }
|
||||
{
|
||||
mutationResult: {
|
||||
data: {
|
||||
editComment: { comment },
|
||||
},
|
||||
},
|
||||
}
|
||||
) => {
|
||||
if (
|
||||
!['PREMOD', 'REJECTED', 'SYSTEM_WITHHELD'].includes(comment.status)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import QuestionBox from '../../../components/QuestionBox';
|
||||
import DefaultQuestionBoxIcon from '../../../components/DefaultQuestionBoxIcon';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import styles from './QuestionBoxBuilder.css';
|
||||
import { Icon } from 'coral-ui';
|
||||
@@ -12,6 +13,7 @@ const icons = [{ default: DefaultIcon }, 'forum', 'build', 'format_quote'];
|
||||
class QuestionBoxBuilder extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
questionBoxIcon,
|
||||
questionBoxContent,
|
||||
onContentChange,
|
||||
@@ -20,7 +22,7 @@ class QuestionBoxBuilder extends React.Component {
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<h4>Include an Icon</h4>
|
||||
<h4>{title}</h4>
|
||||
|
||||
<ul className={styles.iconList}>
|
||||
{icons.map(item => {
|
||||
@@ -53,4 +55,12 @@ class QuestionBoxBuilder extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
QuestionBoxBuilder.propTypes = {
|
||||
title: PropTypes.string,
|
||||
questionBoxIcon: PropTypes.string,
|
||||
questionBoxContent: PropTypes.string,
|
||||
onContentChange: PropTypes.func,
|
||||
onIconChange: PropTypes.func,
|
||||
};
|
||||
|
||||
export default QuestionBoxBuilder;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user