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 | ||
|
|
52490375e4 | ||
|
|
53871cb2c6 | ||
|
|
9a549a2b63 | ||
|
|
4dbcc65126 | ||
|
|
10693f3e5c | ||
|
|
dfd77dbf63 | ||
|
|
7b71511cae | ||
|
|
f260e41274 | ||
|
|
2631d9338a | ||
|
|
94bb223f0e | ||
|
|
0486d2926d | ||
|
|
cb655f15b3 | ||
|
|
2eb0637e61 | ||
|
|
079a20f13e | ||
|
|
f0dcef7b37 | ||
|
|
a2fdc11451 | ||
|
|
6b15c335b5 | ||
|
|
c246caa520 | ||
|
|
8ca32c5808 | ||
|
|
5a48705d49 | ||
|
|
173c257126 | ||
|
|
9e65f1b00d | ||
|
|
bf7dfefcdf | ||
|
|
d4c9749133 | ||
|
|
90a8a87eaf | ||
|
|
deb69d4e8c | ||
|
|
f528fb4345 | ||
|
|
df79530982 | ||
|
|
1a1e3d2d4a | ||
|
|
7517679c5e | ||
|
|
c6d7b714a1 | ||
|
|
d12d0cfe4f | ||
|
|
2c6447cd07 | ||
|
|
ce63113c5d | ||
|
|
3636bb71c4 | ||
|
|
788950fed2 | ||
|
|
4086fe8684 | ||
|
|
6e6d999c9b | ||
|
|
c2f866a077 | ||
|
|
0d037c3acb | ||
|
|
88f06744b4 | ||
|
|
2c1cd50828 | ||
|
|
8c4612a4dd | ||
|
|
4b13ccc5c1 | ||
|
|
b62ab9bff0 | ||
|
|
3740dd98e8 | ||
|
|
4db1cf2656 | ||
|
|
eb44c55e05 | ||
|
|
75da26ff7a | ||
|
|
93d0a265b5 | ||
|
|
494d52ca36 | ||
|
|
c67f92eb44 |
@@ -22,5 +22,9 @@ plugins/*
|
||||
!plugins/talk-plugin-author-menu
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-ignore-user
|
||||
!plugins/talk-plugin-moderation-actions
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-deep-reply-count
|
||||
|
||||
node_modules
|
||||
|
||||
+73
-73
@@ -1,75 +1,75 @@
|
||||
{
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2017
|
||||
},
|
||||
"plugins": [
|
||||
"promise",
|
||||
"json"
|
||||
],
|
||||
"rules": {
|
||||
"indent": ["error",
|
||||
2
|
||||
],
|
||||
"no-console": "off",
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": ["error", "single"],
|
||||
"semi": ["error", "always"],
|
||||
"no-template-curly-in-string": "warn",
|
||||
"no-unsafe-negation": "warn",
|
||||
"array-callback-return": "warn",
|
||||
"arrow-parens": ["warn", "always"],
|
||||
"template-curly-spacing": "warn",
|
||||
"eqeqeq": ["error", "smart"],
|
||||
"no-eval": "error",
|
||||
"no-global-assign": "error",
|
||||
"no-implied-eval": "error",
|
||||
"lines-around-comment": ["warn", {"beforeLineComment": true}],
|
||||
"spaced-comment": ["warn", "always", { "line": { "exceptions": ["-", "="] } }],
|
||||
"no-script-url": "error",
|
||||
"no-throw-literal": "error",
|
||||
"yoda": "warn",
|
||||
"no-path-concat": "error",
|
||||
"eol-last": "warn",
|
||||
"no-nested-ternary": "warn",
|
||||
"no-tabs": "error",
|
||||
"no-unneeded-ternary": "warn",
|
||||
"object-curly-spacing": "warn",
|
||||
"space-infix-ops": ["error"],
|
||||
"space-in-parens": ["error", "never"],
|
||||
"space-unary-ops": ["error", {
|
||||
"words": true,
|
||||
"nonwords": false
|
||||
}],
|
||||
"no-const-assign": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"prefer-template": "warn",
|
||||
"comma-spacing": ["error", {
|
||||
"after": true
|
||||
}],
|
||||
"no-var": "error",
|
||||
"no-lonely-if": "error",
|
||||
"curly": "error",
|
||||
"no-unused-vars": ["error", {
|
||||
"argsIgnorePattern": "^_|next",
|
||||
"varsIgnorePattern": "^_"
|
||||
}],
|
||||
"no-multiple-empty-lines": ["error", {
|
||||
"max": 1
|
||||
}],
|
||||
"newline-per-chained-call": ["error", {
|
||||
"ignoreChainWithDepth": 2
|
||||
}],
|
||||
"promise/no-return-wrap": "error",
|
||||
"promise/param-names": "error",
|
||||
"promise/catch-or-return": "error",
|
||||
"promise/no-native": "off",
|
||||
"promise/no-nesting": "warn",
|
||||
"promise/no-promise-in-callback": "warn",
|
||||
"promise/no-callback-in-promise": "warn"
|
||||
}
|
||||
"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,6 +29,7 @@ plugins/*
|
||||
!plugins/talk-plugin-comment-content
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-featured-comments
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-sort-newest
|
||||
!plugins/talk-plugin-sort-oldest
|
||||
!plugins/talk-plugin-sort-most-replied
|
||||
@@ -38,5 +39,9 @@ plugins/*
|
||||
!plugins/talk-plugin-author-menu
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-ignore-user
|
||||
!plugins/talk-plugin-moderation-actions
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-deep-reply-count
|
||||
|
||||
**/node_modules/*
|
||||
|
||||
@@ -6,11 +6,14 @@ Online comments are broken. Our open-source Talk tool rethinks how moderation, c
|
||||
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
|
||||
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
@@ -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,7 +143,7 @@ export default class UserDetail extends React.Component {
|
||||
<Slot
|
||||
fill="userProfile"
|
||||
data={this.props.data}
|
||||
queryData={root, user}
|
||||
queryData={{root, user}}
|
||||
/>
|
||||
|
||||
<hr/>
|
||||
@@ -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]}
|
||||
|
||||
@@ -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
|
||||
@@ -58,9 +56,8 @@ class UserDetailComment extends React.Component {
|
||||
: null
|
||||
}
|
||||
|
||||
<div className={styles.badgeBar}>
|
||||
{comment.hasParent && <ReplyBadge/>}
|
||||
<CommentType type={commentType}/>
|
||||
<div className={styles.labels}>
|
||||
<CommentLabels comment={comment} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.story}>
|
||||
|
||||
@@ -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';
|
||||
@@ -72,12 +73,12 @@ const CoralHeader = ({
|
||||
<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">
|
||||
<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 href="https://coralproject.net/contribute.html#other-ideas-and-bug-reports" target="_blank" rel="noopener noreferrer">
|
||||
Report a bug or give feedback
|
||||
</a>
|
||||
</MenuItem>
|
||||
@@ -87,8 +88,8 @@ const CoralHeader = ({
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
{`v${process.env.VERSION}`}
|
||||
<li>
|
||||
{`v${process.env.VERSION}`}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: [],
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import CommunityMenu from './CommunityMenu';
|
||||
import People from './People';
|
||||
import FlaggedAccounts from '../containers/FlaggedAccounts';
|
||||
import RejectUsernameDialog from './RejectUsernameDialog';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class Community extends Component {
|
||||
|
||||
@@ -76,7 +77,10 @@ export default class Community extends Component {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FlaggedAccounts />
|
||||
<FlaggedAccounts
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
/>
|
||||
<RejectUsernameDialog
|
||||
open={community.rejectUsernameDialog}
|
||||
handleClose={props.hideRejectUsernameDialog}
|
||||
@@ -90,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
|
||||
};
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
import styles from './CommunityMenu.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {Link} from 'react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
import CountBadge from '../../../components/CountBadge';
|
||||
|
||||
const CommunityMenu = () => {
|
||||
const CommunityMenu = ({flaggedUsernamesCount = 0}) => {
|
||||
const flaggedPath = '/admin/community/flagged';
|
||||
const peoplePath = '/admin/community/people';
|
||||
|
||||
return (
|
||||
<div className='mdl-tabs'>
|
||||
<div className={`mdl-tabs__tab-bar ${styles.tabBar}`}>
|
||||
<div>
|
||||
<Link to={flaggedPath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
|
||||
{t('community.flaggedaccounts')}
|
||||
<CountBadge count={flaggedUsernamesCount} />
|
||||
</Link>
|
||||
<Link to={peoplePath} className={`mdl-tabs__tab ${styles.tab}`} activeClassName={styles.active}>
|
||||
{t('community.people')}
|
||||
@@ -23,4 +26,8 @@ const CommunityMenu = () => {
|
||||
);
|
||||
};
|
||||
|
||||
CommunityMenu.propTypes = {
|
||||
flaggedUsernamesCount: PropTypes.number,
|
||||
};
|
||||
|
||||
export default CommunityMenu;
|
||||
|
||||
@@ -85,7 +85,7 @@ class User extends React.Component {
|
||||
<span className={styles.flaggedByLabel}>
|
||||
{t('community.flags')}({ user.actions.length })
|
||||
</span>:
|
||||
{ user.action_summaries.map(
|
||||
{ user.action_summaries.map(
|
||||
(action, i) => {
|
||||
return <span className={styles.flaggedBy} key={i}>
|
||||
{shortReasons[action.reason]} ({action.count})
|
||||
|
||||
@@ -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(() => {
|
||||
this.props.handleClose();
|
||||
});
|
||||
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 = [
|
||||
|
||||
@@ -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,9 +1,16 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {compose} from 'react-apollo';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import FlaggedAccounts from '../containers/FlaggedAccounts';
|
||||
import FlaggedUser from '../containers/FlaggedUser';
|
||||
|
||||
import {withSetUserStatus, withRejectUsername} from 'coral-framework/graphql/mutations';
|
||||
import {getDefinitionName} from 'coral-framework/utils';
|
||||
|
||||
import {
|
||||
fetchAccounts,
|
||||
updateSorting,
|
||||
@@ -20,15 +27,57 @@ class CommunityContainer extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Community {...this.props} />
|
||||
);
|
||||
return <Community
|
||||
fetchAccounts={this.props.fetchAccounts}
|
||||
community={this.props.community}
|
||||
hideRejectUsernameDialog={this.props.hideRejectUsernameDialog}
|
||||
updateSorting={this.props.updateSorting}
|
||||
newPage={this.props.newPage}
|
||||
route={this.props.route}
|
||||
rejectUsername={this.props.rejectUsername}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
const mapStateToProps = (state) => ({
|
||||
community: state.community,
|
||||
});
|
||||
|
||||
CommunityContainer.propTypes = {
|
||||
community: PropTypes.object,
|
||||
fetchAccounts: PropTypes.func,
|
||||
hideRejectUsernameDialog: PropTypes.func,
|
||||
updateSorting: PropTypes.func,
|
||||
newPage: PropTypes.func,
|
||||
route: PropTypes.object,
|
||||
rejectUsername: PropTypes.func,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object
|
||||
};
|
||||
|
||||
const 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,
|
||||
@@ -41,4 +90,5 @@ export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withSetUserStatus,
|
||||
withRejectUsername,
|
||||
withData,
|
||||
)(CommunityContainer);
|
||||
|
||||
@@ -2,8 +2,9 @@ import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import {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';
|
||||
@@ -24,7 +25,10 @@ class FlaggedAccountsContainer extends Component {
|
||||
}
|
||||
|
||||
approveUser = ({userId}) => {
|
||||
return this.props.setUserStatus({userId, status: 'APPROVED'});
|
||||
return this.props.setUserStatus({
|
||||
userId,
|
||||
status: 'APPROVED'
|
||||
});
|
||||
}
|
||||
|
||||
loadMore = () => {
|
||||
@@ -74,6 +78,16 @@ class FlaggedAccountsContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
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}){
|
||||
@@ -88,31 +102,6 @@ const LOAD_MORE_QUERY = gql`
|
||||
${FlaggedUser.fragments.user}
|
||||
`;
|
||||
|
||||
export const withFlaggedAccountsyQuery = withQuery(gql`
|
||||
query TalkAdmin_FlaggedAccounts {
|
||||
...${getDefinitionName(FlaggedUser.fragments.root)}
|
||||
users(query:{action_type: FLAG, statuses: [PENDING], limit: 10}){
|
||||
hasNextPage
|
||||
endCursor
|
||||
nodes {
|
||||
__typename
|
||||
...${getDefinitionName(FlaggedUser.fragments.user)}
|
||||
}
|
||||
}
|
||||
me {
|
||||
__typename
|
||||
...${getDefinitionName(FlaggedUser.fragments.me)}
|
||||
}
|
||||
}
|
||||
${FlaggedUser.fragments.root}
|
||||
${FlaggedUser.fragments.user}
|
||||
${FlaggedUser.fragments.me}
|
||||
`, {
|
||||
options: {
|
||||
fetchPolicy: 'network-only',
|
||||
},
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
showBanUserDialog,
|
||||
@@ -123,6 +112,23 @@ const mapDispatchToProps = (dispatch) =>
|
||||
|
||||
export default compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
withFlaggedAccountsyQuery,
|
||||
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);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, {Component} from 'react';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {join} from 'path';
|
||||
import join from 'url-join';
|
||||
import styles from './Configure.css';
|
||||
import {Button, Card} from 'coral-ui';
|
||||
import {BASE_URL} from 'coral-framework/constants/url';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,7 +64,6 @@ 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);
|
||||
|
||||
const selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
@@ -62,7 +62,7 @@ const StorySearch = (props) => {
|
||||
{
|
||||
loading
|
||||
? <Spinner />
|
||||
: assets.map((story, i) => {
|
||||
: assets.nodes.map((story, i) => {
|
||||
const storyOpen = story.closedAt === null || new Date(story.closedAt) > new Date();
|
||||
|
||||
return <Story
|
||||
@@ -77,7 +77,7 @@ const StorySearch = (props) => {
|
||||
})
|
||||
}
|
||||
|
||||
{assets.length === 0 && <div className={styles.noResults}>No results</div>}
|
||||
{assets.nodes.length === 0 && <div className={styles.noResults}>No results</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -186,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;
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
@@ -214,7 +215,7 @@ class ModerationContainer extends Component {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const premodEnabled = assetId ? isPremod(asset.settings.moderation) :
|
||||
const premodEnabled = assetId ? isPremod(asset.settings.moderation) :
|
||||
isPremod(settings.moderation);
|
||||
|
||||
const currentQueueConfig = Object.assign({}, this.props.queueConfig);
|
||||
@@ -293,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)}
|
||||
}
|
||||
@@ -319,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,
|
||||
@@ -333,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,
|
||||
@@ -361,6 +362,7 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql`
|
||||
asset_id: id,
|
||||
sortOrder: props.moderation.sortOrder,
|
||||
allAssets: id === null,
|
||||
nullStatuses: null,
|
||||
},
|
||||
fetchPolicy: 'network-only'
|
||||
};
|
||||
|
||||
@@ -89,12 +89,14 @@ class StorySearchContainer extends React.Component {
|
||||
export const withAssetSearchQuery = withQuery(gql`
|
||||
query SearchStories($value: String = "") {
|
||||
assets(query: {value: $value, limit: 10}) {
|
||||
id
|
||||
title
|
||||
url
|
||||
created_at
|
||||
closedAt
|
||||
author
|
||||
nodes {
|
||||
id
|
||||
title
|
||||
url
|
||||
created_at
|
||||
closedAt
|
||||
author
|
||||
}
|
||||
}
|
||||
}
|
||||
`, {
|
||||
|
||||
@@ -13,7 +13,7 @@ export default {
|
||||
},
|
||||
reported: {
|
||||
action_type: 'FLAG',
|
||||
statuses: ['NONE', 'PREMOD'],
|
||||
statuses: ['NONE', 'PREMOD', 'SYSTEM_WITHHELD'],
|
||||
icon: 'flag',
|
||||
name: t('modqueue.reported'),
|
||||
},
|
||||
@@ -28,7 +28,6 @@ export default {
|
||||
name: t('modqueue.rejected'),
|
||||
},
|
||||
all: {
|
||||
statuses: ['NONE', 'PREMOD', 'ACCEPTED', 'REJECTED'],
|
||||
icon: 'question_answer',
|
||||
name: t('modqueue.all'),
|
||||
},
|
||||
|
||||
@@ -50,17 +50,22 @@ export default class Stories extends Component {
|
||||
return `${d.getMonth() + 1}/${d.getDate()}/${d.getFullYear()}`;
|
||||
}
|
||||
|
||||
onStatusClick = (closeStream, id, statusMenuOpen) => () => {
|
||||
onStatusClick = (closeStream, id, statusMenuOpen) => async () => {
|
||||
if (statusMenuOpen) {
|
||||
this.setState((prev) => {
|
||||
prev.statusMenus[id] = false;
|
||||
return prev;
|
||||
});
|
||||
this.props.updateAssetState(id, closeStream ? Date.now() : null)
|
||||
.then(() => {
|
||||
const {search, sort, filter, page} = this.state;
|
||||
this.props.fetchAssets(page, limit, search, sort, filter);
|
||||
});
|
||||
|
||||
try {
|
||||
await this.props.updateAssetState(id, closeStream ? Date.now() : null);
|
||||
const {search, sort, filter, page} = this.state;
|
||||
this.props.fetchAssets(page, limit, search, sort, filter);
|
||||
} catch (err) {
|
||||
|
||||
// TODO: handle error.
|
||||
console.error(err);
|
||||
}
|
||||
} else {
|
||||
this.setState((prev) => {
|
||||
prev.statusMenus[id] = true;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import LinkifyIt from 'linkify-it';
|
||||
import tlds from 'tlds';
|
||||
|
||||
export function createLinkify() {
|
||||
const linkify = new LinkifyIt();
|
||||
linkify.tlds(tlds);
|
||||
return linkify;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export function getCommentType(comment) {
|
||||
let commentType = '';
|
||||
if (comment.status === 'PREMOD') {
|
||||
commentType = 'premod';
|
||||
} else if (comment.actions && comment.actions.some((a) => a.__typename === 'FlagAction')) {
|
||||
commentType = 'flagged';
|
||||
}
|
||||
return commentType;
|
||||
}
|
||||
@@ -1,4 +1,14 @@
|
||||
import LinkifyIt from 'linkify-it';
|
||||
import tlds from 'tlds';
|
||||
const linkify = new LinkifyIt();
|
||||
linkify.tlds(tlds);
|
||||
|
||||
export function matchLinks(text) {
|
||||
return linkify.match(text);
|
||||
}
|
||||
|
||||
export const isPremod = (mod) => mod === 'PRE';
|
||||
|
||||
export const getModPath = (type = 'all', assetId) =>
|
||||
assetId ? `/admin/moderate/${type}/${assetId}` : `/admin/moderate/${type}`;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PureComponent, PropTypes} from 'react';
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import marked from 'marked';
|
||||
|
||||
const renderer = new marked.Renderer();
|
||||
|
||||
@@ -18,7 +18,7 @@ import {getEditableUntilDate} from './util';
|
||||
import {findCommentWithId} from '../graphql/utils';
|
||||
import CommentContent from './CommentContent';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
|
||||
import CommentTombstone from './CommentTombstone';
|
||||
import InactiveCommentLabel from './InactiveCommentLabel';
|
||||
import {EditableCommentContent} from './EditableCommentContent';
|
||||
import {getActionSummary, iPerformedThisAction, forEachError, isCommentActive, getShallowChanges} from 'coral-framework/utils';
|
||||
@@ -209,6 +209,10 @@ export default class Comment extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
commentIsRejected(comment) {
|
||||
return comment.status === 'REJECTED';
|
||||
}
|
||||
|
||||
commentIsIgnored(comment) {
|
||||
const me = this.props.root.me;
|
||||
return (
|
||||
@@ -338,8 +342,12 @@ export default class Comment extends React.Component {
|
||||
commentClassNames = []
|
||||
} = this.props;
|
||||
|
||||
if (!highlighted && this.commentIsRejected(comment)) {
|
||||
return <CommentTombstone action='reject' />;
|
||||
}
|
||||
|
||||
if (this.commentIsIgnored(comment)) {
|
||||
return <IgnoredCommentTombstone />;
|
||||
return <CommentTombstone action='ignore' />;
|
||||
}
|
||||
|
||||
const view = this.getVisibileReplies();
|
||||
|
||||
+17
-4
@@ -2,8 +2,21 @@ import React from 'react';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
// Render in place of a Comment when the author of the comment is ignored
|
||||
class IgnoredCommentTombstone extends React.Component {
|
||||
// Render in place of a Comment when the author of the comment is <action>
|
||||
class CommentTombstone extends React.Component {
|
||||
getCopy() {
|
||||
const {action} = this.props;
|
||||
|
||||
switch (action) {
|
||||
case 'ignore':
|
||||
return t('framework.comment_is_ignored');
|
||||
case 'reject':
|
||||
return t('framework.comment_is_rejected');
|
||||
default :
|
||||
return t('framework.comment_is_hidden');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
@@ -14,11 +27,11 @@ class IgnoredCommentTombstone extends React.Component {
|
||||
padding: '1em',
|
||||
color: '#3E4F71',
|
||||
}}>
|
||||
{t('framework.comment_is_ignored')}
|
||||
{this.getCopy()}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default IgnoredCommentTombstone;
|
||||
export default CommentTombstone;
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
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 {notifyForNewCommentStatus} from 'talk-plugin-commentbox/CommentBox';
|
||||
import {CommentForm} from 'talk-plugin-commentbox/CommentForm';
|
||||
import styles from './Comment.css';
|
||||
@@ -38,10 +39,10 @@ export class EditableCommentContent extends React.Component {
|
||||
maxCharCount: PropTypes.number,
|
||||
|
||||
// edit a comment, passed {{ body }}
|
||||
editComment: React.PropTypes.func,
|
||||
editComment: PropTypes.func,
|
||||
|
||||
// called when editing should be stopped
|
||||
stopEditing: React.PropTypes.func,
|
||||
stopEditing: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Stream from '../containers/Stream';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
@@ -90,8 +91,8 @@ export default class Embed extends React.Component {
|
||||
}
|
||||
|
||||
Embed.propTypes = {
|
||||
data: React.PropTypes.shape({
|
||||
loading: React.PropTypes.bool,
|
||||
error: React.PropTypes.object
|
||||
data: PropTypes.shape({
|
||||
loading: PropTypes.bool,
|
||||
error: PropTypes.object
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Comment.css';
|
||||
import {Button} from 'coral-ui';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import styles from './InactiveCommentLabel.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Button} from 'coral-ui';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
@@ -160,7 +160,7 @@ class Stream extends React.Component {
|
||||
loading={loading}
|
||||
appendTabs={
|
||||
<Tab tabId={'all'} key='all'>
|
||||
All Comments <TabCount active={activeStreamTab === 'all'} sub>{totalCommentCount}</TabCount>
|
||||
{t('stream.all_comments')} <TabCount active={activeStreamTab === 'all'} sub>{totalCommentCount}</TabCount>
|
||||
</Tab>
|
||||
}
|
||||
appendTabPanes={
|
||||
@@ -201,7 +201,7 @@ class Stream extends React.Component {
|
||||
data,
|
||||
root,
|
||||
appendItemArray,
|
||||
root: {asset, asset: {comment: highlightedComment, comments}},
|
||||
root: {asset, asset: {comment: highlightedComment}},
|
||||
postComment,
|
||||
notify,
|
||||
updateItem,
|
||||
@@ -222,9 +222,8 @@ class Stream extends React.Component {
|
||||
const slotProps = {data};
|
||||
const slotQueryData = {root, asset};
|
||||
|
||||
if (!highlightedComment && !comments) {
|
||||
console.error('Talk: No comments came back from the graph given that query. Please, check the query params.');
|
||||
return <StreamError />;
|
||||
if (highlightedComment === null) {
|
||||
return <StreamError>{t('stream.comment_not_found')}</StreamError>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -308,7 +307,7 @@ Stream.propTypes = {
|
||||
postComment: PropTypes.func.isRequired,
|
||||
|
||||
// edit a comment, passed (id, asset_id, { body })
|
||||
editComment: React.PropTypes.func
|
||||
editComment: PropTypes.func
|
||||
};
|
||||
|
||||
export default Stream;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React from 'react';
|
||||
import styles from './StreamError.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const StreamError = () => (
|
||||
export const StreamError = ({children}) => (
|
||||
<div className={styles.streamError}>
|
||||
{t('common.error')}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,8 +15,8 @@ class StreamTabPanel extends React.Component {
|
||||
{loading
|
||||
? <div className={styles.spinnerContainer}><Spinner /></div>
|
||||
: <TabContent activeTab={activeTab} sub={sub}>
|
||||
{tabPanes}
|
||||
</TabContent>
|
||||
{tabPanes}
|
||||
</TabContent>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import styles from './SuspendAccount.css';
|
||||
import {Button} from 'coral-ui';
|
||||
|
||||
@@ -92,12 +92,6 @@ const singleCommentFragment = gql`
|
||||
const withCommentFragments = withFragments({
|
||||
root: gql`
|
||||
fragment CoralEmbedStream_Comment_root on RootQuery {
|
||||
me {
|
||||
ignoredUsers {
|
||||
id
|
||||
}
|
||||
}
|
||||
__typename
|
||||
me {
|
||||
ignoredUsers {
|
||||
id
|
||||
|
||||
@@ -215,6 +215,6 @@ const mapDispatchToProps = (dispatch) =>
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
branch((props) => !props.auth.checkedInitialLogin && props.config, renderComponent(Spinner)),
|
||||
branch((props) => !props.auth.checkedInitialLogin, renderComponent(Spinner)),
|
||||
withEmbedQuery,
|
||||
)(EmbedContainer);
|
||||
|
||||
@@ -51,7 +51,7 @@ class StreamContainer extends React.Component {
|
||||
return prev;
|
||||
}
|
||||
|
||||
if (['PREMOD', 'REJECTED'].includes(commentEdited.status)) {
|
||||
if (['PREMOD', 'REJECTED', 'SYSTEM_WITHHELD'].includes(commentEdited.status)) {
|
||||
return removeCommentFromEmbedQuery(prev, commentEdited.id);
|
||||
}
|
||||
},
|
||||
@@ -178,7 +178,7 @@ class StreamContainer extends React.Component {
|
||||
|
||||
render() {
|
||||
if (!this.props.root.asset
|
||||
|| !this.props.root.asset.comment
|
||||
|| this.props.root.asset.comment === undefined
|
||||
&& !this.props.root.asset.comments
|
||||
) {
|
||||
return <Spinner />;
|
||||
@@ -302,7 +302,7 @@ const fragments = {
|
||||
questionBoxEnable
|
||||
questionBoxContent
|
||||
questionBoxIcon
|
||||
closeTimeout
|
||||
closedTimeout
|
||||
closedMessage
|
||||
charCountEnable
|
||||
charCount
|
||||
|
||||
@@ -26,9 +26,9 @@ class StreamTabPanelContainer extends React.Component {
|
||||
// it does not result in a change of slot children.
|
||||
const changes = getShallowChanges(this.props, next);
|
||||
if (changes.length === 1 && changes[0] === 'reduxState') {
|
||||
const prevUuid = this.getSlotComponents(this.props.tabSlot, this.props).map((cmp) => cmp.talkUuid);
|
||||
const nextUuid = this.getSlotComponents(next.tabSlot, next).map((cmp) => cmp.talkUuid);
|
||||
return !isEqual(prevUuid, nextUuid);
|
||||
const prevKeys = this.getSlotElements(this.props.tabSlot, this.props).map((el) => el.key);
|
||||
const nextKeys = this.getSlotElements(next.tabSlot, next).map((el) => el.key);
|
||||
return !isEqual(prevKeys, nextKeys);
|
||||
}
|
||||
|
||||
// Prevent Slot from rerendering when no props has shallowly changed.
|
||||
@@ -37,42 +37,33 @@ class StreamTabPanelContainer extends React.Component {
|
||||
|
||||
fallbackAllTab(props = this.props) {
|
||||
if (props.activeTab !== props.fallbackTab) {
|
||||
const slotPlugins = this.getSlotComponents(props.tabSlot, props).map((c) => c.talkPluginName);
|
||||
const slotPlugins = this.getSlotElements(props.tabSlot, props).map((el) => el.type.talkPluginName);
|
||||
if (slotPlugins.indexOf(props.activeTab) === -1) {
|
||||
props.setActiveTab(props.fallbackTab);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getSlotComponents(slot, props = this.props) {
|
||||
getSlotElements(slot, props = this.props) {
|
||||
const {plugins} = this.context;
|
||||
return plugins.getSlotComponents(slot, props.reduxState, props.slotProps, props.queryData);
|
||||
return plugins.getSlotElements(slot, props.reduxState, props.slotProps, props.queryData);
|
||||
}
|
||||
|
||||
getPluginTabElements(props = this.props) {
|
||||
const {plugins} = this.context;
|
||||
return this.getSlotComponents(props.tabSlot).map((PluginComponent) => {
|
||||
const pluginProps = plugins.getSlotComponentProps(PluginComponent, props.reduxState, props.slotProps, props.queryData);
|
||||
return this.getSlotElements(props.tabSlot).map((el) => {
|
||||
return (
|
||||
<Tab tabId={PluginComponent.talkPluginName} key={PluginComponent.talkPluginName}>
|
||||
<PluginComponent
|
||||
{...pluginProps}
|
||||
active={this.props.activeTab === PluginComponent.talkPluginName}
|
||||
/>
|
||||
<Tab tabId={el.type.talkPluginName} key={el.type.talkPluginName}>
|
||||
{React.cloneElement(el, {active: this.props.activeTab === el.type.talkPluginName})}
|
||||
</Tab>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
getPluginTabPaneElements(props = this.props) {
|
||||
const {plugins} = this.context;
|
||||
return this.getSlotComponents(props.tabPaneSlot).map((PluginComponent) => {
|
||||
const pluginProps = plugins.getSlotComponentProps(PluginComponent, props.reduxState, props.slotProps, props.queryData);
|
||||
return this.getSlotElements(props.tabPaneSlot).map((el) => {
|
||||
return (
|
||||
<TabPane tabId={PluginComponent.talkPluginName} key={PluginComponent.talkPluginName}>
|
||||
<PluginComponent
|
||||
{...pluginProps}
|
||||
/>
|
||||
<TabPane tabId={el.type.talkPluginName} key={el.type.talkPluginName}>
|
||||
{el}
|
||||
</TabPane>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -64,6 +64,13 @@ export default {
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
actions {
|
||||
__typename
|
||||
... on FlagAction {
|
||||
reason
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment CoralEmbedStream_CreateCommentResponse_Comment on Comment {
|
||||
@@ -109,12 +116,14 @@ export default {
|
||||
},
|
||||
mutations: {
|
||||
PostComment: ({
|
||||
variables: {comment: {asset_id, body, parent_id, tags = []}},
|
||||
variables: {input: {asset_id, body, parent_id, tags = []}},
|
||||
state: {auth},
|
||||
}) => ({
|
||||
optimisticResponse: {
|
||||
createComment: {
|
||||
__typename: 'CreateCommentResponse',
|
||||
errors: null,
|
||||
actions: [],
|
||||
comment: {
|
||||
__typename: 'Comment',
|
||||
user: {
|
||||
@@ -166,7 +175,7 @@ export default {
|
||||
},
|
||||
updateQueries: {
|
||||
CoralEmbedStream_Embed: (prev, {mutationResult: {data: {createComment: {comment}}}}) => {
|
||||
if (prev.asset.settings.moderation === 'PRE' || comment.status === 'PREMOD' || comment.status === 'REJECTED') {
|
||||
if (prev.asset.settings.moderation === 'PRE' || comment.status === 'PREMOD' || comment.status === 'REJECTED' || comment.status === 'SYSTEM_WITHHELD') {
|
||||
return prev;
|
||||
}
|
||||
return insertCommentIntoEmbedQuery(prev, comment);
|
||||
@@ -185,7 +194,7 @@ export default {
|
||||
EditComment: () => ({
|
||||
updateQueries: {
|
||||
CoralEmbedStream_Embed: (prev, {mutationResult: {data: {editComment: {comment}}}}) => {
|
||||
if (!['PREMOD', 'REJECTED'].includes(comment.status)) {
|
||||
if (!['PREMOD', 'REJECTED', 'SYSTEM_WITHHELD'].includes(comment.status)) {
|
||||
return null;
|
||||
}
|
||||
return removeCommentFromEmbedQuery(prev, comment.id);
|
||||
|
||||
@@ -10,11 +10,6 @@ import reducers from './reducers';
|
||||
import TalkProvider from 'coral-framework/components/TalkProvider';
|
||||
import pluginsConfig from 'pluginsConfig';
|
||||
|
||||
const context = createContext({reducers, graphqlExtension, pluginsConfig});
|
||||
|
||||
// TODO: move init code into `bootstrap` service after auth has been refactored.
|
||||
const {store, pym} = context;
|
||||
|
||||
function inIframe() {
|
||||
try {
|
||||
return window.self !== window.top;
|
||||
@@ -23,37 +18,45 @@ function inIframe() {
|
||||
}
|
||||
}
|
||||
|
||||
function init(config = {}) {
|
||||
store.dispatch(addExternalConfig(config));
|
||||
store.dispatch(checkLogin());
|
||||
}
|
||||
// TODO: move init code into `bootstrap` service after auth has been refactored.
|
||||
function preInit({store, pym}) {
|
||||
|
||||
// Don't run this in the popup.
|
||||
if (!window.opener) {
|
||||
if (inIframe()) {
|
||||
// TODO: This is popup specific code and needs to be refactored.
|
||||
if (!inIframe()) {
|
||||
store.dispatch(addExternalConfig({}));
|
||||
store.dispatch(checkLogin());
|
||||
return;
|
||||
}
|
||||
|
||||
pym.onMessage('login', (token) => {
|
||||
if (token) {
|
||||
store.dispatch(handleAuthToken(token));
|
||||
}
|
||||
store.dispatch(checkLogin());
|
||||
});
|
||||
|
||||
pym.onMessage('logout', () => {
|
||||
store.dispatch(logout());
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
pym.sendMessage('getConfig');
|
||||
pym.onMessage('config', (config) => {
|
||||
init(JSON.parse(config));
|
||||
});
|
||||
|
||||
pym.onMessage('login', (token) => {
|
||||
if (token) {
|
||||
store.dispatch(handleAuthToken(token));
|
||||
}
|
||||
store.dispatch(addExternalConfig(JSON.parse(config)));
|
||||
store.dispatch(checkLogin());
|
||||
resolve();
|
||||
});
|
||||
|
||||
pym.onMessage('logout', () => {
|
||||
store.dispatch(logout());
|
||||
});
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render(
|
||||
<TalkProvider {...context}>
|
||||
<AppRouter />
|
||||
</TalkProvider>
|
||||
, document.querySelector('#talk-embed-stream-container')
|
||||
);
|
||||
async function main() {
|
||||
const context = await createContext({reducers, graphqlExtension, pluginsConfig, preInit});
|
||||
render(
|
||||
<TalkProvider {...context}>
|
||||
<AppRouter />
|
||||
</TalkProvider>
|
||||
, document.querySelector('#talk-embed-stream-container')
|
||||
);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
@@ -3,6 +3,7 @@ import pym from 'pym.js';
|
||||
import EventEmitter from 'eventemitter2';
|
||||
import {buildUrl} from 'coral-framework/utils/url';
|
||||
import Snackbar from './Snackbar';
|
||||
import {createStorage, connectStorageToPym} from 'coral-framework/services/storage';
|
||||
|
||||
const NOTIFICATION_OFFSET = 200;
|
||||
|
||||
@@ -134,6 +135,9 @@ export default class Stream {
|
||||
|
||||
// If the user clicks outside the embed, then tell the embed.
|
||||
document.addEventListener('click', this.handleClick.bind(this), true);
|
||||
|
||||
// Listens to storage requests on pym and relay it to local storage.
|
||||
connectStorageToPym(createStorage(), this.pym);
|
||||
}
|
||||
|
||||
login(token) {
|
||||
|
||||
@@ -83,7 +83,7 @@ Talk.render = (el, opts) => {
|
||||
console.warn(
|
||||
'This page does not include a canonical link tag. Talk has inferred this asset_url from the window object. Query params have been stripped, which may cause a single thread to be present across multiple pages.'
|
||||
);
|
||||
|
||||
|
||||
if (!window.location.origin) {
|
||||
window.location.origin = `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}`;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {PureComponent, PropTypes} from 'react';
|
||||
import React, {PureComponent} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import marked from 'marked';
|
||||
|
||||
const renderer = new marked.Renderer();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import SimpleMDE from 'simplemde';
|
||||
import cn from 'classnames';
|
||||
import noop from 'lodash/noop';
|
||||
|
||||
@@ -20,9 +20,9 @@ class Slot extends React.Component {
|
||||
// it does not result in a change of slot children.
|
||||
const changes = getShallowChanges(this.props, next);
|
||||
if (changes.length === 1 && changes[0] === 'reduxState') {
|
||||
const prevChildrenUuid = this.getChildren(this.props).map((child) => child.type.talkUuid);
|
||||
const nextChildrenUuid = this.getChildren(next).map((child) => child.type.talkUuid);
|
||||
return !isEqual(prevChildrenUuid, nextChildrenUuid);
|
||||
const prevChildrenKeys = this.getChildren(this.props).map((child) => child.key);
|
||||
const nextChildrenKeys = this.getChildren(next).map((child) => child.key);
|
||||
return !isEqual(prevChildrenKeys, nextChildrenKeys);
|
||||
}
|
||||
|
||||
// Prevent Slot from rerendering when no props has shallowly changed.
|
||||
|
||||
@@ -6,6 +6,7 @@ export default {
|
||||
'SetCommentStatusResponse',
|
||||
'SuspendUserResponse',
|
||||
'RejectUsernameResponse',
|
||||
'CreateCommentResponse',
|
||||
'SetUserStatusResponse',
|
||||
'CreateFlagResponse',
|
||||
'EditCommentResponse',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user