mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abdb4cbe3e | ||
|
|
20380cd62f | ||
|
|
4c8894d6e9 | ||
|
|
dd5d06aa95 | ||
|
|
525da57419 | ||
|
|
4913eda974 | ||
|
|
cee247f065 | ||
|
|
961111c3f7 | ||
|
|
2d96163b76 | ||
|
|
c3e24a0451 | ||
|
|
42954fb84a | ||
|
|
5f7c4f82e7 | ||
|
|
b482976c58 | ||
|
|
6e89b8c1aa | ||
|
|
e046a9652d | ||
|
|
7a74d5ee83 | ||
|
|
b9086244e2 | ||
|
|
7a4aae93b4 | ||
|
|
a5350c44ae | ||
|
|
a211f94a91 | ||
|
|
bccad5585e | ||
|
|
b3cb99925d | ||
|
|
a96d8c2b3c | ||
|
|
b0219948a0 | ||
|
|
563a6d23dd | ||
|
|
bc5d348c98 | ||
|
|
818ad07eac | ||
|
|
b8f6ab9237 | ||
|
|
f1f9868028 | ||
|
|
b5deac7b48 | ||
|
|
ae430245ec | ||
|
|
ebe7ccfd10 | ||
|
|
f00e8dc335 | ||
|
|
262213a520 | ||
|
|
00f98520f7 | ||
|
|
0dfcbc9927 | ||
|
|
a68e1c9c0b | ||
|
|
75635aff2b | ||
|
|
ab2e036047 | ||
|
|
52c1989780 | ||
|
|
abd458ab82 | ||
|
|
41afe9fae7 | ||
|
|
e6fa0a6d12 | ||
|
|
a0fb6030ca | ||
|
|
c95d0e45f8 | ||
|
|
7144f171ef | ||
|
|
23e1b56c6e | ||
|
|
9b999c6249 | ||
|
|
7a81a125f5 | ||
|
|
79cdd65e30 | ||
|
|
ed22bc34fc | ||
|
|
d7ea88c367 | ||
|
|
afec9932f0 | ||
|
|
2c297d7c2f | ||
|
|
2814070808 | ||
|
|
6ed6726daf | ||
|
|
21936ae467 | ||
|
|
613a7f7073 | ||
|
|
9258347886 | ||
|
|
959193f8b5 | ||
|
|
212c6cd0ce | ||
|
|
5f3931f34b | ||
|
|
aa3327945e | ||
|
|
c8dc5d4cd9 | ||
|
|
11c46439a2 | ||
|
|
a07d661655 | ||
|
|
24391de61d | ||
|
|
33822ff6c0 | ||
|
|
5d153e764d | ||
|
|
597d67f5d2 | ||
|
|
100dd519ee | ||
|
|
eb03d62e51 | ||
|
|
8bfc7e942c | ||
|
|
1a349eb0cd | ||
|
|
f19b333537 | ||
|
|
c6c2d545f8 | ||
|
|
ebe9945492 | ||
|
|
c6403b2da3 | ||
|
|
cd100a23cb | ||
|
|
25761f9e61 | ||
|
|
1a76a83806 | ||
|
|
55ebd1df8e | ||
|
|
c427691bef | ||
|
|
93afd793cb | ||
|
|
35232cf015 | ||
|
|
f864ebf2e3 | ||
|
|
a9cc78b61d | ||
|
|
2c3a52b48c | ||
|
|
0b1a75726c | ||
|
|
384c126d9e | ||
|
|
f97094177a | ||
|
|
4739f86739 | ||
|
|
f386909748 | ||
|
|
aa4f781d25 | ||
|
|
4567b4565f | ||
|
|
3133922100 | ||
|
|
e9bd5e4476 | ||
|
|
f2e06d6d27 | ||
|
|
e31cdebc12 | ||
|
|
e03e7de82f | ||
|
|
c7b755d2ff | ||
|
|
dc7013187d | ||
|
|
20c4148301 | ||
|
|
ba24af015c | ||
|
|
94456674df | ||
|
|
5231c2d627 | ||
|
|
f74e8dda16 | ||
|
|
825d53afcd | ||
|
|
029877080d | ||
|
|
83f10a4863 | ||
|
|
d820d2d2c3 | ||
|
|
a6443f2bcc | ||
|
|
7ace5b369a | ||
|
|
cf66c10262 | ||
|
|
99296ebe69 | ||
|
|
7600ce8d26 | ||
|
|
26256b979e | ||
|
|
0113f02994 | ||
|
|
b3eb3361bb | ||
|
|
fce65e616d | ||
|
|
6a4a0b7f6b | ||
|
|
c41ab58109 | ||
|
|
837a586234 | ||
|
|
2facde3894 | ||
|
|
76a6a0e82b | ||
|
|
bea25f2a11 | ||
|
|
ad361413c7 | ||
|
|
cf61d9e2ca | ||
|
|
77ef9ff111 | ||
|
|
c8cc5e6a34 | ||
|
|
bdd5e47427 | ||
|
|
1574e08197 | ||
|
|
abc651c55d | ||
|
|
fa335564ed | ||
|
|
0ae2919def | ||
|
|
838a3bad2e | ||
|
|
6487072be1 | ||
|
|
a16f101a12 | ||
|
|
647502d701 | ||
|
|
d728065fe5 | ||
|
|
18b8a18f1d | ||
|
|
1089807086 | ||
|
|
0c1c64fdf9 | ||
|
|
90a649db9b | ||
|
|
17ec666d97 | ||
|
|
cda86a67ab | ||
|
|
32d72129e5 | ||
|
|
2905ffb5cb | ||
|
|
433b0d310e | ||
|
|
08848057a5 | ||
|
|
7f517ed4fb | ||
|
|
c364b3e307 | ||
|
|
008df99100 | ||
|
|
bfdfd98135 | ||
|
|
64be131047 | ||
|
|
7115d4c31f | ||
|
|
a944cd2608 | ||
|
|
0e7d74559f | ||
|
|
297b905163 | ||
|
|
9c08b586ec | ||
|
|
dbeca830a2 | ||
|
|
0042767fad | ||
|
|
110a0093af | ||
|
|
6bac6ee5c5 | ||
|
|
aa364e1abe | ||
|
|
6e88cc94e0 | ||
|
|
7976aace00 | ||
|
|
1261e4e25c | ||
|
|
2a655331fc | ||
|
|
98b95cab86 | ||
|
|
d8ca9dfb07 | ||
|
|
ee3cee2a50 | ||
|
|
65a6c408a7 | ||
|
|
d18a9a057b | ||
|
|
b27e3e0a7f | ||
|
|
f2804f2394 | ||
|
|
d1c30ab982 | ||
|
|
ca411a3227 | ||
|
|
5e4a48f4cc | ||
|
|
c93b53c25b | ||
|
|
51f6c0738d | ||
|
|
49f09b87a0 | ||
|
|
06c45faeb0 | ||
|
|
70eee3d4b2 | ||
|
|
ecd4267ea9 | ||
|
|
4f0e8d9e47 | ||
|
|
1c630835c1 | ||
|
|
a9c4c12824 | ||
|
|
a22ee3fefe | ||
|
|
1e9cc82b17 | ||
|
|
65085fea65 | ||
|
|
908b3c20b4 | ||
|
|
59e21cccba | ||
|
|
ab1f1dee3e | ||
|
|
04ad649f01 | ||
|
|
18e36af50a | ||
|
|
8f1ce9cef6 | ||
|
|
e47cb9732e | ||
|
|
5159ecc64e | ||
|
|
851c5fbfbc | ||
|
|
0bc9ede8bc | ||
|
|
d8d758255d | ||
|
|
c1e52ba494 | ||
|
|
b7e501f7a3 | ||
|
|
ee41cc690c | ||
|
|
965b8f9bda | ||
|
|
823c570539 | ||
|
|
221db29539 | ||
|
|
9af21b4226 | ||
|
|
7f85ceb566 | ||
|
|
d20bd20005 | ||
|
|
ce3e29813b | ||
|
|
1f6da44b25 | ||
|
|
0ea951f3d9 | ||
|
|
b75539fb57 | ||
|
|
72530e7501 | ||
|
|
790c20e7ac | ||
|
|
2cbd59b4b5 | ||
|
|
d88919e27b | ||
|
|
7ea22c84ee | ||
|
|
be86a6f3dc | ||
|
|
4046b6e0a6 | ||
|
|
6b74877d01 | ||
|
|
9f33a77745 | ||
|
|
f33c31c70e | ||
|
|
e28f1caaef | ||
|
|
fefacf039f | ||
|
|
6494669262 | ||
|
|
4971ba4722 | ||
|
|
a9626ad12c | ||
|
|
fd71acf7e0 | ||
|
|
05d643d3c3 | ||
|
|
244a4b108d | ||
|
|
1221c6043f | ||
|
|
0cfd8afce7 | ||
|
|
c8d1da5715 | ||
|
|
e5bd8e2a3b | ||
|
|
7051c76b40 | ||
|
|
76fd261141 | ||
|
|
f2ecfa0a0d | ||
|
|
5e295fc337 | ||
|
|
299c6f1ffd | ||
|
|
42e15bd630 | ||
|
|
d16066a981 | ||
|
|
51077b689d | ||
|
|
f69703a8ba | ||
|
|
2e350c2244 | ||
|
|
b43a57328c | ||
|
|
d5fcdd6a76 | ||
|
|
589189023a | ||
|
|
9c5821f6a2 | ||
|
|
671c1d7449 | ||
|
|
22cea681a7 | ||
|
|
56bd3f23c7 | ||
|
|
2b91bcffda | ||
|
|
2b6a5bfe6e | ||
|
|
d57c4b253a | ||
|
|
462fcb6b94 | ||
|
|
8e96ef4692 | ||
|
|
94b9c4b2df | ||
|
|
96e37ac725 | ||
|
|
bd7f81fb35 | ||
|
|
53372ca1e8 | ||
|
|
856ae456f4 | ||
|
|
f6b02a52fe | ||
|
|
aa52063fa4 | ||
|
|
b72bba96fb | ||
|
|
32d9dc6646 | ||
|
|
a31a63e1c8 | ||
|
|
f82a0c80bf | ||
|
|
7b59323b26 | ||
|
|
f1c5d479ad | ||
|
|
d8a956d09c | ||
|
|
a78be6874b | ||
|
|
c95c20c9ad | ||
|
|
43b7326db4 | ||
|
|
b1b3db51a6 | ||
|
|
41cd5b320f | ||
|
|
9b0caa42cd | ||
|
|
aeb8852588 | ||
|
|
b3fe3afd4c | ||
|
|
e96e9c0d5c | ||
|
|
a11f149c5a | ||
|
|
a6b517b3dd | ||
|
|
6f82fd76f5 | ||
|
|
4a9b24f096 | ||
|
|
48875294c8 | ||
|
|
12be487783 | ||
|
|
5953ef1695 | ||
|
|
17ae62c66b | ||
|
|
bb66d7220b | ||
|
|
0cf7d19d4d | ||
|
|
24a8712159 | ||
|
|
7b27feee96 | ||
|
|
0b1e95e66d | ||
|
|
997a336483 | ||
|
|
51a4f0e02e | ||
|
|
869bf10c96 | ||
|
|
0375e7e039 | ||
|
|
9b29261f3d | ||
|
|
fea5f75aa6 | ||
|
|
345d183bc4 | ||
|
|
2564aa98bb | ||
|
|
612ed3a16d | ||
|
|
a5acf8ba82 | ||
|
|
00b46fe0ea | ||
|
|
a722456359 | ||
|
|
943c9decba | ||
|
|
77396ccc31 | ||
|
|
e1a5a64b39 | ||
|
|
44e3d0b811 | ||
|
|
07f5f42870 | ||
|
|
123935c8bc | ||
|
|
2d4adb60be | ||
|
|
f8297e9652 | ||
|
|
74e8c28f45 | ||
|
|
89258638d4 | ||
|
|
52490375e4 | ||
|
|
53871cb2c6 | ||
|
|
902670cdac | ||
|
|
f11cd3c676 | ||
|
|
f088b4d459 | ||
|
|
c0252e160b | ||
|
|
98cb34f554 | ||
|
|
b2ef7fa795 | ||
|
|
ffef2f640d | ||
|
|
26bae69454 | ||
|
|
9a549a2b63 | ||
|
|
c07eaf0edb | ||
|
|
9c2ea693b9 | ||
|
|
7c339aa510 | ||
|
|
2ed6bb4067 | ||
|
|
692d92f645 | ||
|
|
7bb4b03daa | ||
|
|
e132f1d652 | ||
|
|
644788ea31 | ||
|
|
e368c50687 | ||
|
|
4569c1bd4f | ||
|
|
d6c46cb6d1 | ||
|
|
faeadeb61d | ||
|
|
d161733cdc | ||
|
|
2fd6d370c8 | ||
|
|
4dbcc65126 | ||
|
|
824abcdfa3 | ||
|
|
10693f3e5c | ||
|
|
dfd77dbf63 | ||
|
|
36c4c6a59f | ||
|
|
7b71511cae | ||
|
|
99235df566 | ||
|
|
af0e9bc880 | ||
|
|
23c8e2a0e6 | ||
|
|
c17fcfe5ef | ||
|
|
f260e41274 | ||
|
|
f81d5f8957 | ||
|
|
db55689f75 | ||
|
|
f140efe1bd | ||
|
|
fe638a4c54 | ||
|
|
b61afbc1d6 | ||
|
|
2631d9338a | ||
|
|
82e95dc4a1 | ||
|
|
3b7d006d1e | ||
|
|
b5daf5882b | ||
|
|
11f5460604 | ||
|
|
6e90dd6d62 | ||
|
|
635f6d7720 | ||
|
|
a4b40f2c46 | ||
|
|
14541b48a1 | ||
|
|
c7c5167311 | ||
|
|
e39c377e08 | ||
|
|
2887b20e38 | ||
|
|
0a631cfcae | ||
|
|
94bb223f0e | ||
|
|
12c54c39db | ||
|
|
0486d2926d | ||
|
|
cb655f15b3 | ||
|
|
5ad92972b1 | ||
|
|
201f6fd8ae | ||
|
|
b0a6417848 | ||
|
|
f80e4af683 | ||
|
|
2eb0637e61 | ||
|
|
079a20f13e | ||
|
|
7f9f864aeb | ||
|
|
e9f5fcda2a | ||
|
|
b1c9d07ca7 | ||
|
|
f0dcef7b37 | ||
|
|
53c32518b4 | ||
|
|
9d08dc2490 | ||
|
|
e5c385ae21 | ||
|
|
5e5e0028c9 | ||
|
|
cb7e7eae97 | ||
|
|
e98432b17e | ||
|
|
5434413308 | ||
|
|
8b4f21ff4c | ||
|
|
4072c39f05 | ||
|
|
9b9476d44f | ||
|
|
2e72fec4d7 | ||
|
|
1f489ff0e5 | ||
|
|
f582cc2534 | ||
|
|
a2fdc11451 | ||
|
|
a2536eb41b | ||
|
|
c66fb053e7 | ||
|
|
797f0fd0e3 | ||
|
|
02ec1f69ee | ||
|
|
37638cab86 | ||
|
|
4ce6206516 | ||
|
|
67b0624c90 | ||
|
|
9348985191 | ||
|
|
0df7df9dd6 | ||
|
|
ffab9ca8d5 | ||
|
|
6b15c335b5 | ||
|
|
c246caa520 | ||
|
|
b21eae78b0 | ||
|
|
8f3bfd2bd4 | ||
|
|
27661a9294 | ||
|
|
8ca32c5808 | ||
|
|
5a48705d49 | ||
|
|
173c257126 | ||
|
|
9e65f1b00d | ||
|
|
2c3f5385ae | ||
|
|
7bedfefecb | ||
|
|
5ab78491b5 | ||
|
|
a83261397d | ||
|
|
c8d1871418 | ||
|
|
bf7dfefcdf | ||
|
|
081cdfa145 | ||
|
|
fecfcf91ef | ||
|
|
90a0446afa | ||
|
|
ec5c7f32b0 | ||
|
|
21e4cf5865 | ||
|
|
6894060648 | ||
|
|
95d14fc5a6 | ||
|
|
46c031b35d | ||
|
|
ddced5b8c9 | ||
|
|
c786805505 | ||
|
|
75125993b9 | ||
|
|
393005fce6 | ||
|
|
952b253969 | ||
|
|
ad06d5136b | ||
|
|
18016ad23a | ||
|
|
d987724c7d | ||
|
|
87e90e956d | ||
|
|
9140688c65 | ||
|
|
1053c55d19 | ||
|
|
eb67d86163 | ||
|
|
da92a8e7c2 | ||
|
|
d4c9749133 | ||
|
|
d82932306d | ||
|
|
a76464ea16 | ||
|
|
b76f1956dc | ||
|
|
90a8a87eaf | ||
|
|
7823b65c7d | ||
|
|
38f98c7c43 | ||
|
|
30e6f1d169 | ||
|
|
df6f6a72ef | ||
|
|
c09381ff84 | ||
|
|
a172f5be51 | ||
|
|
2fec9e3c0f | ||
|
|
eb586f217b | ||
|
|
e77bdf0b90 | ||
|
|
3e045291ee | ||
|
|
93c1ecd12d | ||
|
|
deb69d4e8c | ||
|
|
4f901d0d56 | ||
|
|
e14d6007a7 | ||
|
|
96006a0196 | ||
|
|
3108f3dac3 | ||
|
|
12d66a49d0 | ||
|
|
7c9e2297df | ||
|
|
fe0c32898d | ||
|
|
091d787e34 | ||
|
|
7ee2794344 | ||
|
|
cc66f1b459 | ||
|
|
50caddee56 | ||
|
|
28dafd3e07 | ||
|
|
2f3d7c5329 | ||
|
|
4b7cc9ffcc | ||
|
|
faecd80543 | ||
|
|
e73c113e0d | ||
|
|
f528fb4345 | ||
|
|
df79530982 | ||
|
|
1a1e3d2d4a | ||
|
|
7517679c5e | ||
|
|
c6d7b714a1 | ||
|
|
d12d0cfe4f | ||
|
|
2c6447cd07 | ||
|
|
ce63113c5d | ||
|
|
3636bb71c4 | ||
|
|
788950fed2 | ||
|
|
4086fe8684 | ||
|
|
6e6d999c9b | ||
|
|
c2f866a077 | ||
|
|
0d037c3acb | ||
|
|
88f06744b4 | ||
|
|
2c1cd50828 | ||
|
|
8c4612a4dd | ||
|
|
4b13ccc5c1 | ||
|
|
b62ab9bff0 | ||
|
|
3740dd98e8 | ||
|
|
4db1cf2656 | ||
|
|
eb44c55e05 | ||
|
|
75da26ff7a | ||
|
|
93d0a265b5 | ||
|
|
494d52ca36 | ||
|
|
c67f92eb44 |
@@ -19,5 +19,12 @@ plugins/*
|
||||
!plugins/talk-plugin-sort-most-liked
|
||||
!plugins/talk-plugin-sort-most-loved
|
||||
!plugins/talk-plugin-sort-most-respected
|
||||
!plugins/talk-plugin-author-menu
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-ignore-user
|
||||
!plugins/talk-plugin-moderation-actions
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-deep-reply-count
|
||||
|
||||
node_modules
|
||||
|
||||
+73
-64
@@ -1,66 +1,75 @@
|
||||
{
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2017
|
||||
},
|
||||
"rules": {
|
||||
"indent": ["error",
|
||||
2
|
||||
],
|
||||
"no-console": [
|
||||
0
|
||||
],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": ["error", "single"],
|
||||
"semi": ["error", "always"],
|
||||
"no-template-curly-in-string": [1],
|
||||
"no-unsafe-negation": [1],
|
||||
"array-callback-return": [1],
|
||||
"arrow-parens": ["warn", "always"],
|
||||
"template-curly-spacing": "warn",
|
||||
"eqeqeq": [2, "smart"],
|
||||
"no-eval": [2],
|
||||
"no-global-assign": [2],
|
||||
"no-implied-eval": [2],
|
||||
"lines-around-comment": ["warn", {"beforeLineComment": true}],
|
||||
"spaced-comment": ["warn", "always", { "line": { "exceptions": ["-", "="] } }],
|
||||
"no-script-url": [2],
|
||||
"no-throw-literal": [2],
|
||||
"yoda": [1],
|
||||
"no-path-concat": [2],
|
||||
"eol-last": [1],
|
||||
"no-nested-ternary": [1],
|
||||
"no-tabs": [2],
|
||||
"no-unneeded-ternary": [1],
|
||||
"object-curly-spacing": [1],
|
||||
"space-infix-ops": ["error"],
|
||||
"space-in-parens": ["error", "never"],
|
||||
"space-unary-ops": ["error", {
|
||||
"words": true,
|
||||
"nonwords": false
|
||||
}],
|
||||
"no-const-assign": [2],
|
||||
"no-duplicate-imports": [2],
|
||||
"prefer-template": [1],
|
||||
"comma-spacing": ["error", {
|
||||
"after": true
|
||||
}],
|
||||
"no-var": [2],
|
||||
"no-lonely-if": [2],
|
||||
"curly": [2],
|
||||
"no-unused-vars": ["error", {
|
||||
"argsIgnorePattern": "^_|next",
|
||||
"varsIgnorePattern": "^_"
|
||||
}],
|
||||
"no-multiple-empty-lines": ["error", {
|
||||
"max": 1
|
||||
}],
|
||||
"newline-per-chained-call": ["error", {
|
||||
"ignoreChainWithDepth": 2
|
||||
}]
|
||||
}
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2017
|
||||
},
|
||||
"plugins": [
|
||||
"json",
|
||||
"promise",
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"promise/no-return-wrap": "error",
|
||||
"promise/param-names": "error",
|
||||
"promise/catch-or-return": "warn",
|
||||
"promise/no-native": "off",
|
||||
"promise/no-nesting": "warn",
|
||||
"promise/no-promise-in-callback": "warn",
|
||||
"promise/no-callback-in-promise": "warn",
|
||||
|
||||
"react/display-name": "off",
|
||||
|
||||
// We'll keeping using this, as we'll switch over to Preact and we don't
|
||||
// need to worry about deprecation.
|
||||
"react/no-find-dom-node": "off",
|
||||
|
||||
// TODO: Use default after fixing the errors.
|
||||
"react/prop-types": "warn",
|
||||
|
||||
"array-callback-return": "warn",
|
||||
"arrow-parens": ["warn", "always"],
|
||||
"comma-spacing": ["error", {"after": true}],
|
||||
"curly": "error",
|
||||
"eol-last": "warn",
|
||||
"eqeqeq": ["error", "smart"],
|
||||
"indent": ["error", 2],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"lines-around-comment": ["warn", {"beforeLineComment": true}],
|
||||
"newline-per-chained-call": ["error", {"ignoreChainWithDepth": 2}],
|
||||
"no-console": "off",
|
||||
"no-const-assign": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-eval": "error",
|
||||
"no-global-assign": "error",
|
||||
"no-implied-eval": "error",
|
||||
"no-lonely-if": "error",
|
||||
"no-multiple-empty-lines": ["error", {"max": 1}],
|
||||
"no-nested-ternary": "warn",
|
||||
"no-path-concat": "error",
|
||||
"no-script-url": "error",
|
||||
"no-tabs": "error",
|
||||
"no-template-curly-in-string": "warn",
|
||||
"no-throw-literal": "error",
|
||||
"no-unneeded-ternary": "warn",
|
||||
"no-unsafe-negation": "warn",
|
||||
"no-unused-vars": ["error", {"argsIgnorePattern": "^_|next", "varsIgnorePattern": "^_"}],
|
||||
"no-var": "error",
|
||||
"object-curly-spacing": "warn",
|
||||
"prefer-template": "warn",
|
||||
"quotes": ["error", "single"],
|
||||
"semi": ["error", "always"],
|
||||
"space-in-parens": ["error", "never"],
|
||||
"space-infix-ops": ["error"],
|
||||
"space-unary-ops": ["error", {"words": true, "nonwords": false}],
|
||||
"spaced-comment": ["warn", "always", {"line": {"exceptions": ["-", "="]}}],
|
||||
"template-curly-spacing": "warn",
|
||||
"yoda": "warn"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,19 @@ plugins/*
|
||||
!plugins/talk-plugin-comment-content
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-featured-comments
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-sort-newest
|
||||
!plugins/talk-plugin-sort-oldest
|
||||
!plugins/talk-plugin-sort-most-replied
|
||||
!plugins/talk-plugin-sort-most-liked
|
||||
!plugins/talk-plugin-sort-most-loved
|
||||
!plugins/talk-plugin-sort-most-respected
|
||||
!plugins/talk-plugin-author-menu
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-ignore-user
|
||||
!plugins/talk-plugin-moderation-actions
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-deep-reply-count
|
||||
|
||||
**/node_modules/*
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
# Talk [](https://circleci.com/gh/coralproject/talk)
|
||||
[](https://dashboard.heroku.com/new?template=https%3A%2F%2Fgithub.com%2Fcoralproject%2Ftalk&env[TALK_FACEBOOK_APP_ID]=ignore&env[TALK_FACEBOOK_APP_SECRET]=ignore)
|
||||
|
||||
Online comments are broken. Our open-source Talk tool rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here.](https://coralproject.net/products/talk.html)
|
||||
|
||||
Third party licenses are available via the `/client/3rdpartylicenses.txt`
|
||||
endpoint when the server is running with built assets.
|
||||
|
||||
## Important Links
|
||||
## Try Talk!
|
||||
|
||||
- Developer Documentation & Setup Guides: https://coralproject.github.io/talk/
|
||||
- Developer Documentation & Setup Guides: https://coralproject.github.io/talk/ (includes Installation Guide, Quickstart, Plugin Guide, API Docs, and more)
|
||||
|
||||
## Roadmap and Release Schedule
|
||||
|
||||
- Talk Roadmap: https://www.pivotaltracker.com/n/projects/1863625
|
||||
|
||||
- Pivotal Tracker Backlog & Release Schedule: https://www.pivotaltracker.com/n/projects/1863625
|
||||
|
||||
## Learn More about Coral
|
||||
|
||||
@@ -19,7 +23,7 @@ endpoint when the server is running with built assets.
|
||||
|
||||
- Blog: https://blog.coralproject.net
|
||||
|
||||
- Community Guides for Journalism: https://guides.coralproject.net/
|
||||
- Community Guides for Journalism: https://guides.coralproject.net/
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -2,10 +2,17 @@ const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const morgan = require('morgan');
|
||||
const path = require('path');
|
||||
const merge = require('lodash/merge');
|
||||
const helmet = require('helmet');
|
||||
const compression = require('compression');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const {BASE_URL, BASE_PATH, MOUNT_PATH} = require('./url');
|
||||
const {
|
||||
BASE_URL,
|
||||
BASE_PATH,
|
||||
MOUNT_PATH,
|
||||
STATIC_URL,
|
||||
HELMET_CONFIGURATION,
|
||||
} = require('./url');
|
||||
const routes = require('./routes');
|
||||
const debug = require('debug')('talk:app');
|
||||
|
||||
@@ -26,9 +33,9 @@ app.set('trust proxy', 1);
|
||||
|
||||
// Enable a suite of security good practices through helmet. We disable
|
||||
// frameguard to allow crossdomain injection of the embed.
|
||||
app.use(helmet({
|
||||
app.use(helmet(merge(HELMET_CONFIGURATION, {
|
||||
frameguard: false,
|
||||
}));
|
||||
})));
|
||||
|
||||
// Compress the responses if appropriate.
|
||||
app.use(compression());
|
||||
@@ -55,6 +62,7 @@ app.set('view engine', 'ejs');
|
||||
app.locals.BASE_URL = BASE_URL;
|
||||
app.locals.BASE_PATH = BASE_PATH;
|
||||
app.locals.MOUNT_PATH = MOUNT_PATH;
|
||||
app.locals.STATIC_URL = STATIC_URL;
|
||||
|
||||
debug(`mounting routes on the ${MOUNT_PATH} path`);
|
||||
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
{
|
||||
"name": "The Coral Project: Talk",
|
||||
"env": {
|
||||
"TALK_SESSION_SECRET": {
|
||||
"TALK_JWT_SECRET": {
|
||||
"description": "The session secret",
|
||||
"generator": "secret"
|
||||
},
|
||||
"TALK_ROOT_URL": {
|
||||
"description": "Please copy the App Name you choose above. If you did not choose one, please do so now and copy it here. Talk on Heroku will not work without this setting.",
|
||||
"value":"https://<COPY APP NAME HERE>.herokuapp.com",
|
||||
"required": true
|
||||
},
|
||||
"TALK_FACEBOOK_APP_ID": {
|
||||
"value": "",
|
||||
"required": true
|
||||
@@ -14,8 +19,7 @@
|
||||
"required": true
|
||||
},
|
||||
"NODE_ENV": "production",
|
||||
"TALK_SMTP_PORT": "2525",
|
||||
"REWRITE_ENV": "TALK_PORT:PORT,TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:POSTMARK_SMTP_SERVER,TALK_SMTP_USERNAME:POSTMARK_API_TOKEN,TALK_SMTP_PASSWORD:POSTMARK_API_TOKEN",
|
||||
"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"
|
||||
},
|
||||
"addons": [{
|
||||
@@ -25,8 +29,8 @@
|
||||
"plan": "rediscloud:30",
|
||||
"as": "REDIS"
|
||||
}, {
|
||||
"plan": "postmark:10k",
|
||||
"as": "POSTMARK"
|
||||
"plan": "mailgun:starter",
|
||||
"as": "MAILGUN"
|
||||
}],
|
||||
"image": "heroku/nodejs",
|
||||
"success_url": "/admin/install"
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
// const util = require('./util');
|
||||
const program = require('./commander');
|
||||
const {head, map} = require('lodash');
|
||||
const Matcher = require('did-you-mean');
|
||||
|
||||
program
|
||||
.command('serve', 'serve the application')
|
||||
@@ -23,6 +24,21 @@ program
|
||||
)
|
||||
.parse(process.argv);
|
||||
|
||||
// If the command wasn't found, output help.
|
||||
const cmds = map(program.commands, '_name');
|
||||
const cmd = head(program.args);
|
||||
if (!cmds.includes(cmd)) {
|
||||
const m = new Matcher(cmds);
|
||||
const similarCMDs = m.list(cmd);
|
||||
|
||||
console.error(`cli '${cmd}' is not a talk cli command. See 'cli --help'.`);
|
||||
if (similarCMDs.length > 0) {
|
||||
const sc = similarCMDs.map(({value}) => `\t${value}\n`).join('');
|
||||
console.error(`\nThe most similar commands are\n${sc}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* When this provess exists, check to see if we have a running command, if we do
|
||||
* check to see if it is still running. If it is, then kill it with a SIGINT
|
||||
@@ -31,6 +47,8 @@ program
|
||||
*/
|
||||
process.once('exit', () => {
|
||||
if (
|
||||
|
||||
// program.runningCommand &&
|
||||
program.runningCommand.killed === false &&
|
||||
program.runningCommand.exitCode === null
|
||||
) {
|
||||
|
||||
+13
-8
@@ -78,18 +78,23 @@ function rangeJobsByState(state = 'complete', limit) {
|
||||
/**
|
||||
* Cleans up the jobs that are in the queue.
|
||||
*/
|
||||
function cleanupJobs(options) {
|
||||
async function cleanupJobs(options) {
|
||||
const n = 100;
|
||||
|
||||
Promise.all([
|
||||
rangeJobsByState('complete', n),
|
||||
options.stuck ? rangeJobsByState('failed', n) : false
|
||||
])
|
||||
.then((joblists) => joblists.filter((jobs) => jobs).map(removeJobs))
|
||||
.then(() => {
|
||||
try {
|
||||
const joblists = await Promise.all([
|
||||
rangeJobsByState('complete', n),
|
||||
options.stuck ? rangeJobsByState('failed', n) : false
|
||||
]);
|
||||
|
||||
await joblists.filter((jobs) => jobs).map(removeJobs);
|
||||
|
||||
util.shutdown();
|
||||
console.log('Removed old jobs');
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ function versionMatch(name, version) {
|
||||
}
|
||||
}
|
||||
|
||||
const EXTERNAL = /^\w[a-z\-0-9\.]+$/; // Match "react", "path", "fs", "lodash.random", etc.
|
||||
const EXTERNAL = /^\w[a-z\-0-9.]+$/; // Match "react", "path", "fs", "lodash.random", etc.
|
||||
|
||||
function reconcilePackages({quiet = false, upgradeRemote = false}) {
|
||||
const fetchable = [];
|
||||
|
||||
+38
-45
@@ -98,39 +98,32 @@ function getUserCreateAnswers(options) {
|
||||
/**
|
||||
* Prompts for input and registers a user based on those.
|
||||
*/
|
||||
function createUser(options) {
|
||||
getUserCreateAnswers(options)
|
||||
.then((answers) => {
|
||||
if (answers.password !== answers.confirmPassword) {
|
||||
return Promise.reject(new Error('Passwords do not match'));
|
||||
}
|
||||
async function createUser(options) {
|
||||
try {
|
||||
const answers = await getUserCreateAnswers(options);
|
||||
if (answers.password !== answers.confirmPassword) {
|
||||
throw new Error('Passwords do not match');
|
||||
}
|
||||
|
||||
return answers;
|
||||
})
|
||||
.then((answers) => {
|
||||
return UsersService
|
||||
.createLocalUser(answers.email.trim(), answers.password.trim(), answers.username.trim())
|
||||
.then((user) => {
|
||||
console.log(`Created user ${user.id}.`);
|
||||
const user = await UsersService.createLocalUser(answers.email.trim(), answers.password.trim(), answers.username.trim());
|
||||
console.log(`Created user ${user.id}.`);
|
||||
|
||||
if (answers.roles.length > 0) {
|
||||
return Promise.all(answers.roles.map((role) => {
|
||||
return UsersService
|
||||
.addRoleToUser(user.id, role)
|
||||
.then(() => {
|
||||
console.log(`Added the role ${role} to User ${user.id}.`);
|
||||
});
|
||||
}));
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown();
|
||||
});
|
||||
if (answers.roles.length > 0) {
|
||||
return Promise.all(answers.roles.map((role) => {
|
||||
return UsersService
|
||||
.addRoleToUser(user.id, role)
|
||||
.then(() => {
|
||||
console.log(`Added the role ${role} to User ${user.id}.`);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
util.shutdown();
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,21 +162,21 @@ function passwd(userID) {
|
||||
validate: validateRequired('Confirm Password is required')
|
||||
}
|
||||
])
|
||||
.then((answers) => {
|
||||
if (answers.password !== answers.confirmPassword) {
|
||||
return Promise.reject(new Error('Password mismatch'));
|
||||
}
|
||||
.then((answers) => {
|
||||
if (answers.password !== answers.confirmPassword) {
|
||||
throw new Error('Password mismatch');
|
||||
}
|
||||
|
||||
return UsersService.changePassword(userID, answers.password);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Password changed.');
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
return UsersService.changePassword(userID, answers.password);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Password changed.');
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -125,7 +125,7 @@ module.exports = async ({fix, limit, batch}) => {
|
||||
// Check that the action summaries match the cached counts.
|
||||
if (!comment.action_counts || !(ACTION_COUNT_FIELD in comment.action_counts) || comment.action_counts[ACTION_COUNT_FIELD] !== count) {
|
||||
|
||||
// Batch updates for those changes.
|
||||
// Batch updates for those changes.
|
||||
commentOperations.push({
|
||||
[`action_counts.${ACTION_COUNT_FIELD}`]: count,
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"extends": "../.eslintrc.json",
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import {Router, Route, IndexRedirect, IndexRoute} from 'react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Router, Route, IndexRedirect, IndexRoute} from 'react-router';
|
||||
|
||||
import Configure from 'routes/Configure';
|
||||
import Dashboard from 'routes/Dashboard';
|
||||
|
||||
@@ -52,16 +52,16 @@ export const newPage = () => ({
|
||||
|
||||
export const setRole = (id, role) => (dispatch, _, {rest}) => {
|
||||
return rest(`/users/${id}/role`, {method: 'POST', body: {role}})
|
||||
.then(() => {
|
||||
return dispatch({type: SET_ROLE, id, role});
|
||||
});
|
||||
.then(() => {
|
||||
return dispatch({type: SET_ROLE, id, role});
|
||||
});
|
||||
};
|
||||
|
||||
export const setCommenterStatus = (id, status) => (dispatch, _, {rest}) => {
|
||||
return rest(`/users/${id}/status`, {method: 'POST', body: {status}})
|
||||
.then(() => {
|
||||
return dispatch({type: SET_COMMENTER_STATUS, id, status});
|
||||
});
|
||||
.then(() => {
|
||||
return dispatch({type: SET_COMMENTER_STATUS, id, status});
|
||||
});
|
||||
};
|
||||
|
||||
// Ban User Dialog
|
||||
|
||||
@@ -26,19 +26,19 @@ const validation = (formData, dispatch, next) => {
|
||||
|
||||
// Required Validation
|
||||
const empty = validKeys
|
||||
.filter((name) => {
|
||||
const cond = !formData[name].length;
|
||||
.filter((name) => {
|
||||
const cond = !formData[name].length;
|
||||
|
||||
if (cond) {
|
||||
if (cond) {
|
||||
|
||||
// Adding Error
|
||||
dispatch(addError(name, 'This field is required.'));
|
||||
} else {
|
||||
dispatch(addError(name, ''));
|
||||
}
|
||||
// Adding Error
|
||||
dispatch(addError(name, 'This field is required.'));
|
||||
} else {
|
||||
dispatch(addError(name, ''));
|
||||
}
|
||||
|
||||
return cond;
|
||||
});
|
||||
return cond;
|
||||
});
|
||||
|
||||
if (empty.length) {
|
||||
dispatch(hasError());
|
||||
@@ -128,18 +128,18 @@ const checkInstallRequest = () => ({type: actions.CHECK_INSTALL_REQUEST});
|
||||
const checkInstallSuccess = (installed) => ({type: actions.CHECK_INSTALL_SUCCESS, installed});
|
||||
const checkInstallFailure = (error) => ({type: actions.CHECK_INSTALL_FAILURE, error});
|
||||
|
||||
export const checkInstall = (next) => (dispatch, _, {rest}) => {
|
||||
export const checkInstall = (next) => async (dispatch, _, {rest}) => {
|
||||
dispatch(checkInstallRequest());
|
||||
rest('/setup')
|
||||
.then(({installed}) => {
|
||||
dispatch(checkInstallSuccess(installed));
|
||||
if (installed) {
|
||||
next();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(checkInstallFailure(errorMessage));
|
||||
});
|
||||
|
||||
try {
|
||||
const {installed} = await rest('/setup');
|
||||
dispatch(checkInstallSuccess(installed));
|
||||
if (installed) {
|
||||
next();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(checkInstallFailure(errorMessage));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,10 +4,8 @@ export const viewUserDetail = (userId) => ({type: actions.VIEW_USER_DETAIL, user
|
||||
export const hideUserDetail = () => ({type: actions.HIDE_USER_DETAIL});
|
||||
|
||||
export const changeUserDetailStatuses = (tab) => {
|
||||
let statuses;
|
||||
if (tab === 'all') {
|
||||
statuses = ['NONE', 'ACCEPTED', 'REJECTED', 'PREMOD'];
|
||||
} else if (tab === 'rejected') {
|
||||
let statuses = null;
|
||||
if (tab === 'rejected') {
|
||||
statuses = ['REJECTED'];
|
||||
}
|
||||
return {type: actions.CHANGE_USER_DETAIL_STATUSES, tab, statuses};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import styles from './ModerationList.css';
|
||||
import {Button} from 'coral-ui';
|
||||
import {menuActionsMap} from '../utils/moderationQueueActionsMap';
|
||||
@@ -26,7 +28,11 @@ const ActionButton = ({type = '', active, ...props}) => {
|
||||
};
|
||||
|
||||
ActionButton.propTypes = {
|
||||
active: PropTypes.bool
|
||||
active: PropTypes.bool,
|
||||
type: PropTypes.oneOf(['APPROVE', 'REJECT']),
|
||||
minimal: PropTypes.bool,
|
||||
acceptComment: PropTypes.func,
|
||||
rejectComment: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ActionButton;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Button, Icon} from 'coral-ui';
|
||||
import {Menu} from 'react-mdl';
|
||||
import cn from 'classnames';
|
||||
@@ -57,6 +58,7 @@ class ActionsMenu extends React.Component {
|
||||
|
||||
ActionsMenu.propTypes = {
|
||||
icon: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default ActionsMenu;
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import {MenuItem} from 'react-mdl';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './ActionsMenu.css';
|
||||
|
||||
const ActionsMenuItem = (props) =>
|
||||
<MenuItem className={cn(styles.menuItem, props.className)} {...props} />;
|
||||
|
||||
ActionsMenuItem.propTypes = {
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ActionsMenuItem;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Layout from 'coral-admin/src/components/ui/Layout';
|
||||
import styles from './NotFound.css';
|
||||
import {Button, TextField, Alert, Success} from 'coral-ui';
|
||||
@@ -69,23 +70,23 @@ class AdminLogin extends React.Component {
|
||||
);
|
||||
const requestPasswordForm = (
|
||||
this.props.passwordRequestSuccess
|
||||
? <p className={styles.passwordRequestSuccess} onClick={() => {
|
||||
location.href = location.href;
|
||||
}}>
|
||||
? <p className={styles.passwordRequestSuccess} onClick={() => {
|
||||
location.href = location.href;
|
||||
}}>
|
||||
{this.props.passwordRequestSuccess} <a className={styles.signInLink} href="#">Sign in</a>
|
||||
<Success />
|
||||
</p>
|
||||
: <form onSubmit={this.handleRequestPassword}>
|
||||
<TextField
|
||||
label='Email Address'
|
||||
value={this.state.email}
|
||||
onChange={(e) => this.setState({email: e.target.value})} />
|
||||
<Button
|
||||
type='submit'
|
||||
cStyle='black'
|
||||
full
|
||||
onClick={this.handleRequestPassword}>Reset Password</Button>
|
||||
</form>
|
||||
: <form onSubmit={this.handleRequestPassword}>
|
||||
<TextField
|
||||
label='Email Address'
|
||||
value={this.state.email}
|
||||
onChange={(e) => this.setState({email: e.target.value})} />
|
||||
<Button
|
||||
type='submit'
|
||||
cStyle='black'
|
||||
full
|
||||
onClick={this.handleRequestPassword}>Reset Password</Button>
|
||||
</form>
|
||||
);
|
||||
return (
|
||||
<Layout fixedDrawer restricted={true}>
|
||||
@@ -104,7 +105,9 @@ AdminLogin.propTypes = {
|
||||
handleLogin: PropTypes.func.isRequired,
|
||||
passwordRequestSuccess: PropTypes.string,
|
||||
loginError: PropTypes.string,
|
||||
recaptchaPublic: PropTypes.string
|
||||
recaptchaPublic: PropTypes.string,
|
||||
requestPasswordReset: PropTypes.func,
|
||||
errorMessage: PropTypes.string,
|
||||
};
|
||||
|
||||
export default AdminLogin;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Dialog} from 'coral-ui';
|
||||
import styles from './BanUserDialog.css';
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bodyLeave {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
||||
@@ -2,11 +2,13 @@ import React from 'react';
|
||||
import {murmur3} from 'murmurhash-js';
|
||||
import {CSSTransitionGroup} from 'react-transition-group';
|
||||
import styles from './CommentAnimatedEdit.css';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default ({children, body}) => {
|
||||
const CommentBodyHighlighter = ({children, body}) => {
|
||||
return (
|
||||
<CSSTransitionGroup
|
||||
component={'div'}
|
||||
className={styles.root}
|
||||
transitionName={{
|
||||
enter: styles.bodyEnter,
|
||||
enterActive: styles.bodyEnterActive,
|
||||
@@ -22,3 +24,10 @@ export default ({children, body}) => {
|
||||
</CSSTransitionGroup>
|
||||
);
|
||||
};
|
||||
|
||||
CommentBodyHighlighter.propTypes = {
|
||||
children: PropTypes.node,
|
||||
body: PropTypes.string,
|
||||
};
|
||||
|
||||
export default CommentBodyHighlighter;
|
||||
|
||||
@@ -1,25 +1,78 @@
|
||||
import React from 'react';
|
||||
import Highlighter from 'react-highlight-words';
|
||||
import Linkify from 'react-linkify';
|
||||
const linkify = new Linkify();
|
||||
import {matchLinks} from '../utils';
|
||||
import memoize from 'lodash/memoize';
|
||||
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
|
||||
// generate a regulare expression that catches the `phrases`.
|
||||
function generateRegExp(phrases) {
|
||||
const inner = phrases
|
||||
.map((phrase) =>
|
||||
phrase.split(/\s+/)
|
||||
.map((word) => escapeRegExp(word))
|
||||
.join('[\\s"?!.]+')
|
||||
).join('|');
|
||||
|
||||
return new RegExp(`(^|[^\\w])(${inner})(?=[^\\w]|$)`, 'iu');
|
||||
}
|
||||
|
||||
// Generate a regular expression detecting `suspectWords` and `bannedWords` phrases.
|
||||
function getPhrasesRegexp(suspectWords, bannedWords) {
|
||||
return generateRegExp([...suspectWords, ...bannedWords]);
|
||||
}
|
||||
|
||||
// Memoized version as arguments rarely change.
|
||||
const getPhrasesRegexpMemoized = memoize(getPhrasesRegexp);
|
||||
|
||||
// markPhrases looks for `supsectWords` and `bannedWords` inside `body` and highlights them by returning
|
||||
// an array of React Elements.
|
||||
function markPhrases(body, suspectWords, bannedWords, keyPrefix) {
|
||||
const regexp = getPhrasesRegexpMemoized(suspectWords, bannedWords);
|
||||
const tokens = body.split(regexp);
|
||||
return tokens.map((token, i) =>
|
||||
i % 3 === 2
|
||||
? <mark key={`${keyPrefix}_${i}`}>{token}</mark>
|
||||
: token
|
||||
);
|
||||
}
|
||||
|
||||
// markLinks looks for links inside `body` and highlights them by returning
|
||||
// an array of React Elements.
|
||||
function markLinks(body) {
|
||||
const matches = matchLinks(body);
|
||||
const content = [];
|
||||
let index = 0;
|
||||
if (matches) {
|
||||
matches
|
||||
.forEach((match, i) => {
|
||||
content.push(body.substring(index, match.index));
|
||||
content.push(<mark key={i}>{match.text}</mark>);
|
||||
index = match.lastIndex;
|
||||
});
|
||||
}
|
||||
content.push(body.substring(index));
|
||||
return content;
|
||||
}
|
||||
|
||||
export default ({suspectWords, bannedWords, body, ...rest}) => {
|
||||
|
||||
const links = linkify.getMatches(body);
|
||||
const linkText = links ? links.map((link) => link.raw) : [];
|
||||
// First highlight links.
|
||||
const content = markLinks(body)
|
||||
.map((element, index) => {
|
||||
|
||||
const searchWords = [
|
||||
...suspectWords,
|
||||
...bannedWords,
|
||||
...linkText
|
||||
];
|
||||
// Keep highlighted links.
|
||||
if (typeof element !== 'string') {
|
||||
return element;
|
||||
}
|
||||
|
||||
// Highlight suspect and banned phrase inside this part of text.
|
||||
return markPhrases(element, suspectWords, bannedWords, index);
|
||||
});
|
||||
return (
|
||||
<Highlighter
|
||||
{...rest}
|
||||
autoEscape={true}
|
||||
searchWords={searchWords}
|
||||
textToHighlight={body}
|
||||
/>
|
||||
<div {...rest}>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
|
||||
.textareaContainer {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
|
||||
import React from 'react';
|
||||
import styles from './CommentBox.css';
|
||||
import {Button} from 'react-mdl';
|
||||
|
||||
// Renders a comment box for creating a new comment
|
||||
export default class CommentBox extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {name: '', body: ''};
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
}
|
||||
|
||||
onSubmit () {
|
||||
const {name, body} = this.state;
|
||||
this.props.onSubmit({name, body});
|
||||
this.setState({body: '', name: ''});
|
||||
}
|
||||
|
||||
render (props, {name, body}) {
|
||||
return (
|
||||
<div>
|
||||
<div class={`${styles.textareaContainer} mdl-textfield mdl-js-textfield`}>
|
||||
<input type='text' value={name} onInput={this.linkState('name')} class='mdl-textfield__input' id='name' />
|
||||
<label class='mdl-textfield__label' for='name'>Your name</label>
|
||||
</div>
|
||||
<div class={`${styles.textareaContainer} mdl-textfield mdl-js-textfield`}>
|
||||
<textarea value={body} onInput={this.linkState('body')} class='mdl-textfield__input' type='text' rows='5' id='comment' />
|
||||
<label class='mdl-textfield__label' for='comment'>Write your comment</label>
|
||||
</div>
|
||||
<Button onClick={this.onSubmit} raised>Post</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
.root {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.coreLabels {
|
||||
> *:not(:last-child) {
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.slot {
|
||||
&:not(:empty) {
|
||||
padding-left: 3px;
|
||||
}
|
||||
> *:not(:last-child) {
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.replyLabel {
|
||||
background-color: #3D73D5;
|
||||
}
|
||||
|
||||
.premodLabel {
|
||||
background-color: #063B9A;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Label from 'coral-ui/components/Label';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import FlagLabel from 'coral-ui/components/FlagLabel';
|
||||
import cn from 'classnames';
|
||||
import styles from './CommentLabels.css';
|
||||
|
||||
function isUserFlagged(actions) {
|
||||
return actions.some((action) => action.__typename === 'FlagAction' && action.user);
|
||||
}
|
||||
|
||||
function hasSuspectedWords(actions) {
|
||||
return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'Matched suspect word filter');
|
||||
}
|
||||
|
||||
function hasHistoryFlag(actions) {
|
||||
return actions.some((action) => action.__typename === 'FlagAction' && action.reason === 'TRUST');
|
||||
}
|
||||
|
||||
const CommentLabels = ({comment, comment: {className, status, actions, hasParent}}) => {
|
||||
return (
|
||||
<div className={cn(className, styles.root)}>
|
||||
<div className={styles.coreLabels}>
|
||||
{hasParent && <Label iconName="reply" className={styles.replyLabel}>reply</Label>}
|
||||
{status === 'PREMOD' && <Label iconName="query_builder" className={styles.premodLabel}>Pre-Mod</Label>}
|
||||
{isUserFlagged(actions) && <FlagLabel iconName="person">User</FlagLabel>}
|
||||
{hasSuspectedWords(actions) && <FlagLabel iconName="sms_failed">Suspect</FlagLabel>}
|
||||
{hasHistoryFlag(actions) && <FlagLabel iconName="sentiment_very_dissatisfied">History</FlagLabel>}
|
||||
</div>
|
||||
<Slot className={styles.slot} fill="adminCommentLabels" queryData={{comment}} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CommentLabels.propTypes = {
|
||||
comment: PropTypes.shape({
|
||||
className: PropTypes.string,
|
||||
status: PropTypes.string,
|
||||
actions: PropTypes.array,
|
||||
hasParent: PropTypes.bool,
|
||||
}),
|
||||
};
|
||||
|
||||
export default CommentLabels;
|
||||
@@ -1,30 +0,0 @@
|
||||
.commentType {
|
||||
display: inline-block;
|
||||
color: white;
|
||||
background: grey;
|
||||
box-sizing: border-box;
|
||||
padding: 2px 5px;
|
||||
font-size: 12px;
|
||||
height: 24px;
|
||||
letter-spacing: 0.4px;
|
||||
line-height: 22px;
|
||||
|
||||
> i {
|
||||
font-size: 14px;
|
||||
vertical-align: text-top;
|
||||
margin: 0;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&.premod {
|
||||
background: #063B9A;
|
||||
}
|
||||
|
||||
&.flagged {
|
||||
background: #d03235;
|
||||
}
|
||||
|
||||
&.no-type {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from './CommentType.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
import cn from 'classnames';
|
||||
|
||||
const CommentType = (props) => {
|
||||
const typeData = getTypeData(props.type);
|
||||
|
||||
return (
|
||||
<span className={cn(styles.commentType, styles[typeData.className], props.className)}>
|
||||
<Icon name={typeData.icon}/>{typeData.text}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const getTypeData = (type) => {
|
||||
switch (type) {
|
||||
case 'premod':
|
||||
return {icon: 'query_builder', text: 'Pre-Mod', className: 'premod'};
|
||||
case 'flagged':
|
||||
return {icon: 'flag', text: 'Flagged', className: 'flagged'};
|
||||
default:
|
||||
return {icon: 'flag', className: 'no-type'};
|
||||
}
|
||||
};
|
||||
|
||||
CommentType.propTypes = {
|
||||
type: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default CommentType;
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
vertical-align: middle;
|
||||
padding: 1px 5px;
|
||||
border-radius: 2px;
|
||||
margin-left: 2px;
|
||||
margin-left: 5px;
|
||||
line-height: 18px;
|
||||
box-sizing: border-box;
|
||||
height: 18px;
|
||||
+6
-5
@@ -1,9 +1,10 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from './CommentCount.css';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './CountBadge.css';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const CommentCount = ({count}) => {
|
||||
const CountBadge = ({count}) => {
|
||||
let number = count;
|
||||
|
||||
// shorten large counts to abbreviations
|
||||
@@ -20,8 +21,8 @@ const CommentCount = ({count}) => {
|
||||
);
|
||||
};
|
||||
|
||||
CommentCount.propTypes = {
|
||||
CountBadge.propTypes = {
|
||||
count: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default CommentCount;
|
||||
export default CountBadge;
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Card} from 'coral-ui';
|
||||
|
||||
const EmptyCard = (props) => (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Icon} from 'coral-ui';
|
||||
import styles from './FlagBox.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from 'react';
|
||||
import Linkify from 'react-linkify';
|
||||
const linkify = new Linkify();
|
||||
import {matchLinks} from '../utils';
|
||||
|
||||
export default ({text, children}) => {
|
||||
const hasLinks = !!linkify.getMatches(text);
|
||||
const hasLinks = !!matchLinks(text);
|
||||
|
||||
if (!hasLinks) {
|
||||
return null;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Button} from 'coral-ui';
|
||||
import styles from './LoadMore.css';
|
||||
import cn from 'classnames';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'components/Modal';
|
||||
import styles from './ModerationKeysModal.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './ModerationList.css';
|
||||
import key from 'keymaster';
|
||||
import Hammer from 'hammerjs';
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Badge} from 'coral-ui';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const ReplyBadge = () => (
|
||||
<Badge icon="reply">
|
||||
{t('modqueue.reply')}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
export default ReplyBadge;
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Dialog} from 'coral-ui';
|
||||
import {RadioGroup, Radio} from 'react-mdl';
|
||||
import styles from './SuspendUserDialog.css';
|
||||
@@ -65,7 +66,7 @@ class SuspendUserDialog extends React.Component {
|
||||
{t('suspenduser.title_suspend')}
|
||||
</h1>
|
||||
<p className={styles.description}>
|
||||
{t('suspenduser.description_suspend', username)}
|
||||
{t('suspenduser.description_suspend', username)}
|
||||
</p>
|
||||
<fieldset>
|
||||
<legend className={styles.legend}>{t('suspenduser.select_duration')}</legend>
|
||||
@@ -103,7 +104,7 @@ class SuspendUserDialog extends React.Component {
|
||||
{t('suspenduser.title_notify')}
|
||||
</h1>
|
||||
<p className={styles.description}>
|
||||
{t('suspenduser.description_notify', username)}
|
||||
{t('suspenduser.description_notify', username)}
|
||||
</p>
|
||||
<fieldset>
|
||||
<legend className={styles.legend}>{t('suspenduser.write_message')}</legend>
|
||||
|
||||
@@ -28,16 +28,26 @@ export default class UserDetail extends React.Component {
|
||||
bulkReject: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
rejectThenReload = (info) => {
|
||||
this.props.rejectComment(info).then(() => {
|
||||
rejectThenReload = async (info) => {
|
||||
try {
|
||||
await this.props.rejectComment(info);
|
||||
this.props.data.refetch();
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
// TODO: handle error.
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
acceptThenReload = (info) => {
|
||||
this.props.acceptComment(info).then(() => {
|
||||
acceptThenReload = async (info) => {
|
||||
try {
|
||||
await this.props.acceptComment(info);
|
||||
this.props.data.refetch();
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
// TODO: handle error.
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
showAll = () => {
|
||||
@@ -133,35 +143,35 @@ export default class UserDetail extends React.Component {
|
||||
<Slot
|
||||
fill="userProfile"
|
||||
data={this.props.data}
|
||||
queryData={root, user}
|
||||
queryData={{root, user}}
|
||||
/>
|
||||
|
||||
<hr/>
|
||||
{
|
||||
selectedCommentIds.length === 0
|
||||
? (
|
||||
<ul className={styles.commentStatuses}>
|
||||
<li className={activeTab === 'all' ? styles.active : ''} onClick={this.showAll}>All</li>
|
||||
<li className={activeTab === 'rejected' ? styles.active : ''} onClick={this.showRejected}>Rejected</li>
|
||||
</ul>
|
||||
)
|
||||
: (
|
||||
<div className={styles.bulkActionGroup}>
|
||||
<Button
|
||||
onClick={bulkAccept}
|
||||
className={styles.bulkAction}
|
||||
cStyle='approve'
|
||||
icon='done'>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={bulkReject}
|
||||
className={styles.bulkAction}
|
||||
cStyle='reject'
|
||||
icon='close'>
|
||||
</Button>
|
||||
{`${selectedCommentIds.length} comments selected`}
|
||||
</div>
|
||||
)
|
||||
? (
|
||||
<ul className={styles.commentStatuses}>
|
||||
<li className={activeTab === 'all' ? styles.active : ''} onClick={this.showAll}>All</li>
|
||||
<li className={activeTab === 'rejected' ? styles.active : ''} onClick={this.showRejected}>Rejected</li>
|
||||
</ul>
|
||||
)
|
||||
: (
|
||||
<div className={styles.bulkActionGroup}>
|
||||
<Button
|
||||
onClick={bulkAccept}
|
||||
className={styles.bulkAction}
|
||||
cStyle='approve'
|
||||
icon='done'>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={bulkReject}
|
||||
className={styles.bulkAction}
|
||||
cStyle='reject'
|
||||
icon='close'>
|
||||
</Button>
|
||||
{`${selectedCommentIds.length} comments selected`}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<div>
|
||||
@@ -173,7 +183,6 @@ export default class UserDetail extends React.Component {
|
||||
key={comment.id}
|
||||
user={user}
|
||||
comment={comment}
|
||||
selected={false}
|
||||
suspectWords={suspectWords}
|
||||
bannedWords={bannedWords}
|
||||
actions={actionsMap[status]}
|
||||
@@ -190,7 +199,7 @@ export default class UserDetail extends React.Component {
|
||||
className={styles.loadMore}
|
||||
loadMore={loadMore}
|
||||
showLoadMore={hasNextPage}
|
||||
/>
|
||||
/>
|
||||
</Drawer>
|
||||
</ClickOutside>
|
||||
);
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.badgeBar {
|
||||
.labels {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Link} from 'react-router';
|
||||
|
||||
import {Icon} from 'coral-ui';
|
||||
import FlagBox from './FlagBox';
|
||||
import ReplyBadge from './ReplyBadge';
|
||||
import styles from './UserDetailComment.css';
|
||||
import CommentType from './CommentType';
|
||||
import {getActionSummary} from 'coral-framework/utils';
|
||||
import ActionButton from 'coral-admin/src/components/ActionButton';
|
||||
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
|
||||
import IfHasLink from 'coral-admin/src/components/IfHasLink';
|
||||
import cn from 'classnames';
|
||||
import {getCommentType} from 'coral-admin/src/utils/comment';
|
||||
import CommentAnimatedEdit from './CommentAnimatedEdit';
|
||||
import CommentLabels from '../containers/CommentLabels';
|
||||
|
||||
import t, {timeago} from 'coral-framework/services/i18n';
|
||||
|
||||
@@ -34,7 +33,6 @@ class UserDetailComment extends React.Component {
|
||||
|
||||
const flagActionSummaries = getActionSummary('FlagActionSummary', comment);
|
||||
const flagActions = comment.actions && comment.actions.filter((a) => a.__typename === 'FlagAction');
|
||||
const commentType = getCommentType(comment);
|
||||
|
||||
return (
|
||||
<li
|
||||
@@ -54,13 +52,12 @@ class UserDetailComment extends React.Component {
|
||||
</span>
|
||||
{
|
||||
(comment.editing && comment.editing.edited)
|
||||
? <span> <span className={styles.editedMarker}>({t('comment.edited')})</span></span>
|
||||
: null
|
||||
? <span> <span className={styles.editedMarker}>({t('comment.edited')})</span></span>
|
||||
: null
|
||||
}
|
||||
|
||||
<div className={styles.badgeBar}>
|
||||
{comment.hasParent && <ReplyBadge/>}
|
||||
<CommentType type={commentType}/>
|
||||
<div className={styles.labels}>
|
||||
<CommentLabels comment={comment} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.story}>
|
||||
@@ -121,10 +118,10 @@ class UserDetailComment extends React.Component {
|
||||
</div>
|
||||
{flagActions && flagActions.length
|
||||
? <FlagBox
|
||||
actions={flagActions}
|
||||
actionSummaries={flagActionSummaries}
|
||||
viewUserDetail={viewUserDetail}
|
||||
/>
|
||||
actions={flagActions}
|
||||
actionSummaries={flagActionSummaries}
|
||||
viewUserDetail={viewUserDetail}
|
||||
/>
|
||||
: null}
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Navigation, Drawer} from 'react-mdl';
|
||||
import {IndexLink, Link} from 'react-router';
|
||||
import styles from './Drawer.css';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Navigation, Header, IconButton, MenuItem, Menu} from 'react-mdl';
|
||||
import {Link, IndexLink} from 'react-router';
|
||||
import styles from './Header.css';
|
||||
@@ -13,9 +14,9 @@ const CoralHeader = ({
|
||||
}) => (
|
||||
<Header className={styles.header}>
|
||||
<Logo className={styles.logo} />
|
||||
<div>
|
||||
{
|
||||
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
|
||||
<div>
|
||||
{
|
||||
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
|
||||
<Navigation className={styles.nav}>
|
||||
<IndexLink
|
||||
id='dashboardNav'
|
||||
@@ -63,36 +64,36 @@ const CoralHeader = ({
|
||||
</Navigation>
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className={styles.rightPanel}>
|
||||
<ul>
|
||||
<li className={styles.settings}>
|
||||
<div>
|
||||
<IconButton name="settings" id="menu-settings"/>
|
||||
<Menu target="menu-settings" align="right">
|
||||
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://github.com/coralproject/talk/releases" target="_blank">
|
||||
}
|
||||
<div className={styles.rightPanel}>
|
||||
<ul>
|
||||
<li className={styles.settings}>
|
||||
<div>
|
||||
<IconButton name="settings" id="menu-settings"/>
|
||||
<Menu target="menu-settings" align="right">
|
||||
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://github.com/coralproject/talk/releases" target="_blank" rel="noopener noreferrer">
|
||||
View latest version
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://coralproject.net/contribute.html#other-ideas-and-bug-reports" target="_blank">
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://coralproject.net/contribute.html#other-ideas-and-bug-reports" target="_blank" rel="noopener noreferrer">
|
||||
Report a bug or give feedback
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleLogout}>
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
{`v${process.env.VERSION}`}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleLogout}>
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
{`v${process.env.VERSION}`}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Header>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Layout as LayoutMDL} from 'react-mdl';
|
||||
import Header from './Header';
|
||||
import Drawer from './Drawer';
|
||||
|
||||
@@ -6,15 +6,22 @@ import {hideBanUserDialog} from '../actions/banUserDialog';
|
||||
import {withSetUserStatus, withSetCommentStatus} from 'coral-framework/graphql/mutations';
|
||||
import {compose} from 'react-apollo';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {getErrorMessages} from 'coral-framework/utils';
|
||||
import {notify} from 'coral-framework/actions/notification';
|
||||
|
||||
class BanUserDialogContainer extends Component {
|
||||
|
||||
banUser = async () => {
|
||||
const {userId, commentId, commentStatus, setUserStatus, setCommentStatus, hideBanUserDialog} = this.props;
|
||||
await setUserStatus({userId, status: 'BANNED'});
|
||||
hideBanUserDialog();
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
await setCommentStatus({commentId, status: 'REJECTED'});
|
||||
const {userId, commentId, commentStatus, setUserStatus, setCommentStatus, hideBanUserDialog, notify} = this.props;
|
||||
try {
|
||||
await setUserStatus({userId, status: 'BANNED'});
|
||||
hideBanUserDialog();
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
await setCommentStatus({commentId, status: 'REJECTED'});
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
notify('error', getErrorMessages(err));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +57,7 @@ const mapStateToProps = ({banUserDialog: {open, userId, username, commentId, com
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
...bindActionCreators({
|
||||
hideBanUserDialog,
|
||||
notify,
|
||||
}, dispatch),
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import {gql} from 'react-apollo';
|
||||
import CommentLabels from '../components/CommentLabels';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
import {getSlotFragmentSpreads} from 'coral-framework/utils';
|
||||
|
||||
const slots = [
|
||||
'adminCommentLabels',
|
||||
];
|
||||
|
||||
export default withFragments({
|
||||
comment: gql`
|
||||
fragment CoralAdmin_CommentLabels_comment on Comment {
|
||||
hasParent
|
||||
status
|
||||
actions {
|
||||
__typename
|
||||
... on FlagAction {
|
||||
reason
|
||||
}
|
||||
user {
|
||||
id
|
||||
}
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'comment')}
|
||||
}
|
||||
`
|
||||
})(CommentLabels);
|
||||
@@ -55,10 +55,10 @@ class LayoutContainer extends Component {
|
||||
toggleShortcutModal={toggleShortcutModal}
|
||||
{...this.props}
|
||||
>
|
||||
<BanUserDialog />
|
||||
<SuspendUserDialog />
|
||||
<UserDetail />
|
||||
{this.props.children}
|
||||
<BanUserDialog />
|
||||
<SuspendUserDialog />
|
||||
<UserDetail />
|
||||
{this.props.children}
|
||||
</Layout>
|
||||
);
|
||||
} else if (loggedIn) {
|
||||
|
||||
@@ -17,21 +17,13 @@ class SuspendUserDialogContainer extends Component {
|
||||
const {userId, username, commentStatus, commentId, hideSuspendUserDialog, setCommentStatus, suspendUser, notify} = this.props;
|
||||
hideSuspendUserDialog();
|
||||
try {
|
||||
const result = await suspendUser({id: userId, message, until});
|
||||
if (result.data.suspendUser.errors) {
|
||||
throw result.data.suspendUser.errors;
|
||||
}
|
||||
await suspendUser({id: userId, message, until});
|
||||
notify(
|
||||
'success',
|
||||
t('suspenduser.notify_suspend_until', username, timeago(until)),
|
||||
);
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
return setCommentStatus({commentId, status: 'REJECTED'})
|
||||
.then((result) => {
|
||||
if (result.data.setCommentStatus.errors) {
|
||||
throw result.data.setCommentStatus.errors;
|
||||
}
|
||||
});
|
||||
await setCommentStatus({commentId, status: 'REJECTED'});
|
||||
}
|
||||
}
|
||||
catch(err) {
|
||||
@@ -54,8 +46,9 @@ class SuspendUserDialogContainer extends Component {
|
||||
|
||||
const withOrganizationName = withQuery(gql`
|
||||
query CoralAdmin_SuspendUserDialog {
|
||||
__typename
|
||||
settings {
|
||||
organizationName
|
||||
organizationName
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
@@ -36,14 +36,19 @@ class UserDetailContainer extends React.Component {
|
||||
isLoadingMore = false;
|
||||
|
||||
// status can be 'ACCEPTED' or 'REJECTED'
|
||||
bulkSetCommentStatus = (status) => {
|
||||
bulkSetCommentStatus = async (status) => {
|
||||
const changes = this.props.selectedCommentIds.map((commentId) => {
|
||||
return this.props.setCommentStatus({commentId, status});
|
||||
});
|
||||
|
||||
Promise.all(changes).then(() => {
|
||||
try {
|
||||
await Promise.all(changes);
|
||||
this.props.clearUserDetailSelections(); // un-select everything
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
// TODO: handle error.
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
bulkReject = () => {
|
||||
@@ -88,13 +93,13 @@ class UserDetailContainer extends React.Component {
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
this.isLoadingMore = false;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.isLoadingMore = false;
|
||||
throw err;
|
||||
});
|
||||
.then(() => {
|
||||
this.isLoadingMore = false;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.isLoadingMore = false;
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {gql} from 'react-apollo';
|
||||
import UserDetailComment from '../components/UserDetailComment';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
import {getDefinitionName} from 'coral-framework/utils';
|
||||
import CommentLabels from './CommentLabels';
|
||||
|
||||
export default withFragments({
|
||||
comment: gql`
|
||||
@@ -35,6 +37,8 @@ export default withFragments({
|
||||
editing {
|
||||
edited
|
||||
}
|
||||
...${getDefinitionName(CommentLabels.fragments.comment)}
|
||||
}
|
||||
${CommentLabels.fragments.comment}
|
||||
`
|
||||
})(UserDetailComment);
|
||||
|
||||
@@ -1,10 +1,33 @@
|
||||
import update from 'immutability-helper';
|
||||
|
||||
export default {
|
||||
mutations: {
|
||||
SetUserStatus: () => ({
|
||||
refetchQueries: ['CoralAdmin_Community'],
|
||||
SetUserStatus: ({variables: {status, userId}}) => ({
|
||||
updateQueries: {
|
||||
TalkAdmin_FlaggedAccounts: (prev) => {
|
||||
if (status !== 'APPROVED') {
|
||||
return prev;
|
||||
}
|
||||
const updated = update(prev, {
|
||||
users: {
|
||||
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)},
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
}),
|
||||
RejectUsername: () => ({
|
||||
refetchQueries: ['CoralAdmin_Community'],
|
||||
RejectUsername: ({variables: {input: {id: userId}}}) => ({
|
||||
updateQueries: {
|
||||
TalkAdmin_FlaggedAccounts: (prev) => {
|
||||
const updated = update(prev, {
|
||||
users: {
|
||||
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)},
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -12,23 +12,23 @@ import {toast} from 'react-toastify';
|
||||
import {createNotificationService} from './services/notification';
|
||||
import {hideShortcutsNote} from './actions/moderation';
|
||||
|
||||
function hidrateStore({store, storage}) {
|
||||
smoothscroll.polyfill();
|
||||
|
||||
function init({store, storage}) {
|
||||
if (storage && storage.getItem('coral:shortcutsNote') === 'hide') {
|
||||
store.dispatch(hideShortcutsNote());
|
||||
}
|
||||
}
|
||||
|
||||
smoothscroll.polyfill();
|
||||
async function main() {
|
||||
const notification = createNotificationService(toast);
|
||||
const context = await createContext({reducers, graphqlExtension, pluginsConfig, notification, init});
|
||||
render(
|
||||
<TalkProvider {...context}>
|
||||
<App />
|
||||
</TalkProvider>
|
||||
, document.querySelector('#root')
|
||||
);
|
||||
}
|
||||
|
||||
const notification = createNotificationService(toast);
|
||||
const context = createContext({reducers, graphqlExtension, pluginsConfig, notification});
|
||||
|
||||
// hidrate Store with external data.
|
||||
hidrateStore(context);
|
||||
|
||||
render(
|
||||
<TalkProvider {...context}>
|
||||
<App />
|
||||
</TalkProvider>
|
||||
, document.querySelector('#root')
|
||||
);
|
||||
main();
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as actions from '../constants/userDetail';
|
||||
const initialState = {
|
||||
userId: null,
|
||||
activeTab: 'all',
|
||||
statuses: ['NONE', 'ACCEPTED', 'REJECTED', 'PREMOD'],
|
||||
statuses: null,
|
||||
selectedCommentIds: [],
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.actionButton {
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import styles from './Community.css';
|
||||
import styles from './ActionButton.css';
|
||||
import {Button} from 'coral-ui';
|
||||
import {menuActionsMap} from '../../../utils/moderationQueueActionsMap';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
// TODO: Needs refactoring.
|
||||
const ActionButton = ({type = '', user, ...props}) => {
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -1,299 +0,0 @@
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.container {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
padding-bottom: 200px;
|
||||
}
|
||||
|
||||
.leftColumn {
|
||||
padding: 42px 56px;
|
||||
width: 234px;
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
width: calc(100% - 300px);
|
||||
padding: 34px 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mainFlaggedContent {
|
||||
width: 100%;
|
||||
padding: 34px 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.roleButton {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.searchBox {
|
||||
width: 100%;
|
||||
padding: 9px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
background: white;
|
||||
box-sizing: border-box;
|
||||
height: 40px;
|
||||
|
||||
i {
|
||||
color: #A1A1A1
|
||||
}
|
||||
|
||||
input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
padding: 0 2px 0 15px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
padding: 8px 0;
|
||||
list-style: none;
|
||||
display: block;
|
||||
|
||||
&.singleView .listItem {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.singleView .listItem.activeItem {
|
||||
display: block;
|
||||
height: 100%;
|
||||
font-size: 1.5em;
|
||||
line-height: 1.5em;
|
||||
border: none;
|
||||
|
||||
.actions {
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
left: 25%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 50%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.listItem {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 18px;
|
||||
width: 100%;
|
||||
max-width: 660px;
|
||||
min-width: 400px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
transition: all 200ms;
|
||||
padding: 10px 0 0;
|
||||
min-height: 220px;
|
||||
|
||||
.container {
|
||||
display: block;
|
||||
padding: 0 14px;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.context {
|
||||
a {
|
||||
color: #f36451;
|
||||
text-decoration: underline;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.sideActions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
padding: 50px 18px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.itemHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.author {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
min-width: 230px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.itemBody {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-right: 16px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: #757575;
|
||||
font-size: 40px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.created {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-top: 0px;
|
||||
flex: 1;
|
||||
color: black;
|
||||
max-width: 500px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.flagged {
|
||||
color: rgba(255, 0, 0, .5);
|
||||
padding-top: 15px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.flagCount{
|
||||
font-size: 12px;
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: #444;
|
||||
margin-top: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@media (--big-viewport) {
|
||||
.listItem {
|
||||
border: 1px solid #e0e0e0;
|
||||
margin-bottom: 30px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
&.activeItem {
|
||||
border: 2px solid #333;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.hasLinks {
|
||||
color: #f00;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.banned {
|
||||
color: #f00;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.ban {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.banButton {
|
||||
width: 114px;
|
||||
letter-spacing: 1px;
|
||||
|
||||
i {
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.flaggedByCount {
|
||||
display: block;
|
||||
text-align: left;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.flaggedByCount i {
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.flaggedBy {
|
||||
display: inline;
|
||||
padding: 3px;
|
||||
font-size: 14px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.flaggedByLabel {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.flaggedReasons {
|
||||
margin-left: 24px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
p.flaggedByReason {
|
||||
font-size: 1tpx;
|
||||
margin: 0px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.button {
|
||||
composes: buttonReset from 'coral-framework/styles/reset.css';
|
||||
vertical-align: text-bottom;
|
||||
&:hover {
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,9 @@ import React, {Component} from 'react';
|
||||
|
||||
import CommunityMenu from './CommunityMenu';
|
||||
import People from './People';
|
||||
import FlaggedAccounts from './FlaggedAccounts';
|
||||
import FlaggedAccounts from '../containers/FlaggedAccounts';
|
||||
import RejectUsernameDialog from './RejectUsernameDialog';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class Community extends Component {
|
||||
|
||||
@@ -54,7 +55,7 @@ export default class Community extends Component {
|
||||
}
|
||||
|
||||
getTabContent(searchValue, props) {
|
||||
const {community, root: {users}, viewUserDetail} = props;
|
||||
const {community} = props;
|
||||
const activeTab = props.route.path === ':id' ? 'flagged' : props.route.path;
|
||||
|
||||
if (activeTab === 'people') {
|
||||
@@ -77,14 +78,8 @@ export default class Community extends Component {
|
||||
return (
|
||||
<div>
|
||||
<FlaggedAccounts
|
||||
commenters={users}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
showSuspendUserDialog={props.showSuspendUserDialog}
|
||||
showRejectUsernameDialog={props.showRejectUsernameDialog}
|
||||
approveUser={props.approveUser}
|
||||
rejectUsername={props.rejectUsername}
|
||||
currentUser={this.props.currentUser}
|
||||
viewUserDetail={viewUserDetail}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
/>
|
||||
<RejectUsernameDialog
|
||||
open={community.rejectUsernameDialog}
|
||||
@@ -99,15 +94,25 @@ export default class Community extends Component {
|
||||
render() {
|
||||
const {searchValue} = this.state;
|
||||
const tab = this.getTabContent(searchValue, this.props);
|
||||
const {root: {flaggedUsernamesCount}} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CommunityMenu />
|
||||
<div>
|
||||
{ tab }
|
||||
</div>
|
||||
<CommunityMenu flaggedUsernamesCount={flaggedUsernamesCount} />
|
||||
<div>{tab}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Community.propTypes = {
|
||||
community: PropTypes.object,
|
||||
fetchAccounts: PropTypes.func,
|
||||
hideRejectUsernameDialog: PropTypes.func,
|
||||
updateSorting: PropTypes.func,
|
||||
newPage: PropTypes.func,
|
||||
route: PropTypes.object,
|
||||
rejectUsername: PropTypes.func,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object
|
||||
};
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
.tab {
|
||||
flex: 1;
|
||||
color: white;
|
||||
text-transform: capitalize;
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
letter-spacing: 1px;
|
||||
transition: border-bottom 200ms;
|
||||
}
|
||||
|
||||
.tabBar {
|
||||
background-color: rgba(44, 44, 44, 0.89);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: white;
|
||||
box-sizing: border-box;
|
||||
border-bottom: solid 5px #F36451;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
import styles from './styles.css';
|
||||
import styles from './CommunityMenu.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {Link} from 'react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
import CountBadge from '../../../components/CountBadge';
|
||||
|
||||
const CommunityMenu = () => {
|
||||
const CommunityMenu = ({flaggedUsernamesCount = 0}) => {
|
||||
const flaggedPath = '/admin/community/flagged';
|
||||
const peoplePath = '/admin/community/people';
|
||||
|
||||
return (
|
||||
<div className='mdl-tabs'>
|
||||
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
|
||||
<div>
|
||||
<Link to={flaggedPath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
|
||||
{t('community.flaggedaccounts')}
|
||||
<CountBadge count={flaggedUsernamesCount} />
|
||||
</Link>
|
||||
<Link to={peoplePath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
|
||||
{t('community.people')}
|
||||
@@ -23,4 +26,8 @@ const CommunityMenu = () => {
|
||||
);
|
||||
};
|
||||
|
||||
CommunityMenu.propTypes = {
|
||||
flaggedUsernamesCount: PropTypes.number,
|
||||
};
|
||||
|
||||
export default CommunityMenu;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
.container {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
padding-bottom: 200px;
|
||||
}
|
||||
|
||||
.mainFlaggedContent {
|
||||
width: 100%;
|
||||
padding: 34px 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.userLeave {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.userLeaveActive {
|
||||
opacity: 0;
|
||||
transition: opacity 800ms;
|
||||
}
|
||||
|
||||
.userEnter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.userEnterActive {
|
||||
opacity: 1.0;
|
||||
transition: opacity 800ms;
|
||||
}
|
||||
|
||||
@@ -1,38 +1,74 @@
|
||||
import React from 'react';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import styles from './Community.css';
|
||||
import EmptyCard from 'coral-admin/src/components/EmptyCard';
|
||||
import User from './User';
|
||||
import LoadMore from '../../../components/LoadMore';
|
||||
import FlaggedUser from '../containers/FlaggedUser';
|
||||
import {CSSTransitionGroup} from 'react-transition-group';
|
||||
import styles from './FlaggedAccounts.css';
|
||||
|
||||
const FlaggedAccounts = (props) => {
|
||||
const {commenters} = props;
|
||||
const hasResults = commenters && !!commenters.length;
|
||||
class FlaggedAccounts extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
users,
|
||||
loadMore,
|
||||
showBanUserDialog,
|
||||
showSuspendUserDialog,
|
||||
showRejectUsernameDialog,
|
||||
approveUser,
|
||||
me,
|
||||
viewUserDetail,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.mainFlaggedContent}>
|
||||
{
|
||||
hasResults
|
||||
? commenters.map((commenter, index) => {
|
||||
return <User
|
||||
user={commenter}
|
||||
key={index}
|
||||
index={index}
|
||||
modActionButtons={['APPROVE', 'REJECT']}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
showSuspendUserDialog={props.showSuspendUserDialog}
|
||||
showRejectUsernameDialog={props.showRejectUsernameDialog}
|
||||
approveUser={props.approveUser}
|
||||
currentUser={props.currentUser}
|
||||
viewUserDetail={props.viewUserDetail}
|
||||
/>;
|
||||
})
|
||||
: <EmptyCard>{t('community.no_flagged_accounts')}</EmptyCard>
|
||||
}
|
||||
const hasResults = users.nodes && !!users.nodes.length;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.mainFlaggedContent}>
|
||||
{
|
||||
hasResults
|
||||
? <CSSTransitionGroup
|
||||
component={'ul'}
|
||||
className={styles.list}
|
||||
transitionName={{
|
||||
enter: styles.userEnter,
|
||||
enterActive: styles.userEnterActive,
|
||||
leave: styles.userLeave,
|
||||
leaveActive: styles.userLeaveActive,
|
||||
}}
|
||||
transitionEnter={true}
|
||||
transitionLeave={true}
|
||||
transitionEnterTimeout={1000}
|
||||
transitionLeaveTimeout={1000}
|
||||
>
|
||||
{
|
||||
users.nodes.map((user) => {
|
||||
return (
|
||||
<FlaggedUser
|
||||
user={user}
|
||||
key={user.id}
|
||||
modActionButtons={['APPROVE', 'REJECT']}
|
||||
showBanUserDialog={showBanUserDialog}
|
||||
showSuspendUserDialog={showSuspendUserDialog}
|
||||
showRejectUsernameDialog={showRejectUsernameDialog}
|
||||
approveUser={approveUser}
|
||||
me={me}
|
||||
viewUserDetail={viewUserDetail}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</CSSTransitionGroup>
|
||||
: <EmptyCard>{t('community.no_flagged_accounts')}</EmptyCard>
|
||||
}
|
||||
<LoadMore
|
||||
loadMore={loadMore}
|
||||
showLoadMore={users.hasNextPage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FlaggedAccounts;
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.root {
|
||||
composes: mdl-shadow--2dp mdl-card from global;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 18px;
|
||||
width: 100%;
|
||||
max-width: 660px;
|
||||
min-width: 400px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
transition: box-shadow 200ms, margin-bottom 200ms;
|
||||
padding: 10px 0 0;
|
||||
min-height: 220px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
|
||||
}
|
||||
}
|
||||
|
||||
.rootSelected {
|
||||
composes: mdl-shadow--16dp from global;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: block;
|
||||
padding: 0 14px;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flaggedByCount {
|
||||
display: block;
|
||||
text-align: left;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.flagIcon {
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.flaggedBy {
|
||||
display: inline;
|
||||
padding: 3px;
|
||||
font-size: 14px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.flaggedByLabel {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.flaggedReasons {
|
||||
margin-left: 24px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.flaggedByReason {
|
||||
font-size: 1tpx;
|
||||
margin: 0px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
|
||||
@media (--big-viewport) {
|
||||
.root {
|
||||
border: 1px solid #e0e0e0;
|
||||
margin-bottom: 30px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
&.rootSelected {
|
||||
border: 2px solid #333;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
composes: buttonReset from 'coral-framework/styles/reset.css';
|
||||
vertical-align: text-bottom;
|
||||
&:hover {
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import React from 'react';
|
||||
import styles from './FlaggedUser.css';
|
||||
|
||||
import ActionButton from './ActionButton';
|
||||
|
||||
// TODO: Should not rely on plugin.
|
||||
import {username} from 'talk-plugin-flags/helpers/flagReasons';
|
||||
|
||||
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
|
||||
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
|
||||
import cn from 'classnames';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
// TODO: Should work with custom flags too.
|
||||
const shortReasons = {
|
||||
[username.other]: t('community.other'),
|
||||
[username.spam]: t('community.spam_ads'),
|
||||
[username.offensive]: t('community.offensive'),
|
||||
[username.nolike]: t('community.dont_like_username'),
|
||||
[username.impersonating]: t('community.impersonating'),
|
||||
};
|
||||
|
||||
// Render a single user for the list
|
||||
class User extends React.Component {
|
||||
|
||||
showSuspenUserDialog = () => this.props.showSuspendUserDialog({
|
||||
userId: this.props.user.id,
|
||||
username: this.props.user.username,
|
||||
});
|
||||
|
||||
showBanUserDialog = () => this.props.showBanUserDialog({
|
||||
userId: this.props.user.id,
|
||||
username: this.props.user.username,
|
||||
});
|
||||
|
||||
viewAuthorDetail = () => this.props.viewUserDetail(this.props.user.id);
|
||||
|
||||
render() {
|
||||
const {
|
||||
user,
|
||||
modActionButtons,
|
||||
viewUserDetail,
|
||||
selected,
|
||||
approveUser,
|
||||
showRejectUsernameDialog,
|
||||
me,
|
||||
className,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<li
|
||||
tabIndex={0}
|
||||
className={cn(className, styles.root, {[styles.rootSelected]: selected})}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.author}>
|
||||
<button
|
||||
onClick={this.viewAuthorDetail}
|
||||
className={styles.button}>
|
||||
{user.username}
|
||||
</button>
|
||||
{me.id !== user.id &&
|
||||
<ActionsMenu icon="not_interested">
|
||||
<ActionsMenuItem
|
||||
disabled={user.status === 'BANNED'}
|
||||
onClick={this.showSuspenUserDialog}>
|
||||
Suspend User
|
||||
</ActionsMenuItem>
|
||||
<ActionsMenuItem
|
||||
disabled={user.status === 'BANNED'}
|
||||
onClick={this.showBanUserDialog}>
|
||||
Ban User
|
||||
</ActionsMenuItem>
|
||||
</ActionsMenu>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.body}>
|
||||
<div className={styles.flagged}>
|
||||
<div className={styles.flaggedByCount}>
|
||||
<i className={cn('material-icons', styles.flagIcon)}>flag</i>
|
||||
<span className={styles.flaggedByLabel}>
|
||||
{t('community.flags')}({ user.actions.length })
|
||||
</span>:
|
||||
{ user.action_summaries.map(
|
||||
(action, i) => {
|
||||
return <span className={styles.flaggedBy} key={i}>
|
||||
{shortReasons[action.reason]} ({action.count})
|
||||
</span>;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.flaggedReasons}>
|
||||
{ user.action_summaries.map(
|
||||
(action_sum, i) => {
|
||||
return <div key={i}>
|
||||
<span className={styles.flaggedByLabel}>
|
||||
{shortReasons[action_sum.reason]} ({action_sum.count})
|
||||
</span>
|
||||
{user.actions.map(
|
||||
|
||||
// find the action by action_sum.reason
|
||||
(action, j) => {
|
||||
if (action.reason === action_sum.reason) {
|
||||
return <p className={styles.flaggedByReason} key={j}>
|
||||
{action.user &&
|
||||
<button onClick={() => {viewUserDetail(action.user.id);}} className={styles.button}>
|
||||
{action.user.username}
|
||||
</button>
|
||||
}
|
||||
: {action.message ? action.message : 'n/a'}
|
||||
</p>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
)}
|
||||
</div>;
|
||||
}
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.sideActions}>
|
||||
<div className={styles.actions}>
|
||||
{modActionButtons.map((action, i) =>
|
||||
<ActionButton key={i}
|
||||
type={action.toUpperCase()}
|
||||
user={user}
|
||||
approveUser={approveUser}
|
||||
showRejectUsernameDialog={showRejectUsernameDialog}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default User;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import styles from './Community.css';
|
||||
import styles from './styles.css';
|
||||
import Table from '../containers/Table';
|
||||
import {Pager, Icon} from 'coral-ui';
|
||||
import EmptyCard from '../../../components/EmptyCard';
|
||||
@@ -45,12 +45,12 @@ const People = ({commenters, searchValue, onSearchChange, ...props}) => {
|
||||
<div className={styles.mainContent}>
|
||||
{
|
||||
hasResults
|
||||
? <Table
|
||||
? <Table
|
||||
headers={tableHeaders}
|
||||
commenters={commenters}
|
||||
onHeaderClickHandler={props.onHeaderClickHandler}
|
||||
/>
|
||||
: <EmptyCard>{t('community.no_results')}</EmptyCard>
|
||||
: <EmptyCard>{t('community.no_results')}</EmptyCard>
|
||||
}
|
||||
<Pager
|
||||
totalPages={props.totalPages}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {Dialog, Button} from 'coral-ui';
|
||||
import styles from './RejectUsernameDialog.css';
|
||||
@@ -48,11 +49,15 @@ class RejectUsernameDialog extends Component {
|
||||
|
||||
const cancel = this.props.handleClose;
|
||||
const next = () => this.setState({stage: stage + 1});
|
||||
const suspend = () => {
|
||||
rejectUsername({id: user.user.id, message: this.state.email})
|
||||
.then(() => {
|
||||
const suspend = async () => {
|
||||
try {
|
||||
await rejectUsername({id: user.user.id, message: this.state.email});
|
||||
this.props.handleClose();
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
// TODO: handle error.
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const suspendModalActions = [
|
||||
@@ -71,21 +76,21 @@ class RejectUsernameDialog extends Component {
|
||||
const {stage} = this.state;
|
||||
|
||||
return <Dialog
|
||||
className={styles.suspendDialog}
|
||||
id="rejectUsernameDialog"
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
onCancel={handleClose}
|
||||
title={t('reject_username.suspend_user')}>
|
||||
<div className={styles.title}>
|
||||
{t(stages[stage].title, t('reject_username.username'))}
|
||||
</div>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.description}>
|
||||
{t(stages[stage].description, t('reject_username.username'))}
|
||||
</div>
|
||||
{
|
||||
stage === 1 &&
|
||||
className={styles.suspendDialog}
|
||||
id="rejectUsernameDialog"
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
onCancel={handleClose}
|
||||
title={t('reject_username.suspend_user')}>
|
||||
<div className={styles.title}>
|
||||
{t(stages[stage].title, t('reject_username.username'))}
|
||||
</div>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.description}>
|
||||
{t(stages[stage].description, t('reject_username.username'))}
|
||||
</div>
|
||||
{
|
||||
stage === 1 &&
|
||||
<div className={styles.writeContainer}>
|
||||
<div className={styles.emailMessage}>{t('reject_username.write_message')}</div>
|
||||
<div className={styles.emailContainer}>
|
||||
@@ -96,16 +101,16 @@ class RejectUsernameDialog extends Component {
|
||||
onChange={this.onEmailChange}/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className={styles.modalButtons}>
|
||||
{Object.keys(stages[stage].options).map((key, i) => (
|
||||
<Button key={i} onClick={this.onActionClick(stage, i)}>
|
||||
{t(stages[stage].options[key], t('reject_username.username'))}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>;
|
||||
}
|
||||
<div className={styles.modalButtons}>
|
||||
{Object.keys(stages[stage].options).map((key, i) => (
|
||||
<Button key={i} onClick={this.onActionClick(stage, i)}>
|
||||
{t(stages[stage].options[key], t('reject_username.username'))}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ export default ({headers, commenters, onHeaderClickHandler, onRoleChange, onComm
|
||||
<tr>
|
||||
{headers.map((header, i) =>(
|
||||
<th
|
||||
key={i}
|
||||
className="mdl-data-table__cell--non-numeric"
|
||||
onClick={() => onHeaderClickHandler({field: header.field})}>
|
||||
key={i}
|
||||
className="mdl-data-table__cell--non-numeric"
|
||||
onClick={() => onHeaderClickHandler({field: header.field})}>
|
||||
{header.title}
|
||||
</th>
|
||||
))}
|
||||
@@ -28,7 +28,8 @@ export default ({headers, commenters, onHeaderClickHandler, onRoleChange, onComm
|
||||
{row.created_at}
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<SelectField label={'Select me'} value={row.status || ''}
|
||||
<SelectField
|
||||
value={row.status || ''}
|
||||
className={styles.selectField}
|
||||
label={t('community.status')}
|
||||
onChange={(status) => onCommenterStatusChange(row.id, status)}>
|
||||
@@ -37,7 +38,8 @@ export default ({headers, commenters, onHeaderClickHandler, onRoleChange, onComm
|
||||
</SelectField>
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<SelectField label={'Select me'} value={row.roles[0] || ''}
|
||||
<SelectField
|
||||
value={row.roles[0] || ''}
|
||||
className={styles.selectField}
|
||||
label={t('community.role')}
|
||||
onChange={(role) => onRoleChange(row.id, role)}>
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import React from 'react';
|
||||
import styles from './Community.css';
|
||||
|
||||
import ActionButton from './ActionButton';
|
||||
import {username} from 'talk-plugin-flags/helpers/flagReasons';
|
||||
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
|
||||
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const shortReasons = {
|
||||
[username.other]: t('community.other'),
|
||||
[username.spam]: t('community.spam_ads'),
|
||||
[username.offensive]: t('community.offensive'),
|
||||
[username.nolike]: t('community.dont_like_username'),
|
||||
[username.impersonating]: t('community.impersonating'),
|
||||
};
|
||||
|
||||
// Render a single user for the list
|
||||
const User = (props) => {
|
||||
const {user, modActionButtons} = props;
|
||||
let userStatus = user.status;
|
||||
|
||||
const showSuspenUserDialog = () => props.showSuspendUserDialog({
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
});
|
||||
|
||||
const showBanUserDialog = () => props.showBanUserDialog({
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
});
|
||||
|
||||
// Do not display unless the user status is 'pending' or 'banned'.
|
||||
// This means that they have already been reviewed and approved.
|
||||
return (userStatus === 'PENDING' || userStatus === 'BANNED') &&
|
||||
<li tabIndex={props.index} className={`mdl-card ${props.selected ? 'mdl-shadow--8dp' : 'mdl-shadow--2dp'} ${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.itemHeader}>
|
||||
<div className={styles.author}>
|
||||
<button onClick={() => {props.viewUserDetail(user.id);}} className={styles.button}>{user.username}</button>
|
||||
{props.currentUser.id !== user.id &&
|
||||
<ActionsMenu icon="not_interested">
|
||||
<ActionsMenuItem
|
||||
disabled={user.status === 'BANNED'}
|
||||
onClick={showSuspenUserDialog}>
|
||||
Suspend User</ActionsMenuItem>
|
||||
<ActionsMenuItem
|
||||
disabled={user.status === 'BANNED'}
|
||||
onClick={showBanUserDialog}>
|
||||
Ban User
|
||||
</ActionsMenuItem>
|
||||
</ActionsMenu>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.itemBody}>
|
||||
<div className={styles.body}>
|
||||
<div className={styles.flaggedByCount}>
|
||||
<i className="material-icons">flag</i><span className={styles.flaggedByLabel}>{t('community.flags')}({ user.actions.length })</span>:
|
||||
{ user.action_summaries.map(
|
||||
(action, i) => {
|
||||
return <span className={styles.flaggedBy} key={i}>
|
||||
{shortReasons[action.reason]} ({action.count})
|
||||
</span>;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.flaggedReasons}>
|
||||
{ user.action_summaries.map(
|
||||
(action_sum, i) => {
|
||||
return <div key={i}>
|
||||
<span className={styles.flaggedByLabel}>
|
||||
{shortReasons[action_sum.reason]} ({action_sum.count})
|
||||
</span>
|
||||
{user.actions.map(
|
||||
|
||||
// find the action by action_sum.reason
|
||||
(action, j) => {
|
||||
if (action.reason === action_sum.reason) {
|
||||
return <p className={styles.flaggedByReason} key={j}>
|
||||
{action.user &&
|
||||
<button onClick={() => {props.viewUserDetail(action.user.id);}} className={styles.button}>
|
||||
{action.user.username}
|
||||
</button>
|
||||
}
|
||||
: {action.message ? action.message : 'n/a'}
|
||||
</p>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
)}
|
||||
</div>;
|
||||
}
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.sideActions}>
|
||||
<div className={`actions ${styles.actions}`}>
|
||||
{modActionButtons.map((action, i) =>
|
||||
<ActionButton key={i}
|
||||
type={action.toUpperCase()}
|
||||
user={user}
|
||||
approveUser={props.approveUser}
|
||||
showRejectUsernameDialog={props.showRejectUsernameDialog}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>;
|
||||
};
|
||||
|
||||
export default User;
|
||||
@@ -1,119 +1,56 @@
|
||||
/**
|
||||
* deprecated as this file contains styles from multiple components. Please remove this file
|
||||
* when styles have been refactored.
|
||||
*/
|
||||
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.listContainer {
|
||||
max-width: 860px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.tabBar {
|
||||
background-color: rgba(44, 44, 44, 0.89);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
color: white;
|
||||
text-transform: capitalize;
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
letter-spacing: 1px;
|
||||
transition: border-bottom 200ms;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: white;
|
||||
box-sizing: border-box;
|
||||
border-bottom: solid 5px #F36451;
|
||||
}
|
||||
|
||||
.active > span {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.active:after {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.showShortcuts {
|
||||
position: absolute;
|
||||
right: 130px;
|
||||
.container {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
|
||||
span {
|
||||
margin-left: 7px;
|
||||
}
|
||||
padding-bottom: 200px;
|
||||
}
|
||||
|
||||
@media (--big-viewport) {
|
||||
.tab {
|
||||
flex: none;
|
||||
}
|
||||
.leftColumn {
|
||||
padding: 42px 56px;
|
||||
width: 234px;
|
||||
}
|
||||
|
||||
.approve {
|
||||
margin-top: 10px;
|
||||
.mainContent {
|
||||
width: calc(100% - 300px);
|
||||
padding: 34px 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.notFound {
|
||||
position: relative;
|
||||
margin: 20px auto;
|
||||
text-align: center;
|
||||
padding: 68px 45px;
|
||||
vertical-align: middle;
|
||||
min-width: 500px;
|
||||
|
||||
a {
|
||||
color: rgb(244, 126, 107);
|
||||
font-weight: 500;
|
||||
|
||||
&.goToStreams {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
}
|
||||
}
|
||||
.roleButton {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #2c2c2c;
|
||||
color: white;
|
||||
margin-bottom: -1px;
|
||||
.searchBox {
|
||||
width: 100%;
|
||||
padding: 9px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
background: white;
|
||||
box-sizing: border-box;
|
||||
height: 40px;
|
||||
|
||||
.settingsButton {
|
||||
i {
|
||||
vertical-align: middle;
|
||||
margin-left: 10px;
|
||||
margin-top: -4px;
|
||||
}
|
||||
i {
|
||||
color: #A1A1A1
|
||||
}
|
||||
|
||||
.moderateAsset {
|
||||
a {
|
||||
text-align: center;
|
||||
-webkit-box-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
color: white;
|
||||
text-transform: capitalize;
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
letter-spacing: 1px;
|
||||
transition: opacity 200ms;
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
opacity: .8;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
padding: 0 2px 0 15px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.list {
|
||||
padding: 8px 0;
|
||||
list-style: none;
|
||||
@@ -134,15 +71,12 @@ span {
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
left: 25%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 50%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,6 +93,7 @@ span {
|
||||
min-height: 220px;
|
||||
|
||||
.container {
|
||||
display: block;
|
||||
padding: 0 14px;
|
||||
min-height: 180px;
|
||||
}
|
||||
@@ -184,9 +119,9 @@ span {
|
||||
right: 0;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
padding: 40px 18px;
|
||||
padding: 50px 18px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.itemHeader {
|
||||
display: flex;
|
||||
@@ -194,6 +129,8 @@ span {
|
||||
justify-content: space-between;
|
||||
|
||||
.author {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
min-width: 230px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -221,18 +158,12 @@ span {
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-top: 0px;
|
||||
flex: 1;
|
||||
color: black;
|
||||
max-width: 500px;
|
||||
word-wrap: break-word;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.flagged {
|
||||
@@ -245,11 +176,8 @@ span {
|
||||
font-size: 12px;
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.empty {
|
||||
color: #444;
|
||||
margin-top: 50px;
|
||||
@@ -301,34 +229,14 @@ span {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.Comment {
|
||||
.moderateArticle {
|
||||
font-size: 12px;
|
||||
a {
|
||||
display: inline-block;
|
||||
color: #679af3;
|
||||
text-decoration: none;
|
||||
font-size: 1em;
|
||||
font-weight: 400;
|
||||
letter-spacing: .5px;
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
.banButton {
|
||||
width: 114px;
|
||||
letter-spacing: 1px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
opacity: .9;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flagBox {
|
||||
max-width: 480px;
|
||||
border-top: 1px solid rgba(66, 66, 66, 0.12);
|
||||
h3 {
|
||||
i {
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,21 +3,20 @@ import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import {Spinner} from 'coral-ui';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import FlaggedAccounts from '../containers/FlaggedAccounts';
|
||||
import FlaggedUser from '../containers/FlaggedUser';
|
||||
|
||||
import {withSetUserStatus, withRejectUsername} from 'coral-framework/graphql/mutations';
|
||||
|
||||
import {showBanUserDialog} from 'actions/banUserDialog';
|
||||
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
|
||||
import {getDefinitionName} from 'coral-framework/utils';
|
||||
|
||||
import {
|
||||
fetchAccounts,
|
||||
updateSorting,
|
||||
newPage,
|
||||
showRejectUsernameDialog,
|
||||
hideRejectUsernameDialog
|
||||
} from '../../../actions/community';
|
||||
import {viewUserDetail} from '../../../actions/userDetail';
|
||||
|
||||
import Community from '../components/Community';
|
||||
|
||||
@@ -27,85 +26,69 @@ class CommunityContainer extends Component {
|
||||
this.props.fetchAccounts({});
|
||||
}
|
||||
|
||||
approveUser = ({userId}) => {
|
||||
return this.props.setUserStatus({userId, status: 'APPROVED'});
|
||||
}
|
||||
|
||||
banUser = ({userId}) => {
|
||||
return this.props.setUserStatus({userId, status: 'BANNED'});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.data.error) {
|
||||
return <div>{this.props.data.error.message}</div>;
|
||||
}
|
||||
|
||||
if (!('users' in this.props.root)) {
|
||||
return <div><Spinner/></div>;
|
||||
}
|
||||
return (
|
||||
<Community {...this.props} approveUser={this.approveUser} banUser={this.banUser}/>
|
||||
);
|
||||
return <Community
|
||||
fetchAccounts={this.props.fetchAccounts}
|
||||
community={this.props.community}
|
||||
hideRejectUsernameDialog={this.props.hideRejectUsernameDialog}
|
||||
updateSorting={this.props.updateSorting}
|
||||
newPage={this.props.newPage}
|
||||
route={this.props.route}
|
||||
rejectUsername={this.props.rejectUsername}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
export const withCommunityQuery = withQuery(gql`
|
||||
query CoralAdmin_Community($action_type: ACTION_TYPE) {
|
||||
users(query:{action_type: $action_type}){
|
||||
id
|
||||
username
|
||||
status
|
||||
roles
|
||||
actions{
|
||||
id
|
||||
created_at
|
||||
... on FlagAction {
|
||||
reason
|
||||
message
|
||||
user {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
action_summaries {
|
||||
count
|
||||
... on FlagActionSummary {
|
||||
reason
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`, {
|
||||
options: ({params: {action_type = 'FLAG'}}) => {
|
||||
return {
|
||||
variables: {
|
||||
action_type: action_type
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
community: state.community,
|
||||
currentUser: state.auth.user,
|
||||
});
|
||||
|
||||
CommunityContainer.propTypes = {
|
||||
community: PropTypes.object,
|
||||
fetchAccounts: PropTypes.func,
|
||||
hideRejectUsernameDialog: PropTypes.func,
|
||||
updateSorting: PropTypes.func,
|
||||
newPage: PropTypes.func,
|
||||
route: PropTypes.object,
|
||||
rejectUsername: PropTypes.func,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object
|
||||
};
|
||||
|
||||
const withData = withQuery(gql`
|
||||
query TalkAdmin_FlaggedUsernamesCount {
|
||||
flaggedUsernamesCount: userCount(query: {
|
||||
action_type: FLAG,
|
||||
statuses: [PENDING]
|
||||
})
|
||||
...${getDefinitionName(FlaggedAccounts.fragments.root)}
|
||||
...${getDefinitionName(FlaggedUser.fragments.root)}
|
||||
me {
|
||||
...${getDefinitionName(FlaggedUser.fragments.me)}
|
||||
__typename
|
||||
}
|
||||
}
|
||||
${FlaggedAccounts.fragments.root}
|
||||
${FlaggedUser.fragments.root}
|
||||
${FlaggedUser.fragments.me}
|
||||
`, {
|
||||
options: {
|
||||
fetchPolicy: 'network-only',
|
||||
},
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
fetchAccounts,
|
||||
showBanUserDialog,
|
||||
showSuspendUserDialog,
|
||||
showRejectUsernameDialog,
|
||||
hideRejectUsernameDialog,
|
||||
updateSorting,
|
||||
newPage,
|
||||
viewUserDetail,
|
||||
}, dispatch);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withCommunityQuery,
|
||||
withSetUserStatus,
|
||||
withRejectUsername,
|
||||
withData,
|
||||
)(CommunityContainer);
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import {withFragments} from 'plugin-api/beta/client/hocs';
|
||||
import {Spinner} from 'coral-ui';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {withSetUserStatus} from 'coral-framework/graphql/mutations';
|
||||
import {showBanUserDialog} from 'actions/banUserDialog';
|
||||
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
|
||||
import {showRejectUsernameDialog} from '../../../actions/community';
|
||||
import {viewUserDetail} from '../../../actions/userDetail';
|
||||
import {getDefinitionName} from 'coral-framework/utils';
|
||||
import {appendNewNodes} from 'plugin-api/beta/client/utils';
|
||||
import update from 'immutability-helper';
|
||||
|
||||
import FlaggedAccounts from '../components/FlaggedAccounts';
|
||||
import FlaggedUser from '../containers/FlaggedUser';
|
||||
|
||||
class FlaggedAccountsContainer extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
approveUser = ({userId}) => {
|
||||
return this.props.setUserStatus({
|
||||
userId,
|
||||
status: 'APPROVED'
|
||||
});
|
||||
}
|
||||
|
||||
loadMore = () => {
|
||||
return this.props.data.fetchMore({
|
||||
query: LOAD_MORE_QUERY,
|
||||
variables: {
|
||||
limit: 5,
|
||||
cursor: this.props.root.users.endCursor,
|
||||
},
|
||||
updateQuery: (previous, {fetchMoreResult:{users}}) => {
|
||||
const updated = update(previous, {
|
||||
users: {
|
||||
nodes: {
|
||||
$apply: (nodes) => appendNewNodes(nodes, users.nodes),
|
||||
},
|
||||
hasNextPage: {$set: users.hasNextPage},
|
||||
endCursor: {$set: users.endCursor},
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.props.data.error) {
|
||||
return <div>{this.props.data.error.message}</div>;
|
||||
}
|
||||
|
||||
if (this.props.data.loading) {
|
||||
return <div><Spinner/></div>;
|
||||
}
|
||||
return (
|
||||
<FlaggedAccounts
|
||||
showBanUserDialog={this.props.showBanUserDialog}
|
||||
showSuspendUserDialog={this.props.showSuspendUserDialog}
|
||||
showRejectUsernameDialog={this.props.showRejectUsernameDialog}
|
||||
viewUserDetail={this.props.viewUserDetail}
|
||||
approveUser={this.approveUser}
|
||||
loadMore={this.loadMore}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
users={this.props.root.users}
|
||||
me={this.props.root.me}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FlaggedAccountsContainer.propTypes = {
|
||||
showBanUserDialog: PropTypes.func,
|
||||
showSuspendUserDialog: PropTypes.func,
|
||||
showRejectUsernameDialog: PropTypes.func,
|
||||
viewUserDetail: PropTypes.func,
|
||||
setUserStatus: PropTypes.func,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object
|
||||
};
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query TalkAdmin_LoadMoreFlaggedAccounts($limit: Int, $cursor: Cursor) {
|
||||
users(query:{action_type: FLAG, statuses: [PENDING], limit: $limit, cursor: $cursor}){
|
||||
hasNextPage
|
||||
endCursor
|
||||
nodes {
|
||||
__typename
|
||||
...${getDefinitionName(FlaggedUser.fragments.user)}
|
||||
}
|
||||
}
|
||||
}
|
||||
${FlaggedUser.fragments.user}
|
||||
`;
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
showBanUserDialog,
|
||||
showSuspendUserDialog,
|
||||
showRejectUsernameDialog,
|
||||
viewUserDetail,
|
||||
}, dispatch);
|
||||
|
||||
export default compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
withSetUserStatus,
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment TalkAdminCommunity_FlaggedAccounts_root on RootQuery {
|
||||
users(query:{action_type: FLAG, statuses: [PENDING], limit: 10}){
|
||||
hasNextPage
|
||||
endCursor
|
||||
nodes {
|
||||
__typename
|
||||
...${getDefinitionName(FlaggedUser.fragments.user)}
|
||||
}
|
||||
}
|
||||
me {
|
||||
id
|
||||
}
|
||||
}
|
||||
${FlaggedUser.fragments.user}
|
||||
`,
|
||||
}),
|
||||
)(FlaggedAccountsContainer);
|
||||
@@ -0,0 +1,42 @@
|
||||
import {gql} from 'react-apollo';
|
||||
import FlaggedUser from '../components/FlaggedUser';
|
||||
import {withFragments} from 'plugin-api/beta/client/hocs';
|
||||
|
||||
export default withFragments({
|
||||
root: gql`
|
||||
fragment TalkAdminCommunity_FlaggedUser_root on RootQuery {
|
||||
__typename
|
||||
}
|
||||
`,
|
||||
me: gql`
|
||||
fragment TalkAdminCommunity_FlaggedUser_me on User {
|
||||
id
|
||||
}
|
||||
`,
|
||||
user: gql`
|
||||
fragment TalkAdminCommunity_FlaggedUser_user on User {
|
||||
id
|
||||
username
|
||||
status
|
||||
roles
|
||||
actions{
|
||||
id
|
||||
created_at
|
||||
... on FlagAction {
|
||||
reason
|
||||
message
|
||||
user {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
action_summaries {
|
||||
count
|
||||
... on FlagActionSummary {
|
||||
reason
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
})(FlaggedUser);
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @TODO: deprecated as this file contains styles from multiple components. Please refactor.
|
||||
*/
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
|
||||
|
||||
@@ -111,20 +111,20 @@ export default class Configure extends Component {
|
||||
(bool, error) => this.state.errors[error] ? false : bool, this.state.changed);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.leftColumn}>
|
||||
<List onChange={this.changeSection} activeItem={activeSection}>
|
||||
<Item itemId='stream' icon='speaker_notes'>
|
||||
{t('configure.stream_settings')}
|
||||
</Item>
|
||||
<Item itemId='moderation' icon='thumbs_up_down'>
|
||||
{t('configure.moderation_settings')}
|
||||
</Item>
|
||||
<Item itemId='tech' icon='code'>
|
||||
{t('configure.tech_settings')}
|
||||
</Item>
|
||||
</List>
|
||||
<div className={styles.saveBox}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.leftColumn}>
|
||||
<List onChange={this.changeSection} activeItem={activeSection}>
|
||||
<Item itemId='stream' icon='speaker_notes'>
|
||||
{t('configure.stream_settings')}
|
||||
</Item>
|
||||
<Item itemId='moderation' icon='thumbs_up_down'>
|
||||
{t('configure.moderation_settings')}
|
||||
</Item>
|
||||
<Item itemId='tech' icon='code'>
|
||||
{t('configure.tech_settings')}
|
||||
</Item>
|
||||
</List>
|
||||
<div className={styles.saveBox}>
|
||||
{
|
||||
showSave ?
|
||||
<Button
|
||||
@@ -136,25 +136,25 @@ export default class Configure extends Component {
|
||||
>
|
||||
{t('configure.save_changes')}
|
||||
</Button>
|
||||
:
|
||||
:
|
||||
<Button
|
||||
raised
|
||||
disabled
|
||||
icon='check'
|
||||
full
|
||||
>
|
||||
{t('configure.save_changes')}
|
||||
</Button>
|
||||
{t('configure.save_changes')}
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className={styles.mainContent}>
|
||||
{ this.props.saveFetchingError }
|
||||
{ this.props.fetchSettingsError }
|
||||
{ section }
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.mainContent}>
|
||||
{ this.props.saveFetchingError }
|
||||
{ this.props.fetchSettingsError }
|
||||
{ section }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, {Component} from 'react';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import join from 'url-join';
|
||||
import styles from './Configure.css';
|
||||
import {Button, Card} from 'coral-ui';
|
||||
import {BASE_URL} from 'coral-framework/constants/url';
|
||||
@@ -25,7 +26,7 @@ class EmbedLink extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const coralJsUrl = [BASE_URL, '/embed.js'].join('');
|
||||
const coralJsUrl = join(BASE_URL, '/embed.js');
|
||||
const nonce = String(Math.random()).slice(2);
|
||||
const streamElementId = `coral_talk_${nonce}`;
|
||||
const embedText = `
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Configure.css';
|
||||
import {Card} from 'coral-ui';
|
||||
import {Checkbox} from 'react-mdl';
|
||||
|
||||
@@ -93,15 +93,15 @@ const StreamSettings = ({updateSettings, settingsError, settings, errors}) => {
|
||||
value={settings.charCount}
|
||||
disabled={settings.charCountEnable ? '' : 'disabled'}
|
||||
/>
|
||||
<span>{t('configure.comment_count_text_post')}</span>
|
||||
{
|
||||
errors.charCount &&
|
||||
<span>{t('configure.comment_count_text_post')}</span>
|
||||
{
|
||||
errors.charCount &&
|
||||
<span className={styles.settingsError}>
|
||||
<br/>
|
||||
<Icon name="error_outline"/>
|
||||
{t('configure.comment_count_error')}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Card} from 'coral-ui';
|
||||
import Domainlist from './Domainlist';
|
||||
import EmbedLink from './EmbedLink';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './Widget.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
@@ -14,19 +15,19 @@ const ActivityWidget = ({assets}) => {
|
||||
<div className={styles.widgetTable}>
|
||||
{
|
||||
assets.length
|
||||
? assets.map((asset) => {
|
||||
return (
|
||||
<div className={styles.rowLinkify} key={asset.id}>
|
||||
<Link className={styles.linkToModerate} to={`/admin/moderate/${asset.id}`}>Moderate</Link>
|
||||
<p className={styles.widgetCount}>{asset.commentCount}</p>
|
||||
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
|
||||
<p className={styles.assetTitle}>{asset.title}</p>
|
||||
</a>
|
||||
<p className={styles.lede}>{asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}</p>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: <div className={styles.rowLinkify}>{t('dashboard.no_activity')}</div>
|
||||
? assets.map((asset) => {
|
||||
return (
|
||||
<div className={styles.rowLinkify} key={asset.id}>
|
||||
<Link className={styles.linkToModerate} to={`/admin/moderate/${asset.id}`}>Moderate</Link>
|
||||
<p className={styles.widgetCount}>{asset.commentCount}</p>
|
||||
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
|
||||
<p className={styles.assetTitle}>{asset.title}</p>
|
||||
</a>
|
||||
<p className={styles.lede}>{asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}</p>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: <div className={styles.rowLinkify}>{t('dashboard.no_activity')}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Dashboard.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @TODO: deprecated as this file contains styles from multiple components. Please refactor.
|
||||
*/
|
||||
|
||||
.Dashboard {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './Widget.css';
|
||||
|
||||
@@ -16,24 +17,24 @@ const FlagWidget = ({assets}) => {
|
||||
<div className={styles.widgetTable}>
|
||||
{
|
||||
assets.length
|
||||
? assets.map((asset) => {
|
||||
let flagSummary = null;
|
||||
if (asset.action_summaries) {
|
||||
flagSummary = asset.action_summaries.find((s) => s.__typename === 'FlagAssetActionSummary');
|
||||
}
|
||||
? assets.map((asset) => {
|
||||
let flagSummary = null;
|
||||
if (asset.action_summaries) {
|
||||
flagSummary = asset.action_summaries.find((s) => s.__typename === 'FlagAssetActionSummary');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.rowLinkify} key={asset.id}>
|
||||
<Link className={styles.linkToModerate} to={`/admin/moderate/reported/${asset.id}`}>Moderate</Link>
|
||||
<p className={styles.widgetCount}>{flagSummary ? flagSummary.actionCount : 0}</p>
|
||||
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
|
||||
<p className={styles.assetTitle}>{asset.title}</p>
|
||||
</a>
|
||||
<p className={styles.lede}>{asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}</p>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: <div className={styles.rowLinkify}>{t('dashboard.no_flags')}</div>
|
||||
return (
|
||||
<div className={styles.rowLinkify} key={asset.id}>
|
||||
<Link className={styles.linkToModerate} to={`/admin/moderate/reported/${asset.id}`}>Moderate</Link>
|
||||
<p className={styles.widgetCount}>{flagSummary ? flagSummary.actionCount : 0}</p>
|
||||
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
|
||||
<p className={styles.assetTitle}>{asset.title}</p>
|
||||
</a>
|
||||
<p className={styles.lede}>{asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}</p>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: <div className={styles.rowLinkify}>{t('dashboard.no_flags')}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './Widget.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
@@ -15,20 +16,20 @@ const LikeWidget = ({assets}) => {
|
||||
<div className={styles.widgetTable}>
|
||||
{
|
||||
assets.length
|
||||
? assets.map((asset) => {
|
||||
const likeSummary = asset.action_summaries.find((s) => s.type === 'LikeAssetActionSummary');
|
||||
return (
|
||||
<div className={styles.rowLinkify} key={asset.id}>
|
||||
<Link className={styles.linkToModerate} to={`/admin/moderate/${asset.id}`}>Moderate</Link>
|
||||
<p className={styles.widgetCount}>{likeSummary ? likeSummary.actionCount : 0}</p>
|
||||
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
|
||||
<p className={styles.assetTitle}>{asset.title}</p>
|
||||
</a>
|
||||
<p className={styles.lede}>{asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}</p>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: <div className={styles.rowLinkify}>{t('dashboard.no_likes')}</div>
|
||||
? assets.map((asset) => {
|
||||
const likeSummary = asset.action_summaries.find((s) => s.type === 'LikeAssetActionSummary');
|
||||
return (
|
||||
<div className={styles.rowLinkify} key={asset.id}>
|
||||
<Link className={styles.linkToModerate} to={`/admin/moderate/${asset.id}`}>Moderate</Link>
|
||||
<p className={styles.widgetCount}>{likeSummary ? likeSummary.actionCount : 0}</p>
|
||||
<a className={styles.linkToAsset} href={`${asset.url}`} target="_blank">
|
||||
<p className={styles.assetTitle}>{asset.title}</p>
|
||||
</a>
|
||||
<p className={styles.lede}>{asset.author} — Published: {new Date(asset.created_at).toLocaleDateString()}</p>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: <div className={styles.rowLinkify}>{t('dashboard.no_likes')}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* @TODO: deprecated as this file contains styles from multiple components. Please refactor.
|
||||
*/
|
||||
|
||||
:root {
|
||||
--row-height: 60px;
|
||||
}
|
||||
|
||||
@@ -42,15 +42,15 @@ export const witDashboardQuery = withQuery(gql`
|
||||
}
|
||||
}
|
||||
`, {
|
||||
options: ({settings: {dashboardWindowStart, dashboardWindowEnd}}) => {
|
||||
return {
|
||||
variables: {
|
||||
from: dashboardWindowStart,
|
||||
to: dashboardWindowEnd
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
options: ({settings: {dashboardWindowStart, dashboardWindowEnd}}) => {
|
||||
return {
|
||||
variables: {
|
||||
from: dashboardWindowStart,
|
||||
to: dashboardWindowEnd
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
|
||||
+6
-6
@@ -1,12 +1,12 @@
|
||||
.Install {
|
||||
.install {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
padding: 50px 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
.header {
|
||||
font-size: 2em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {Component} from 'react';
|
||||
import styles from './style.css';
|
||||
import styles from './Install.css';
|
||||
import {Wizard, WizardNav} from 'coral-ui';
|
||||
import Layout from 'coral-admin/src/components/ui/Layout';
|
||||
|
||||
@@ -37,11 +37,11 @@ export default class Install extends Component {
|
||||
|
||||
return (
|
||||
<Layout restricted={true}>
|
||||
<div className={styles.Install}>
|
||||
<div className={styles.install}>
|
||||
{
|
||||
!install.alreadyInstalled ? (
|
||||
<div>
|
||||
<h2>Welcome to the Coral Project</h2>
|
||||
<h2 className={styles.header}>Welcome to the Coral Project</h2>
|
||||
{ install.step !== 0 ? <WizardNav items={install.navItems} currentStep={install.step} icon='check'/> : null }
|
||||
<Wizard
|
||||
currentStep={install.step}
|
||||
|
||||
@@ -19,7 +19,7 @@ const InitialStep = (props) => {
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.email}
|
||||
noValidate
|
||||
/>
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className={styles.textField}
|
||||
@@ -29,7 +29,7 @@ const InitialStep = (props) => {
|
||||
onChange={handleUserChange}
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.username}
|
||||
/>
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className={styles.textField}
|
||||
@@ -39,7 +39,7 @@ const InitialStep = (props) => {
|
||||
onChange={handleUserChange}
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.password}
|
||||
/>
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className={styles.textField}
|
||||
@@ -49,13 +49,13 @@ const InitialStep = (props) => {
|
||||
onChange={handleUserChange}
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.confirmPassword}
|
||||
/>
|
||||
/>
|
||||
|
||||
{
|
||||
!props.install.isLoading ?
|
||||
<Button cStyle='black' type="submit" full>{t('install.create.save')}</Button>
|
||||
:
|
||||
<Spinner />
|
||||
<Button cStyle='black' type="submit" full>{t('install.create.save')}</Button>
|
||||
:
|
||||
<Spinner />
|
||||
}
|
||||
{props.install.installRequest === 'FAILURE' && <div className={styles.error}>Error: {props.install.installRequestError}</div>}
|
||||
</form>
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @TODO: deprecated as this file contains styles from multiple components. Please remove this file
|
||||
* when styles have been refactored.
|
||||
*/
|
||||
|
||||
.step {
|
||||
padding: 20px 0;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {compose} from 'react-apollo';
|
||||
@@ -31,7 +32,7 @@ class InstallContainer extends Component {
|
||||
}
|
||||
|
||||
InstallContainer.contextTypes = {
|
||||
router: React.PropTypes.object
|
||||
router: PropTypes.object
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Link} from 'react-router';
|
||||
|
||||
import {Icon} from 'coral-ui';
|
||||
import ReplyBadge from 'coral-admin/src/components/ReplyBadge';
|
||||
import FlagBox from 'coral-admin/src/components/FlagBox';
|
||||
import styles from './styles.css';
|
||||
import CommentType from 'coral-admin/src/components/CommentType';
|
||||
import CommentLabels from 'coral-admin/src/components/CommentLabels';
|
||||
import CommentAnimatedEdit from 'coral-admin/src/components/CommentAnimatedEdit';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import {getActionSummary} from 'coral-framework/utils';
|
||||
@@ -15,7 +15,6 @@ import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
|
||||
import IfHasLink from 'coral-admin/src/components/IfHasLink';
|
||||
import cn from 'classnames';
|
||||
import {getCommentType} from 'coral-admin/src/utils/comment';
|
||||
|
||||
import t, {timeago} from 'coral-framework/services/i18n';
|
||||
|
||||
@@ -65,9 +64,8 @@ class Comment extends React.Component {
|
||||
|
||||
const flagActionSummaries = getActionSummary('FlagActionSummary', comment);
|
||||
const flagActions = comment.actions && comment.actions.filter((a) => a.__typename === 'FlagAction');
|
||||
const commentType = getCommentType(comment);
|
||||
|
||||
let selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
|
||||
const selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
|
||||
|
||||
const queryData = {root, comment, asset: comment.asset};
|
||||
|
||||
@@ -91,8 +89,8 @@ class Comment extends React.Component {
|
||||
</span>
|
||||
{
|
||||
(comment.editing && comment.editing.edited)
|
||||
? <span> <span className={styles.editedMarker}>({t('comment.edited')})</span></span>
|
||||
: null
|
||||
? <span> <span className={styles.editedMarker}>({t('comment.edited')})</span></span>
|
||||
: null
|
||||
}
|
||||
{currentUserId !== comment.user.id &&
|
||||
<ActionsMenu icon="not_interested">
|
||||
@@ -108,8 +106,9 @@ class Comment extends React.Component {
|
||||
</ActionsMenu>
|
||||
}
|
||||
<div className={styles.adminCommentInfoBar}>
|
||||
{comment.hasParent && <ReplyBadge/>}
|
||||
<CommentType type={commentType} className={styles.commentType}/>
|
||||
<CommentLabels
|
||||
comment={comment}
|
||||
/>
|
||||
<Slot
|
||||
fill="adminCommentInfoBar"
|
||||
data={data}
|
||||
@@ -126,7 +125,7 @@ class Comment extends React.Component {
|
||||
</div>
|
||||
<CommentAnimatedEdit body={comment.body}>
|
||||
<div className={styles.itemBody}>
|
||||
<p className={styles.body}>
|
||||
<div className={styles.body}>
|
||||
<CommentBodyHighlighter
|
||||
suspectWords={suspectWords}
|
||||
bannedWords={bannedWords}
|
||||
@@ -140,7 +139,7 @@ class Comment extends React.Component {
|
||||
>
|
||||
<Icon name="open_in_new" /> {t('comment.view_context')}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<Slot
|
||||
fill="adminCommentContent"
|
||||
data={data}
|
||||
@@ -192,10 +191,10 @@ class Comment extends React.Component {
|
||||
/>
|
||||
{flagActions && flagActions.length
|
||||
? <FlagBox
|
||||
actions={flagActions}
|
||||
actionSummaries={flagActionSummaries}
|
||||
viewUserDetail={viewUserDetail}
|
||||
/>
|
||||
actions={flagActions}
|
||||
actionSummaries={flagActionSummaries}
|
||||
viewUserDetail={viewUserDetail}
|
||||
/>
|
||||
: null}
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Icon} from 'coral-ui';
|
||||
import styles from './styles.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import CommentCount from './CommentCount';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import CountBadge from '../../../components/CountBadge';
|
||||
import styles from './styles.css';
|
||||
import {SelectField, Option} from 'react-mdl-selectfield';
|
||||
import {Icon} from 'coral-ui';
|
||||
@@ -27,7 +28,7 @@ const ModerationMenu = ({
|
||||
to={getModPath(queue.key, asset.id)}
|
||||
className={cn('mdl-tabs__tab', styles.tab, {[styles.active]: activeTab === queue.key})}
|
||||
activeClassName={styles.active}>
|
||||
<Icon name={queue.icon} className={styles.tabIcon} /> {queue.name} <CommentCount count={queue.count} />
|
||||
<Icon name={queue.icon} className={styles.tabIcon} /> {queue.name} <CountBadge count={queue.count} />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Comment from '../containers/Comment';
|
||||
import styles from './styles.css';
|
||||
@@ -97,7 +98,7 @@ class ModerationQueue extends React.Component {
|
||||
rejectComment={props.rejectComment}
|
||||
currentAsset={props.currentAsset}
|
||||
currentUserId={this.props.currentUserId}
|
||||
/>;
|
||||
/>;
|
||||
})
|
||||
}
|
||||
</CSSTransitionGroup>
|
||||
@@ -110,7 +111,7 @@ class ModerationQueue extends React.Component {
|
||||
<LoadMore
|
||||
loadMore={this.loadMore}
|
||||
showLoadMore={comments.length < commentCount}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './StorySearch.css';
|
||||
|
||||
const formatDate = (date) => {
|
||||
|
||||
@@ -8,7 +8,7 @@ const StorySearch = (props) => {
|
||||
|
||||
const {
|
||||
root: {
|
||||
assets = []
|
||||
assets,
|
||||
},
|
||||
data: {loading}
|
||||
} = props;
|
||||
@@ -61,23 +61,23 @@ const StorySearch = (props) => {
|
||||
|
||||
{
|
||||
loading
|
||||
? <Spinner />
|
||||
: assets.map((story, i) => {
|
||||
const storyOpen = story.closedAt === null || new Date(story.closedAt) > new Date();
|
||||
? <Spinner />
|
||||
: assets.nodes.map((story, i) => {
|
||||
const storyOpen = story.closedAt === null || new Date(story.closedAt) > new Date();
|
||||
|
||||
return <Story
|
||||
key={i}
|
||||
id={story.id}
|
||||
title={story.title}
|
||||
createdAt={new Date(story.created_at).toISOString()}
|
||||
open={storyOpen}
|
||||
author={story.author}
|
||||
goToStory={props.goToStory}
|
||||
/>;
|
||||
})
|
||||
return <Story
|
||||
key={i}
|
||||
id={story.id}
|
||||
title={story.title}
|
||||
createdAt={new Date(story.created_at).toISOString()}
|
||||
open={storyOpen}
|
||||
author={story.author}
|
||||
goToStory={props.goToStory}
|
||||
/>;
|
||||
})
|
||||
}
|
||||
|
||||
{assets.length === 0 && <div className={styles.noResults}>No results</div>}
|
||||
{assets.nodes.length === 0 && <div className={styles.noResults}>No results</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @TODO: deprecated as this file contains styles from multiple components. Please remove this file
|
||||
* when styles have been refactored.
|
||||
*/
|
||||
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.listContainer {
|
||||
@@ -181,6 +186,14 @@ span {
|
||||
padding: 10px 0;
|
||||
min-height: 0;
|
||||
|
||||
/*
|
||||
Fix rendering issues in Safari by promoting this
|
||||
into its own layer.
|
||||
|
||||
https://www.pivotaltracker.com/story/show/151142211
|
||||
*/
|
||||
transform: translateZ(0);
|
||||
|
||||
.container {
|
||||
padding: 0 14px;
|
||||
}
|
||||
@@ -444,20 +457,20 @@ span {
|
||||
|
||||
.commentLeave {
|
||||
opacity: 1.0;
|
||||
transition: opacity 800ms;
|
||||
}
|
||||
|
||||
.commentLeaveActive {
|
||||
opacity: 0;
|
||||
transition: opacity 800ms;
|
||||
}
|
||||
|
||||
.commentEnter {
|
||||
opacity: 0;
|
||||
transition: opacity 800ms;
|
||||
}
|
||||
|
||||
.commentEnterActive {
|
||||
opacity: 1.0;
|
||||
transition: opacity 800ms;
|
||||
}
|
||||
|
||||
.editedMarker {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {gql} from 'react-apollo';
|
||||
import Comment from '../components/Comment';
|
||||
import CommentLabels from '../../../containers/CommentLabels';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
import {getSlotFragmentSpreads} from 'coral-framework/utils';
|
||||
import {getSlotFragmentSpreads, getDefinitionName} from 'coral-framework/utils';
|
||||
|
||||
const slots = [
|
||||
'adminCommentInfoBar',
|
||||
@@ -58,6 +59,8 @@ export default withFragments({
|
||||
}
|
||||
hasParent
|
||||
${getSlotFragmentSpreads(slots, 'comment')}
|
||||
...${getDefinitionName(CommentLabels.fragments.comment)}
|
||||
}
|
||||
${CommentLabels.fragments.comment}
|
||||
`
|
||||
})(Comment);
|
||||
|
||||
@@ -170,7 +170,8 @@ class ModerationContainer extends Component {
|
||||
cursor: this.props.root[tab].endCursor,
|
||||
sortOrder: this.props.data.variables.sortOrder,
|
||||
asset_id: this.props.data.variables.asset_id,
|
||||
statuses: this.props.queueConfig[tab].statuses,
|
||||
statuses: this.props.queueConfig[tab].statuses || null,
|
||||
tags: this.props.queueConfig[tab].tags || null,
|
||||
action_type: this.props.queueConfig[tab].action_type,
|
||||
};
|
||||
return this.props.data.fetchMore({
|
||||
@@ -208,17 +209,22 @@ class ModerationContainer extends Component {
|
||||
// Still loading.
|
||||
return <Spinner />;
|
||||
}
|
||||
} else if(asset !== undefined || !('premodCount' in root)) {
|
||||
} else if (asset !== undefined || !('premodCount' in root)) {
|
||||
|
||||
// loading.
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const premodEnabled = assetId ? isPremod(asset.settings.moderation) : isPremod(settings.moderation);
|
||||
const premodEnabled = assetId ? isPremod(asset.settings.moderation) :
|
||||
isPremod(settings.moderation);
|
||||
|
||||
const currentQueueConfig = Object.assign({}, this.props.queueConfig);
|
||||
if (premodEnabled) {
|
||||
|
||||
if (premodEnabled && root.newCount === 0) {
|
||||
delete currentQueueConfig.new;
|
||||
} else {
|
||||
}
|
||||
|
||||
if (!premodEnabled && root.premodCount === 0) {
|
||||
delete currentQueueConfig.premod;
|
||||
}
|
||||
|
||||
@@ -288,8 +294,8 @@ const COMMENT_REJECTED_SUBSCRIPTION = gql`
|
||||
`;
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $sortOrder: SORT_ORDER, $asset_id: ID, $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sortOrder: $sortOrder, action_type: $action_type}) {
|
||||
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $sortOrder: SORT_ORDER, $asset_id: ID, $tags:[String!], $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sortOrder: $sortOrder, action_type: $action_type, tags: $tags}) {
|
||||
nodes {
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
}
|
||||
@@ -314,10 +320,10 @@ const commentConnectionFragment = gql`
|
||||
`;
|
||||
|
||||
const withModQueueQuery = withQuery(({queueConfig}) => gql`
|
||||
query CoralAdmin_Moderation($asset_id: ID, $sortOrder: SORT_ORDER, $allAssets: Boolean!) {
|
||||
query CoralAdmin_Moderation($asset_id: ID, $sortOrder: SORT_ORDER, $allAssets: Boolean!, $nullStatuses: [COMMENT_STATUS!]) {
|
||||
${Object.keys(queueConfig).map((queue) => `
|
||||
${queue}: comments(query: {
|
||||
${queueConfig[queue].statuses ? `statuses: [${queueConfig[queue].statuses.join(', ')}],` : ''}
|
||||
statuses: ${queueConfig[queue].statuses ? `[${queueConfig[queue].statuses.join(', ')}],` : '$nullStatuses'}
|
||||
${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''}
|
||||
${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''}
|
||||
asset_id: $asset_id,
|
||||
@@ -328,7 +334,7 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql`
|
||||
`)}
|
||||
${Object.keys(queueConfig).map((queue) => `
|
||||
${queue}Count: commentCount(query: {
|
||||
${queueConfig[queue].statuses ? `statuses: [${queueConfig[queue].statuses.join(', ')}],` : ''}
|
||||
statuses: ${queueConfig[queue].statuses ? `[${queueConfig[queue].statuses.join(', ')}],` : '$nullStatuses'}
|
||||
${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''}
|
||||
${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''}
|
||||
asset_id: $asset_id,
|
||||
@@ -355,8 +361,10 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql`
|
||||
variables: {
|
||||
asset_id: id,
|
||||
sortOrder: props.moderation.sortOrder,
|
||||
allAssets: id === null
|
||||
}
|
||||
allAssets: id === null,
|
||||
nullStatuses: null,
|
||||
},
|
||||
fetchPolicy: 'network-only'
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user