mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
950efba3fe | ||
|
|
af592e326f | ||
|
|
c2aa2077f4 | ||
|
|
26476f42f7 | ||
|
|
3434fb4fed | ||
|
|
a6bc29af9a | ||
|
|
d86ff8418d | ||
|
|
e71bcbe58d | ||
|
|
77c838ea22 | ||
|
|
39313a20fa | ||
|
|
ad4427286d | ||
|
|
dc82db6eda | ||
|
|
4bb2b3d731 | ||
|
|
d812a0f696 | ||
|
|
ba11eb233a | ||
|
|
e76d109f93 | ||
|
|
78db4077e1 | ||
|
|
0519b55707 | ||
|
|
5602bf222c | ||
|
|
bfd6cfbaa4 | ||
|
|
9ec7027f3c | ||
|
|
4acb6aeb6c | ||
|
|
c3bccaf4a8 | ||
|
|
e162a97919 | ||
|
|
f63a7915ea | ||
|
|
18710e2345 | ||
|
|
ff29fa04e5 | ||
|
|
7a9023744a | ||
|
|
b1c03973e6 | ||
|
|
9e4759cbfd | ||
|
|
94c98f000c | ||
|
|
0b2cc29b4a | ||
|
|
0efa419b35 | ||
|
|
c2154b6efa | ||
|
|
cfd52144f4 | ||
|
|
239377dd7f | ||
|
|
c46b29aed9 | ||
|
|
43e02e1574 | ||
|
|
ffd88e0ef0 | ||
|
|
9820e575b2 | ||
|
|
1e329c8967 | ||
|
|
73034a20f0 | ||
|
|
8f7171b54e | ||
|
|
b6e4581ba2 | ||
|
|
78444a0bb4 | ||
|
|
4d7e64e1ec | ||
|
|
b8e340c471 | ||
|
|
39c1fcf83a | ||
|
|
4b2d9fc93a | ||
|
|
f56b8ca3a4 | ||
|
|
0a324a2268 | ||
|
|
5f5f0ffa14 | ||
|
|
fc27e02140 | ||
|
|
f40f209395 | ||
|
|
e924ed9189 | ||
|
|
4053a25239 | ||
|
|
de094ee948 | ||
|
|
aecae3efbe | ||
|
|
67ade3a323 | ||
|
|
6d2e74eeec | ||
|
|
c7ee8307e6 | ||
|
|
eaeb7ea0da | ||
|
|
9e28be8246 | ||
|
|
431bbd4f03 | ||
|
|
0f69df26f6 | ||
|
|
c140a62822 | ||
|
|
cad25eedd1 | ||
|
|
87af05c0d9 | ||
|
|
c54a8bdc4f | ||
|
|
40963e236f | ||
|
|
d857585c7f | ||
|
|
3999b806c7 | ||
|
|
6c1ed826d0 | ||
|
|
4224f18ba7 | ||
|
|
6feaa94fba | ||
|
|
d639596047 | ||
|
|
f5409b11bf | ||
|
|
a0118751c7 | ||
|
|
6860b2b03f | ||
|
|
ec8cfdf5f7 | ||
|
|
142c5b32f1 | ||
|
|
2bca30d3c1 | ||
|
|
3dde285776 | ||
|
|
1304669416 | ||
|
|
9703bc8645 | ||
|
|
85d67dba19 | ||
|
|
ffc880c33b | ||
|
|
ae881df7f3 | ||
|
|
33dd2e5c0a | ||
|
|
dbe471fa96 | ||
|
|
afe411515d | ||
|
|
a214969527 | ||
|
|
bd16ff863d | ||
|
|
c0b0627d6a | ||
|
|
13a52e8d64 | ||
|
|
968dd14720 | ||
|
|
065af06709 | ||
|
|
b2ce217a41 | ||
|
|
e93e4756bf | ||
|
|
349c972f8b | ||
|
|
f515e22217 | ||
|
|
90c99727cb | ||
|
|
54ce58c48f | ||
|
|
175b677afd | ||
|
|
39b34d624a | ||
|
|
e9a781607a | ||
|
|
05a6af49c4 | ||
|
|
4b2aa04ef6 | ||
|
|
5481491bf2 | ||
|
|
eef55f33fc | ||
|
|
2ad5219e59 | ||
|
|
e08024a136 | ||
|
|
9f465ee7ce | ||
|
|
29a64d540b | ||
|
|
749189f0e5 | ||
|
|
41413a9e91 | ||
|
|
e65c71cae7 | ||
|
|
c7775cfdfd | ||
|
|
78473cb21f | ||
|
|
4a3bdb0633 | ||
|
|
fdafa1ca42 | ||
|
|
243f846f3b | ||
|
|
8f72ff7ca2 | ||
|
|
adf0b65fde | ||
|
|
3bb0cb9168 | ||
|
|
670084427d | ||
|
|
4b6a3b5b9d | ||
|
|
c8727e0f41 | ||
|
|
705c54f397 | ||
|
|
f4fc8915e3 | ||
|
|
1b2929af29 | ||
|
|
1774f98519 | ||
|
|
4883bb17b9 | ||
|
|
2f71e1bf8e | ||
|
|
408a082f2d | ||
|
|
37dd6ae044 | ||
|
|
2e0a4df2be | ||
|
|
6965b33968 | ||
|
|
d2235f9918 | ||
|
|
5ebb99740b | ||
|
|
2978d6138c | ||
|
|
4dbd9e7df1 | ||
|
|
686096bfae | ||
|
|
ddb80a95c1 | ||
|
|
18cc8b9389 | ||
|
|
22467ac123 | ||
|
|
d526a404ae | ||
|
|
aaad7aa86f | ||
|
|
74a2ed4a41 | ||
|
|
bad0363628 | ||
|
|
ffbcd93c3f | ||
|
|
2407cb9594 | ||
|
|
61bc372640 | ||
|
|
ee3d576a49 | ||
|
|
3c04e6c7ec | ||
|
|
52491d81a9 | ||
|
|
77723e3fb3 | ||
|
|
bba14f502a | ||
|
|
44913864eb | ||
|
|
3c66401546 | ||
|
|
2e6a7c0559 | ||
|
|
e7fc33c667 | ||
|
|
1cc484b61e | ||
|
|
17fefcb4bd | ||
|
|
f4d520fbaa | ||
|
|
f8e0556904 | ||
|
|
a5c08129ce | ||
|
|
7c859ad4c3 | ||
|
|
2b2a762623 | ||
|
|
b9cfddc5c0 | ||
|
|
7dca937de2 | ||
|
|
6cb3fc7be8 | ||
|
|
95211e6cb9 | ||
|
|
db322d192f | ||
|
|
bf98050d33 | ||
|
|
caa588f11d | ||
|
|
764ba4ab36 | ||
|
|
9c139eb703 | ||
|
|
c289ed149f | ||
|
|
05d80a0549 | ||
|
|
8bef21274d | ||
|
|
869fb9d029 | ||
|
|
cb077a87dc | ||
|
|
6d662e4469 | ||
|
|
901c96a67a | ||
|
|
6b5a690c4f | ||
|
|
9debb0d48d | ||
|
|
484ae2539b | ||
|
|
1dfce22b89 | ||
|
|
e36098369b | ||
|
|
aec0251c4f | ||
|
|
c9613337d4 | ||
|
|
ac67b0484b | ||
|
|
edac21a4f4 | ||
|
|
f75924e683 | ||
|
|
0966aa9463 | ||
|
|
30cd16fd56 | ||
|
|
86f4ae4b6d | ||
|
|
219fcdddea | ||
|
|
4464183b42 | ||
|
|
ed8d9e985c | ||
|
|
840fef84db | ||
|
|
6c9411d173 | ||
|
|
6afb300d86 | ||
|
|
894141619c | ||
|
|
7e135cef47 | ||
|
|
34726e2c4e | ||
|
|
1f6c29cee6 | ||
|
|
2234acc28e | ||
|
|
13e6645f26 | ||
|
|
09983e412d | ||
|
|
3563f7e2ae | ||
|
|
dc5a3e43a6 | ||
|
|
2e59f64738 | ||
|
|
91e08cb117 | ||
|
|
c4b6cb54e6 | ||
|
|
118301c2b4 | ||
|
|
d6ddd9c195 | ||
|
|
596c214f1c | ||
|
|
9b7f1dc43e | ||
|
|
c4cddefcff | ||
|
|
062ce58cf8 | ||
|
|
8b6a993587 | ||
|
|
26bd01cbda | ||
|
|
f492040a57 | ||
|
|
d7fd8ac714 | ||
|
|
3e273a76f1 | ||
|
|
7504dc6dfe | ||
|
|
0f9387dbb9 | ||
|
|
ec54376a53 | ||
|
|
0793784f5a | ||
|
|
00a7b11039 | ||
|
|
5138e79424 | ||
|
|
00eff653cb | ||
|
|
a59464e45d | ||
|
|
feeaa8ee8b | ||
|
|
1c4b8a0a67 | ||
|
|
8ad5675916 | ||
|
|
7a7d45cf91 | ||
|
|
e77e62236c | ||
|
|
8e142a5ecb | ||
|
|
3d57122a65 | ||
|
|
665d8c1ef0 | ||
|
|
bd2c4d4bfc | ||
|
|
81e68c8ef4 | ||
|
|
cb5a41f119 | ||
|
|
082287387a | ||
|
|
9143bd2b31 | ||
|
|
2def894237 | ||
|
|
7dd7f9ca54 | ||
|
|
b33cde03c3 | ||
|
|
d80cd6f0a2 | ||
|
|
ebb85d17ce | ||
|
|
efe84f3f98 | ||
|
|
335349963c | ||
|
|
495e3b009f | ||
|
|
2d82f4c217 | ||
|
|
6a70077398 | ||
|
|
d25ca0ef6f | ||
|
|
985fc05a47 | ||
|
|
73cbe4ab4b | ||
|
|
8d656b51a0 | ||
|
|
0a889fd1b3 | ||
|
|
c18c43e5c8 | ||
|
|
c22f8d0dc0 | ||
|
|
0799e9d8ee | ||
|
|
9645ccb6cd | ||
|
|
be51704b1f | ||
|
|
7e2d414f13 | ||
|
|
d2e87bcbda | ||
|
|
9d168c6af8 | ||
|
|
3a67768c51 | ||
|
|
e3ccb8560b | ||
|
|
c355e10453 | ||
|
|
e20e55b589 | ||
|
|
c13ea699d9 | ||
|
|
873932b6a6 | ||
|
|
bc42539fc4 | ||
|
|
5f608e9bc6 | ||
|
|
2bfa99fa1a | ||
|
|
c12fe526b8 | ||
|
|
fc3bc0a542 | ||
|
|
78019396e2 | ||
|
|
7d31487275 | ||
|
|
f5c3f7e9bc | ||
|
|
9e127d2300 | ||
|
|
172bc22af5 | ||
|
|
4be9e74ed9 | ||
|
|
f4d134d0e2 | ||
|
|
752825db68 | ||
|
|
9a36473c8f | ||
|
|
3fa2af975c | ||
|
|
57a77a7edf | ||
|
|
6caae2df97 | ||
|
|
c00e534984 | ||
|
|
b638b783fe | ||
|
|
de18ab1e15 | ||
|
|
75751285fe | ||
|
|
04390c5acd | ||
|
|
ce25cfb159 | ||
|
|
e8f73ddb87 | ||
|
|
e6f796e99c | ||
|
|
0507e750b9 | ||
|
|
fe4cddda60 | ||
|
|
63347dc0eb | ||
|
|
426025f8c2 | ||
|
|
6fd4d1576e | ||
|
|
26ca3622c2 | ||
|
|
ef7693afa5 | ||
|
|
de9383e657 | ||
|
|
1e517469c3 | ||
|
|
66934cbb82 | ||
|
|
2246b16cf4 | ||
|
|
1fc07e8649 | ||
|
|
4e9b19c7f6 | ||
|
|
a8f794ef76 | ||
|
|
6825b4c999 | ||
|
|
54fb334288 | ||
|
|
798732cbcf | ||
|
|
e18d92944a | ||
|
|
f0498fb3ba | ||
|
|
0f938b19a3 | ||
|
|
bcd18ccb32 | ||
|
|
c6dd182f56 | ||
|
|
cae974d0df | ||
|
|
c41320e155 | ||
|
|
66ca985405 | ||
|
|
a366c6efa4 | ||
|
|
366f40a6e4 | ||
|
|
9da61b5991 | ||
|
|
9feeaf4dca | ||
|
|
7f9e09461c | ||
|
|
d15694041a | ||
|
|
59cdc9fd5d | ||
|
|
b196bb379a | ||
|
|
c7242f5d98 | ||
|
|
ace6cbb158 | ||
|
|
0523faee2b | ||
|
|
e4834027b0 | ||
|
|
303e2e84c5 | ||
|
|
04fd056cab | ||
|
|
74b545a8d9 | ||
|
|
957fd34168 | ||
|
|
dd836ad28e | ||
|
|
196ae5e29c | ||
|
|
f4e4c1d810 | ||
|
|
f3469f3dc4 | ||
|
|
d101989ebd | ||
|
|
b36c70acc1 | ||
|
|
25359ee88d | ||
|
|
6a06cb4684 | ||
|
|
12652d59c4 | ||
|
|
8e8458781d | ||
|
|
852b21e264 | ||
|
|
81d049e533 | ||
|
|
3fe8a8fb8d | ||
|
|
0f6cc20478 | ||
|
|
26c109e8ae | ||
|
|
1e34af6aa3 | ||
|
|
60c62e4f1a | ||
|
|
30dddf387c | ||
|
|
0528106fdf | ||
|
|
bdd207524c | ||
|
|
4be19fd028 | ||
|
|
66202d1fd6 | ||
|
|
d98a76272c | ||
|
|
15d0ecbe51 | ||
|
|
ca653fd5ff | ||
|
|
468a1683ce | ||
|
|
d827ebef72 | ||
|
|
65588f234a | ||
|
|
bb2ea77008 | ||
|
|
392c5511eb | ||
|
|
a9be049bf6 | ||
|
|
bf752a50ca | ||
|
|
a402817cc9 | ||
|
|
669eb360df | ||
|
|
25dd979a37 | ||
|
|
e653a5f9c0 | ||
|
|
5d26f23b7b | ||
|
|
e2a361276b | ||
|
|
319bca3ba8 | ||
|
|
02ca59638a | ||
|
|
b2cf5efed6 | ||
|
|
7258c3dd08 | ||
|
|
4bbd561a1b | ||
|
|
49d20db26f | ||
|
|
14d035adb1 | ||
|
|
844ce5dbfe | ||
|
|
8f1d9f051e | ||
|
|
a111439541 | ||
|
|
72331699b5 | ||
|
|
544ef081dc | ||
|
|
ddf3816f5a | ||
|
|
7d022ed380 | ||
|
|
40539bf3b7 | ||
|
|
c1a26d2e71 | ||
|
|
6f55189658 | ||
|
|
3c9b4bd20e | ||
|
|
2a6543bcea | ||
|
|
716a61aa5c | ||
|
|
127f0c1e20 | ||
|
|
fa83ba9556 | ||
|
|
317541a63b | ||
|
|
fd5eb7d23d | ||
|
|
06f308c182 | ||
|
|
2345daf783 | ||
|
|
df2bce4d53 | ||
|
|
c99411b8fd | ||
|
|
022a57479d | ||
|
|
56001d7780 | ||
|
|
a010302242 | ||
|
|
f306d54924 | ||
|
|
c1ff7e9c84 | ||
|
|
3255b8d23b | ||
|
|
8ea3ab9c02 | ||
|
|
ec9a57bede | ||
|
|
5f3da9a09a | ||
|
|
dbfd0fd25e | ||
|
|
ac62cbf348 | ||
|
|
c612902779 | ||
|
|
3938b7883f | ||
|
|
cc320cf80a | ||
|
|
3c52d58a6e | ||
|
|
f282aa47c9 | ||
|
|
426d7dbfec | ||
|
|
ee78f08598 | ||
|
|
519480c0e4 | ||
|
|
45d6800143 | ||
|
|
e3cdad1f2a | ||
|
|
66cfd20f7b | ||
|
|
af40a85071 | ||
|
|
b11a730373 | ||
|
|
b709cb485e | ||
|
|
45d4c79c26 | ||
|
|
6aece16fd0 | ||
|
|
2684ac4933 | ||
|
|
1f0e9da5d0 | ||
|
|
bfa2f6edb9 | ||
|
|
ea7998257c | ||
|
|
25c178b267 | ||
|
|
d69b1f98c8 | ||
|
|
77d83e04d5 | ||
|
|
86c4d70daf | ||
|
|
e256b0da8a | ||
|
|
bf77ddc49b | ||
|
|
ebe6341b01 | ||
|
|
34a0e314e8 | ||
|
|
951f16f0a1 | ||
|
|
988b39f19d | ||
|
|
9333b407bb | ||
|
|
8867fd54f3 | ||
|
|
56bd3041d6 | ||
|
|
a53bb6650a | ||
|
|
0a99ae50b1 | ||
|
|
6bad3fdc2f | ||
|
|
e5c3474455 | ||
|
|
1f2caada3f | ||
|
|
66f068359f |
+24
-22
@@ -1,32 +1,34 @@
|
||||
**/*.html
|
||||
dist
|
||||
docs
|
||||
**/*.html
|
||||
node_modules
|
||||
plugins/*
|
||||
!plugins/talk-plugin-facebook-auth
|
||||
public
|
||||
|
||||
!plugins/talk-plugin-akismet
|
||||
!plugins/talk-plugin-auth
|
||||
!plugins/talk-plugin-respect
|
||||
!plugins/talk-plugin-offtopic
|
||||
!plugins/talk-plugin-like
|
||||
!plugins/talk-plugin-mod
|
||||
!plugins/talk-plugin-love
|
||||
!plugins/talk-plugin-viewing-options
|
||||
!plugins/talk-plugin-author-menu
|
||||
!plugins/talk-plugin-comment-content
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-deep-reply-count
|
||||
!plugins/talk-plugin-facebook-auth
|
||||
!plugins/talk-plugin-featured-comments
|
||||
!plugins/talk-plugin-sort-newest
|
||||
!plugins/talk-plugin-sort-oldest
|
||||
!plugins/talk-plugin-sort-most-replied
|
||||
!plugins/talk-plugin-flag-details
|
||||
!plugins/talk-plugin-ignore-user
|
||||
!plugins/talk-plugin-like
|
||||
!plugins/talk-plugin-love
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-mod
|
||||
!plugins/talk-plugin-moderation-actions
|
||||
!plugins/talk-plugin-offtopic
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-respect
|
||||
!plugins/talk-plugin-sort-most-liked
|
||||
!plugins/talk-plugin-sort-most-loved
|
||||
!plugins/talk-plugin-sort-most-replied
|
||||
!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
|
||||
!plugins/talk-plugin-sort-newest
|
||||
!plugins/talk-plugin-sort-oldest
|
||||
!plugins/talk-plugin-subscriber
|
||||
!plugins/talk-plugin-flag-details
|
||||
public
|
||||
node_modules
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-viewing-options
|
||||
@@ -23,6 +23,7 @@ browserstack.err
|
||||
|
||||
plugins.json
|
||||
plugins/*
|
||||
!plugins/talk-plugin-akismet
|
||||
!plugins/talk-plugin-facebook-auth
|
||||
!plugins/talk-plugin-auth
|
||||
!plugins/talk-plugin-respect
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"exec": "npm-run-all --parallel generate-introspection start:development",
|
||||
"verbose": true,
|
||||
"ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"],
|
||||
"ext": "js,json,graphql,yml",
|
||||
"watch": [
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
FROM coralproject/talk:latest
|
||||
|
||||
# Setup the build arguments
|
||||
ONBUILD ARG TALK_ADDTL_COMMENTS_ON_LOAD_MORE=10
|
||||
ONBUILD ARG TALK_ASSET_COMMENTS_LOAD_DEPTH=10
|
||||
ONBUILD ARG TALK_REPLY_COMMENTS_LOAD_DEPTH=3
|
||||
ONBUILD ARG TALK_THREADING_LEVEL=3
|
||||
ONBUILD ARG TALK_DEFAULT_STREAM_TAB=all
|
||||
ONBUILD ARG TALK_DEFAULT_LANG=en
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright 2017 Mozilla Foundation
|
||||
Copyright 2018 Mozilla Foundation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -5,7 +5,6 @@ const uuid = require('uuid');
|
||||
const merge = require('lodash/merge');
|
||||
const helmet = require('helmet');
|
||||
const plugins = require('./services/plugins');
|
||||
const compression = require('compression');
|
||||
const { HELMET_CONFIGURATION } = require('./config');
|
||||
const { MOUNT_PATH } = require('./url');
|
||||
const routes = require('./routes');
|
||||
@@ -72,9 +71,6 @@ app.use(
|
||||
)
|
||||
);
|
||||
|
||||
// Compress the responses if appropriate.
|
||||
app.use(compression());
|
||||
|
||||
//==============================================================================
|
||||
// VIEW CONFIGURATION
|
||||
//==============================================================================
|
||||
|
||||
@@ -18,7 +18,6 @@ program
|
||||
.command('token', 'work with the access tokens')
|
||||
.command('users', 'work with the application auth')
|
||||
.command('migration', 'provides utilities for migrating the database')
|
||||
.command('verify', 'provides utilities for performing data verification')
|
||||
.command(
|
||||
'plugins',
|
||||
'provides utilities for interacting with the plugin system'
|
||||
@@ -41,20 +40,3 @@ if (!commands.includes(command)) {
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * When this process 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
|
||||
// * signal. This is for the use case where we want to kill the process that is
|
||||
// * labeled with the PID written out by the parent process.
|
||||
// */
|
||||
// process.once('exit', () => {
|
||||
// if (
|
||||
|
||||
// // program.runningCommand &&
|
||||
// program.runningCommand.killed === false &&
|
||||
// program.runningCommand.exitCode === null
|
||||
// ) {
|
||||
// program.runningCommand.kill('SIGINT');
|
||||
// }
|
||||
// });
|
||||
|
||||
@@ -14,6 +14,7 @@ const AssetsService = require('../services/assets');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const scraper = require('../services/scraper');
|
||||
const inquirer = require('inquirer');
|
||||
const { URL } = require('url');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
@@ -125,6 +126,70 @@ async function merge(srcID, dstID) {
|
||||
}
|
||||
}
|
||||
|
||||
async function rewrite(search, replace, options) {
|
||||
try {
|
||||
search = new RegExp(search);
|
||||
|
||||
const assets = await AssetModel.find({
|
||||
url: { $regex: search },
|
||||
});
|
||||
if (assets.length === 0) {
|
||||
console.log(`No assets found with the pattern: ${search}`);
|
||||
return util.shutdown(0);
|
||||
}
|
||||
|
||||
let opts = [];
|
||||
assets.forEach(({ id, url: oldURL }) => {
|
||||
// Replace the url.
|
||||
const newURL = oldURL.replace(search, replace);
|
||||
|
||||
// Try to validate that the new url is valid.
|
||||
try {
|
||||
new URL(newURL);
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Rewrite would have replaced the valid URL ${oldURL} with an invalid one ${newURL}`
|
||||
);
|
||||
}
|
||||
|
||||
opts.push({
|
||||
find: { id },
|
||||
updateOne: { $set: { url: newURL } },
|
||||
id,
|
||||
oldURL,
|
||||
newURL,
|
||||
});
|
||||
});
|
||||
|
||||
if (opts.length > 0) {
|
||||
if (options.dryRun) {
|
||||
const table = new Table({ head: ['ID', 'Old URL', 'New URL'] });
|
||||
|
||||
opts.forEach(({ id, oldURL, newURL }) => {
|
||||
table.push([id, oldURL, newURL]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
} else {
|
||||
const bulk = AssetModel.collection.initializeUnorderedBulkOp();
|
||||
opts.forEach(({ find, updateOne, oldURL, newURL }) => {
|
||||
// If the url was updated with the operation, then queue up the update op.
|
||||
if (newURL !== oldURL) {
|
||||
bulk.find(find).updateOne(updateOne);
|
||||
}
|
||||
});
|
||||
await bulk.execute();
|
||||
console.log(`${opts.length} assets had their url's updated`);
|
||||
}
|
||||
}
|
||||
|
||||
util.shutdown(0);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
@@ -151,6 +216,14 @@ program
|
||||
)
|
||||
.action(merge);
|
||||
|
||||
program
|
||||
.command('rewrite <search> <replace>')
|
||||
.option('-d, --dry-run', 'enables dry run of the replacement')
|
||||
.description(
|
||||
"rewrites asset url's using the provided regex replacement pattern"
|
||||
)
|
||||
.action(rewrite);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
|
||||
+59
-27
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
const util = require('./util');
|
||||
const _ = require('lodash');
|
||||
const program = require('commander');
|
||||
const inquirer = require('inquirer');
|
||||
const mongoose = require('../services/mongoose');
|
||||
@@ -25,46 +26,60 @@ async function createMigration(name) {
|
||||
}
|
||||
}
|
||||
|
||||
async function runMigrations() {
|
||||
async function runMigrations(options) {
|
||||
const { yes, queryBatchSize, updateBatchSize } = options;
|
||||
try {
|
||||
let { backedUp } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'backedUp',
|
||||
message: 'Did you perform a database backup',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
if (!yes) {
|
||||
const { backedUp } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'backedUp',
|
||||
message: 'Did you perform a database backup',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
||||
if (!backedUp) {
|
||||
throw new Error(
|
||||
'Please backup your databases prior to migrations occuring'
|
||||
);
|
||||
if (!backedUp) {
|
||||
throw new Error(
|
||||
'Please backup your databases prior to migrations occuring'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the migrations to run.
|
||||
let migrations = await MigrationService.listPending();
|
||||
const migrations = await MigrationService.listPending();
|
||||
|
||||
console.log('Now going to run the following migrations:\n');
|
||||
|
||||
for (let { filename } of migrations) {
|
||||
for (const { filename } of migrations) {
|
||||
console.log(`\tmigrations/${filename}`);
|
||||
}
|
||||
|
||||
let { confirm } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: 'Proceed with migrations',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
if (!yes) {
|
||||
const { confirm } = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: 'Proceed with migrations',
|
||||
default: false,
|
||||
},
|
||||
]);
|
||||
|
||||
if (confirm) {
|
||||
// Run the migrations.
|
||||
await MigrationService.run(migrations);
|
||||
if (confirm) {
|
||||
// Run the migrations.
|
||||
await MigrationService.run(migrations, {
|
||||
queryBatchSize,
|
||||
updateBatchSize,
|
||||
});
|
||||
} else {
|
||||
console.warn('Skipping migrations');
|
||||
}
|
||||
} else {
|
||||
console.warn('Skipping migrations');
|
||||
// Run the migrations.
|
||||
await MigrationService.run(migrations, {
|
||||
queryBatchSize,
|
||||
updateBatchSize,
|
||||
});
|
||||
}
|
||||
|
||||
util.shutdown();
|
||||
@@ -83,8 +98,25 @@ program
|
||||
.description('creates a new migration')
|
||||
.action(createMigration);
|
||||
|
||||
// Bypasses issue that defaults + coercion doesn't work well together.
|
||||
// Ref: https://github.com/tj/commander.js/issues/400#issuecomment-310860869
|
||||
const parse10 = _.ary(_.partialRight(parseInt, 10), 1);
|
||||
|
||||
program
|
||||
.command('run')
|
||||
.option(
|
||||
'-q, --query-batch-size <n>',
|
||||
'change the size of queried documents that are batched at a time',
|
||||
parse10,
|
||||
10000
|
||||
)
|
||||
.option(
|
||||
'-u, --update-batch-size <n>',
|
||||
'change the size of documents that are batched before the update is sent',
|
||||
parse10,
|
||||
20000
|
||||
)
|
||||
.option('-y, --yes', 'will answer yes to all questions')
|
||||
.description('runs all pending migrations')
|
||||
.action(runMigrations);
|
||||
|
||||
|
||||
+45
-43
@@ -15,9 +15,9 @@ const inquirer = require('inquirer');
|
||||
require('colors');
|
||||
const emoji = require('node-emoji');
|
||||
|
||||
const dir = process.cwd();
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const dir = path.resolve(__dirname, '..');
|
||||
const spawn = require('cross-spawn');
|
||||
const semver = require('semver');
|
||||
const resolve = require('resolve');
|
||||
@@ -235,7 +235,7 @@ async function reconcileLocalPlugins({ skipRemote, dryRun }) {
|
||||
|
||||
if (output.status) {
|
||||
throw new Error(
|
||||
'Could not install local plugin dependencies, errors occured during install'
|
||||
'Could not install local plugin dependencies, errors occurred during install'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -253,59 +253,61 @@ async function reconcilePluginDeps({
|
||||
dryRun,
|
||||
upgradeRemote,
|
||||
}) {
|
||||
let startTime = new Date();
|
||||
try {
|
||||
let startTime = new Date();
|
||||
|
||||
// We don't need to do anything if we skip everything....
|
||||
if (skipLocal && skipRemote) {
|
||||
return;
|
||||
}
|
||||
// We don't need to do anything if we skip everything....
|
||||
if (skipLocal && skipRemote) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Traverse local plugins and install dependencies if enabled.
|
||||
if (!skipLocal) {
|
||||
await reconcileLocalPlugins({ skipRemote, dryRun });
|
||||
}
|
||||
// Traverse local plugins and install dependencies if enabled.
|
||||
if (!skipLocal) {
|
||||
await reconcileLocalPlugins({ skipRemote, dryRun });
|
||||
}
|
||||
|
||||
// Locate any external plugins and install them.
|
||||
if (!skipRemote) {
|
||||
let results = [];
|
||||
try {
|
||||
results = await reconcileRemotePlugins({
|
||||
// Locate any external plugins and install them.
|
||||
if (!skipRemote) {
|
||||
const results = await reconcileRemotePlugins({
|
||||
skipLocal,
|
||||
skipRemote,
|
||||
dryRun,
|
||||
upgradeRemote,
|
||||
});
|
||||
} catch (e) {
|
||||
throw e;
|
||||
|
||||
let status;
|
||||
if (dryRun) {
|
||||
status = '[dry-run] success'.green;
|
||||
} else {
|
||||
status = 'success'.green;
|
||||
}
|
||||
|
||||
let message;
|
||||
if (results.upgradable.length === 0 && results.fetchable.length === 0) {
|
||||
message = 'Already up-to-date.';
|
||||
} else if (results.upgradable.length === 0) {
|
||||
message = `Fetched ${results.fetchable.length} new plugins.`;
|
||||
} else if (results.fetchable.length === 0) {
|
||||
message = `Upgraded ${results.upgradable.length} new plugins.`;
|
||||
} else {
|
||||
message = `Fetched ${results.fetchable.length} new plugins, upgraded ${
|
||||
results.upgradable.length
|
||||
} plugins.`;
|
||||
}
|
||||
|
||||
console.log(`\n${status} ${message}`);
|
||||
}
|
||||
|
||||
let status;
|
||||
if (dryRun) {
|
||||
status = '[dry-run] success'.green;
|
||||
} else {
|
||||
status = 'success'.green;
|
||||
}
|
||||
let endTime = new Date();
|
||||
|
||||
let message;
|
||||
if (results.upgradable.length === 0 && results.fetchable.length === 0) {
|
||||
message = 'Already up-to-date.';
|
||||
} else if (results.upgradable.length === 0) {
|
||||
message = `Fetched ${results.fetchable.length} new plugins.`;
|
||||
} else if (results.fetchable.length === 0) {
|
||||
message = `Upgraded ${results.upgradable.length} new plugins.`;
|
||||
} else {
|
||||
message = `Fetched ${results.fetchable.length} new plugins, upgraded ${
|
||||
results.upgradable.length
|
||||
} plugins.`;
|
||||
}
|
||||
|
||||
console.log(`\n${status} ${message}`);
|
||||
let totalTime = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(
|
||||
2
|
||||
);
|
||||
console.log(`✨ Done in ${totalTime}s.`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let endTime = new Date();
|
||||
|
||||
let totalTime = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(2);
|
||||
console.log(`✨ Done in ${totalTime}s.`);
|
||||
}
|
||||
|
||||
async function createSeedPlugin() {
|
||||
|
||||
+8
-1
@@ -13,6 +13,7 @@ const MODERATION_OPTIONS = require('../models/enum/moderation_options');
|
||||
const SettingsService = require('../services/settings');
|
||||
const SetupService = require('../services/setup');
|
||||
const UsersService = require('../services/users');
|
||||
const MigrationService = require('../services/migration');
|
||||
const errors = require('../errors');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
@@ -51,6 +52,12 @@ const performSetup = async () => {
|
||||
if (program.defaults) {
|
||||
await SettingsService.init();
|
||||
|
||||
// Get the migrations to run.
|
||||
let migrations = await MigrationService.listPending();
|
||||
|
||||
// Perform all migrations.
|
||||
await MigrationService.run(migrations);
|
||||
|
||||
console.log('Settings created.');
|
||||
console.log('\nTalk is now installed!');
|
||||
|
||||
@@ -194,7 +201,7 @@ const performSetup = async () => {
|
||||
);
|
||||
};
|
||||
|
||||
// Start tthe setup process.
|
||||
// Start the setup process.
|
||||
performSetup()
|
||||
.then(() => {
|
||||
util.shutdown();
|
||||
|
||||
+60
-9
@@ -8,6 +8,7 @@ const util = require('./util');
|
||||
const program = require('commander');
|
||||
const inquirer = require('inquirer');
|
||||
const { graphql } = require('graphql');
|
||||
const helpers = require('../services/migration/helpers');
|
||||
const { stripIndent } = require('common-tags');
|
||||
const Table = require('cli-table');
|
||||
|
||||
@@ -28,11 +29,23 @@ const CommentModel = require('../models/comment');
|
||||
const ActionModel = require('../models/action');
|
||||
const USER_ROLES = require('../models/enum/user_roles');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const databaseVerifications = require('./verifications/database');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
|
||||
/**
|
||||
* transforms a specific action to a removal action on the target model.
|
||||
*/
|
||||
const actionDecrTransformer = ({ item_id, action_type, group_id }) => ({
|
||||
query: { id: item_id },
|
||||
update: {
|
||||
$inc: {
|
||||
[`action_counts.${action_type.toLowerCase()}`]: -1,
|
||||
[`action_counts.${action_type.toLowerCase()}_${group_id.toLowerCase()}`]: -1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Deletes a user and cleans up their associated verifications.
|
||||
*/
|
||||
@@ -63,8 +76,27 @@ async function deleteUser(userID) {
|
||||
return util.shutdown();
|
||||
}
|
||||
|
||||
const { transformSingleWithCursor } = helpers({
|
||||
queryBatchSize: 10000,
|
||||
updateBatchSize: 10000,
|
||||
});
|
||||
|
||||
console.warn("Removing user's actions");
|
||||
|
||||
// Remove all actions against comments.
|
||||
await transformSingleWithCursor(
|
||||
ActionModel.collection.find({ user_id: user.id, item_type: 'COMMENTS' }),
|
||||
actionDecrTransformer,
|
||||
CommentModel
|
||||
);
|
||||
|
||||
// Remove all actions against users.
|
||||
await transformSingleWithCursor(
|
||||
ActionModel.collection.find({ user_id: user.id, item_type: 'USERS' }),
|
||||
actionDecrTransformer,
|
||||
UserModel
|
||||
);
|
||||
|
||||
// Remove all the user's actions.
|
||||
await ActionModel.where({ user_id: user.id })
|
||||
.setOptions({ multi: true })
|
||||
@@ -72,18 +104,34 @@ async function deleteUser(userID) {
|
||||
|
||||
console.warn("Removing user's comments");
|
||||
|
||||
// Removes all the user's reply counts on each of the comments that they
|
||||
// have commented on.
|
||||
await transformSingleWithCursor(
|
||||
CommentModel.collection.aggregate([
|
||||
{ $match: { author_id: user.id } },
|
||||
{
|
||||
$group: {
|
||||
_id: '$parent_id',
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
]),
|
||||
({ _id: parent_id, count }) => ({
|
||||
query: { id: parent_id },
|
||||
update: {
|
||||
$inc: {
|
||||
reply_count: -1 * count,
|
||||
},
|
||||
},
|
||||
}),
|
||||
CommentModel
|
||||
);
|
||||
|
||||
// Remove all the user's comments.
|
||||
await CommentModel.where({ author_id: user.id })
|
||||
.setOptions({ multi: true })
|
||||
.remove();
|
||||
|
||||
console.warn('Updating the database indexes');
|
||||
|
||||
// Update the counts that might have changed.
|
||||
for (const verification of databaseVerifications) {
|
||||
await verification({ fix: true, limit: Infinity, batch: 1000 });
|
||||
}
|
||||
|
||||
console.warn('Removing the user');
|
||||
|
||||
// Remove the user.
|
||||
@@ -161,7 +209,10 @@ async function searchUsers() {
|
||||
}
|
||||
|
||||
return data.users.nodes.map(user => {
|
||||
const emails = user.emails.join(', ');
|
||||
const emails = user.profiles
|
||||
.filter(({ provider }) => provider === 'local')
|
||||
.map(({ id }) => id)
|
||||
.join(', ');
|
||||
return {
|
||||
name: `${user.username} (${emails}) ${user.id.gray} - ${
|
||||
user.role.gray
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const databaseVerifications = require('./verifications/database');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
|
||||
async function database({ fix = false, limit = Infinity, batch = 1000 }) {
|
||||
try {
|
||||
for (const verification of databaseVerifications) {
|
||||
await verification({ fix, limit, batch });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Failed to process all the ${databaseVerifications.length} verifications`,
|
||||
err
|
||||
);
|
||||
util.shutdown(1);
|
||||
return;
|
||||
}
|
||||
|
||||
util.shutdown();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
program
|
||||
.command('db')
|
||||
.description('verifies the database integrity')
|
||||
.option('-f, --fix', 'fix the problems found with database inconsistencies')
|
||||
.option(
|
||||
'-l, --limit [size]',
|
||||
'limit the amount of documents to process in a single pass, this will ensure only a maximum number of batch operations are issued [default: inf]',
|
||||
parseInt
|
||||
)
|
||||
.option(
|
||||
'-b, --batch [size]',
|
||||
'batch size to process verifications and repairs of documents [default: 1000]',
|
||||
parseInt
|
||||
)
|
||||
.action(database);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.outputHelp();
|
||||
util.shutdown();
|
||||
}
|
||||
+2
-1
@@ -63,5 +63,6 @@ process.once('SIGUSR2', () => util.shutdown(0, 'SIGUSR2'));
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on('unhandledRejection', err => {
|
||||
throw err;
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
const UserModel = require('../../../models/user');
|
||||
const CommentModel = require('../../../models/comment');
|
||||
const ActionsService = require('../../../services/actions');
|
||||
const { arrayJoinBy } = require('../../../graph/loaders/util');
|
||||
const { get } = require('lodash');
|
||||
const debug = require('debug')('talk:cli:verify');
|
||||
|
||||
const MODELS = [UserModel, CommentModel];
|
||||
|
||||
async function processBatch(Model, documents) {
|
||||
// Get an array of all the document id's.
|
||||
const documentIDs = documents.map(({ id }) => id);
|
||||
|
||||
// Store all the operations on this batch in this array that we'll return
|
||||
// later.
|
||||
const operations = [];
|
||||
|
||||
// Get the action summaries for this batch.
|
||||
const totalActionSummaries = await ActionsService.getActionSummaries(
|
||||
documentIDs
|
||||
).then(arrayJoinBy(documentIDs, 'item_id'));
|
||||
|
||||
// Iterate over the documents.
|
||||
for (let i = 0; i < documents.length; i++) {
|
||||
const document = documents[i];
|
||||
const actionSummaries = totalActionSummaries[i];
|
||||
|
||||
let ops = [];
|
||||
|
||||
for (const actionSummary of actionSummaries) {
|
||||
if (actionSummary.group_id === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// And we generate the group id.
|
||||
const ACTION_TYPE = actionSummary.action_type.toLowerCase();
|
||||
const GROUP_ID = actionSummary.group_id.toLowerCase();
|
||||
|
||||
if (GROUP_ID.length <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// And we add a new batch operation if the action summary is associated
|
||||
// with a group.
|
||||
const ACTION_COUNT_FIELD = `${ACTION_TYPE}_${GROUP_ID}`;
|
||||
|
||||
// Check that the action summaries match the cached counts.
|
||||
if (
|
||||
get(document, ['action_counts', ACTION_COUNT_FIELD]) !==
|
||||
actionSummary.count
|
||||
) {
|
||||
// Batch updates for those changes.
|
||||
ops.push({
|
||||
[`action_counts.${ACTION_COUNT_FIELD}`]: actionSummary.count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Group all the action summaries together from all the different group
|
||||
// ids.
|
||||
const groupedActionSummaries = actionSummaries.reduce(
|
||||
(acc, actionSummary) => {
|
||||
// action_type is already snake cased (as it would have had to be when it
|
||||
// was inserted in the database).
|
||||
const ACTION_TYPE = actionSummary.action_type.toLowerCase();
|
||||
|
||||
if (!(ACTION_TYPE in acc)) {
|
||||
acc[ACTION_TYPE] = 0;
|
||||
}
|
||||
|
||||
acc[ACTION_TYPE] += actionSummary.count;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
for (const ACTION_COUNT_FIELD of Object.keys(groupedActionSummaries)) {
|
||||
const count = groupedActionSummaries[ACTION_COUNT_FIELD];
|
||||
|
||||
// Check that the action summaries match the cached counts.
|
||||
if (get(document, ['action_counts', ACTION_COUNT_FIELD]) !== count) {
|
||||
// Batch updates for those changes.
|
||||
ops.push({
|
||||
[`action_counts.${ACTION_COUNT_FIELD}`]: count,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If this comment has action summaries that should be updated, then
|
||||
// perform an update!
|
||||
if (ops.length > 0) {
|
||||
operations.push({
|
||||
updateOne: {
|
||||
filter: {
|
||||
id: document.id,
|
||||
},
|
||||
update: {
|
||||
$set: Object.assign({}, ...ops),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return operations;
|
||||
}
|
||||
|
||||
module.exports = async ({ fix, batch }) => {
|
||||
for (const Model of MODELS) {
|
||||
const cursor = Model.collection
|
||||
.find({})
|
||||
.project({
|
||||
id: 1,
|
||||
action_counts: 1,
|
||||
})
|
||||
.sort({ created_at: 1 });
|
||||
|
||||
let operations = [];
|
||||
let documents = [];
|
||||
|
||||
// While there are documents to process.
|
||||
while (await cursor.hasNext()) {
|
||||
// Load the document.
|
||||
const document = await cursor.next();
|
||||
|
||||
// Push the document into the documents array.
|
||||
documents.push(document);
|
||||
|
||||
// Check to see if the length of the documents array requires us to
|
||||
// process it.
|
||||
if (documents.length > batch) {
|
||||
// Process this batch.
|
||||
let batchOperations = await processBatch(Model, documents);
|
||||
|
||||
// Push the batch operations into the model operations.
|
||||
operations.push(...batchOperations);
|
||||
|
||||
// Clear this batch contents.
|
||||
documents = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Check to see if there are any documents left over.
|
||||
if (documents.length > 0) {
|
||||
// Process this batch.
|
||||
let batchOperations = await processBatch(Model, documents);
|
||||
|
||||
// Push the batch operations into the model operations.
|
||||
operations.push(...batchOperations);
|
||||
}
|
||||
|
||||
const OPERATIONS_LENGTH = operations.length;
|
||||
|
||||
console.log(
|
||||
`action_counts.js: ${OPERATIONS_LENGTH} ${
|
||||
Model.collection.name
|
||||
} need their action counts fixed.`
|
||||
);
|
||||
|
||||
// If fix was enabled, execute the batch writes.
|
||||
if (OPERATIONS_LENGTH > 0) {
|
||||
if (fix) {
|
||||
debug(
|
||||
`action_counts.js: fixing ${OPERATIONS_LENGTH} ${
|
||||
Model.collection.name
|
||||
}...`
|
||||
);
|
||||
|
||||
while (operations.length) {
|
||||
let result = await Model.collection.bulkWrite(
|
||||
operations.splice(0, batch)
|
||||
);
|
||||
|
||||
debug(
|
||||
`action_counts.js: fixed batch of ${result.modifiedCount} ${
|
||||
Model.collection.name
|
||||
}.`
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`action_counts.js: applied all ${OPERATIONS_LENGTH} fixes to ${
|
||||
Model.collection.name
|
||||
}.`
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
'Skipping fixing, --fix was not enabled, pass --fix to fix these errors'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,140 +0,0 @@
|
||||
const CommentModel = require('../../../models/comment');
|
||||
const { singleJoinBy } = require('../../../graph/loaders/util');
|
||||
const debug = require('debug')('talk:cli:verify');
|
||||
|
||||
const getBatch = async (limit, offset) =>
|
||||
CommentModel.find({})
|
||||
.select({ id: 1, action_counts: 1, reply_count: 1 })
|
||||
.limit(limit)
|
||||
.skip(offset)
|
||||
.sort('created_at');
|
||||
|
||||
module.exports = async ({ fix, limit, batch }) => {
|
||||
let operations = [];
|
||||
|
||||
// Count how many comments there are to process.
|
||||
const totalCount = await CommentModel.count();
|
||||
|
||||
let offset = 0;
|
||||
let comments = [];
|
||||
let commentIDs = [];
|
||||
|
||||
console.log(`Processing ${totalCount} comments in batches of ${limit}...`);
|
||||
|
||||
// Keep processing documents until there are is none left.
|
||||
while (offset < totalCount) {
|
||||
// Get a batch of comments.
|
||||
comments = await getBatch(batch, offset);
|
||||
commentIDs = comments.map(({ id }) => id);
|
||||
|
||||
// Get their reply counts.
|
||||
let allReplyCounts = await CommentModel.aggregate([
|
||||
{
|
||||
$match: {
|
||||
parent_id: {
|
||||
$in: commentIDs,
|
||||
},
|
||||
status: {
|
||||
$in: ['NONE', 'ACCEPTED'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$parent_id',
|
||||
count: {
|
||||
$sum: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
.then(singleJoinBy(commentIDs, '_id'))
|
||||
.then(results => results.map(result => (result ? result.count : 0)));
|
||||
|
||||
// Loop over the comments, with their action summaries.
|
||||
for (let i = 0; i < comments.length; i++) {
|
||||
let comment = comments[i];
|
||||
let replyCount = allReplyCounts[i];
|
||||
|
||||
// And check to see if the action summaries we just computed match what is
|
||||
// currently set for the comments.
|
||||
let commentOperations = [];
|
||||
|
||||
// If the reply count needs to be updated, then update it!
|
||||
if (comment.reply_count !== replyCount) {
|
||||
commentOperations.push({
|
||||
reply_count: replyCount,
|
||||
});
|
||||
}
|
||||
|
||||
// If this comment has action summaries that should be updated, then
|
||||
// perform an update!
|
||||
if (commentOperations.length > 0) {
|
||||
operations.push({
|
||||
updateOne: {
|
||||
filter: {
|
||||
id: comment.id,
|
||||
},
|
||||
update: {
|
||||
$set: Object.assign({}, ...commentOperations),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
debug(`Processed batch of ${comments.length} comments.`);
|
||||
|
||||
if (operations.length >= limit) {
|
||||
debug(
|
||||
`Queued operations are ${
|
||||
operations.length
|
||||
}, reached limit of ${limit}, not processing any more.`
|
||||
);
|
||||
|
||||
if (operations.length > limit) {
|
||||
debug(
|
||||
`${operations.length -
|
||||
limit} operations have been truncated to enforce the limit`
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
offset += batch;
|
||||
}
|
||||
|
||||
const OPERATIONS_LENGTH = operations.length;
|
||||
|
||||
if (limit < Infinity && offset + comments.length < totalCount) {
|
||||
console.log(
|
||||
`Processed ${offset +
|
||||
comments.length}/${totalCount} comments because we reached the update limit of ${limit}.`
|
||||
);
|
||||
} else {
|
||||
console.log(`Processed all ${totalCount} comments.`);
|
||||
}
|
||||
|
||||
console.log(`${OPERATIONS_LENGTH} documents need fixing.`);
|
||||
|
||||
// If fix was enabled, execute the batch writes.
|
||||
if (OPERATIONS_LENGTH > 0) {
|
||||
if (fix) {
|
||||
debug(`Fixing ${OPERATIONS_LENGTH} documents...`);
|
||||
|
||||
while (operations.length) {
|
||||
let batchOperations = operations.splice(0, batch);
|
||||
let result = await CommentModel.collection.bulkWrite(batchOperations);
|
||||
|
||||
debug(`Fixed batch of ${result.modifiedCount} documents.`);
|
||||
}
|
||||
|
||||
console.log(`Applied all ${OPERATIONS_LENGTH} fixes.`);
|
||||
} else {
|
||||
console.warn(
|
||||
'Skipping fixing, --fix was not enabled, pass --fix to fix these errors'
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
// This will import all the verifications that should be run by the:
|
||||
//
|
||||
// cli verify database
|
||||
//
|
||||
// command. They exist in the form:
|
||||
//
|
||||
// async ({fix = false, batch = 1000}) => {}
|
||||
//
|
||||
// where their options are derived.
|
||||
module.exports = [require('./comment_replies'), require('./action_counts')];
|
||||
+1
-3
@@ -37,7 +37,7 @@ dependencies:
|
||||
|
||||
# Install node dependencies.
|
||||
- yarn --version
|
||||
- yarn global add node-gyp nsp --force
|
||||
- yarn global add node-gyp --force
|
||||
- yarn
|
||||
|
||||
post:
|
||||
@@ -59,8 +59,6 @@ test:
|
||||
- yarn test
|
||||
# Run the end to end tests
|
||||
- yarn e2e:ci
|
||||
# Check dependancies using nsp.
|
||||
- nsp check
|
||||
|
||||
deployment:
|
||||
release:
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Router, Route, IndexRedirect, IndexRoute } from 'react-router';
|
||||
import Configure from 'routes/Configure';
|
||||
import Install from 'routes/Install';
|
||||
import Stories from 'routes/Stories';
|
||||
import Community from 'routes/Community/containers/Community';
|
||||
import Community from 'routes/Community';
|
||||
import { ModerationLayout, Moderation } from 'routes/Moderation';
|
||||
|
||||
import Layout from 'containers/Layout';
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
import bowser from 'bowser';
|
||||
import * as actions from '../constants/auth';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
|
||||
//==============================================================================
|
||||
// SIGN IN
|
||||
//==============================================================================
|
||||
|
||||
export const handleLogin = (email, password, recaptchaResponse) => (
|
||||
dispatch,
|
||||
_,
|
||||
{ rest, client, storage }
|
||||
) => {
|
||||
dispatch({ type: actions.LOGIN_REQUEST });
|
||||
|
||||
const params = {
|
||||
method: 'POST',
|
||||
body: {
|
||||
email,
|
||||
password,
|
||||
},
|
||||
};
|
||||
|
||||
if (recaptchaResponse) {
|
||||
params.headers = {
|
||||
'X-Recaptcha-Response': recaptchaResponse,
|
||||
};
|
||||
}
|
||||
|
||||
return rest('/auth/local', params)
|
||||
.then(({ user, token }) => {
|
||||
if (!user) {
|
||||
if (!bowser.safari && !bowser.ios && storage) {
|
||||
storage.removeItem('token');
|
||||
}
|
||||
return dispatch(checkLoginFailure('not logged in'));
|
||||
}
|
||||
|
||||
dispatch(handleAuthToken(token));
|
||||
client.resetWebsocket();
|
||||
dispatch(checkLoginSuccess(user));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
|
||||
if (error.translation_key === 'NOT_AUTHORIZED') {
|
||||
// invalid credentials
|
||||
dispatch({
|
||||
type: actions.LOGIN_FAILURE,
|
||||
message: t('error.email_password'),
|
||||
});
|
||||
} else if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
|
||||
dispatch({
|
||||
type: actions.LOGIN_MAXIMUM_EXCEEDED,
|
||||
message: t(`error.${error.translation_key}`),
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: actions.LOGIN_FAILURE,
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// FORGOT PASSWORD
|
||||
//==============================================================================
|
||||
|
||||
const forgotPasswordRequest = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_REQUEST,
|
||||
});
|
||||
|
||||
const forgotPasswordSuccess = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS,
|
||||
});
|
||||
|
||||
const forgotPasswordFailure = error => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const requestPasswordReset = email => (dispatch, _, { rest }) => {
|
||||
dispatch(forgotPasswordRequest(email));
|
||||
const redirectUri = location.href;
|
||||
|
||||
return rest('/account/password/reset', {
|
||||
method: 'POST',
|
||||
body: { email, loc: redirectUri },
|
||||
})
|
||||
.then(() => dispatch(forgotPasswordSuccess()))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch(forgotPasswordFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// CHECK LOGIN
|
||||
//==============================================================================
|
||||
|
||||
const checkLoginRequest = () => ({
|
||||
type: actions.CHECK_LOGIN_REQUEST,
|
||||
});
|
||||
|
||||
const checkLoginSuccess = (user, isAdmin) => ({
|
||||
type: actions.CHECK_LOGIN_SUCCESS,
|
||||
user,
|
||||
isAdmin,
|
||||
});
|
||||
|
||||
const checkLoginFailure = error => ({
|
||||
type: actions.CHECK_LOGIN_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const checkLogin = () => (dispatch, _, { rest, client, storage }) => {
|
||||
dispatch(checkLoginRequest());
|
||||
return rest('/auth')
|
||||
.then(({ user }) => {
|
||||
if (!user) {
|
||||
if (!bowser.safari && !bowser.ios && storage) {
|
||||
storage.removeItem('token');
|
||||
}
|
||||
return dispatch(checkLoginFailure('not logged in'));
|
||||
}
|
||||
|
||||
client.resetWebsocket();
|
||||
dispatch(checkLoginSuccess(user));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch(checkLoginFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// LOGOUT
|
||||
//==============================================================================
|
||||
|
||||
export const logout = () => (dispatch, _, { rest, client, storage }) => {
|
||||
return rest('/auth', { method: 'DELETE' }).then(() => {
|
||||
if (storage) {
|
||||
storage.removeItem('token');
|
||||
}
|
||||
|
||||
// Reset the websocket.
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch({ type: actions.LOGOUT });
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// AUTH TOKEN
|
||||
//==============================================================================
|
||||
|
||||
export const handleAuthToken = token => (dispatch, _, { storage }) => {
|
||||
if (storage) {
|
||||
storage.setItem('exp', jwtDecode(token).exp);
|
||||
storage.setItem('token', token);
|
||||
}
|
||||
dispatch({ type: 'HANDLE_AUTH_TOKEN' });
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
HIDE_BANUSER_DIALOG,
|
||||
SHOW_REJECT_USERNAME_DIALOG,
|
||||
HIDE_REJECT_USERNAME_DIALOG,
|
||||
SET_INDICATOR_TRACK,
|
||||
} from '../constants/community';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
@@ -68,3 +69,9 @@ export const showRejectUsernameDialog = user => ({
|
||||
export const hideRejectUsernameDialog = () => ({
|
||||
type: HIDE_REJECT_USERNAME_DIALOG,
|
||||
});
|
||||
|
||||
// Enable or disable the activity indicator subscriptions.
|
||||
export const setIndicatorTrack = track => ({
|
||||
type: SET_INDICATOR_TRACK,
|
||||
track,
|
||||
});
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export const CONFIG_UPDATED = 'CONFIG_UPDATED';
|
||||
|
||||
export const fetchConfig = () => dispatch => {
|
||||
let json = document.getElementById('data');
|
||||
let data = JSON.parse(json.textContent);
|
||||
dispatch({ type: CONFIG_UPDATED, data });
|
||||
};
|
||||
@@ -4,10 +4,10 @@ export const toggleModal = open => ({ type: actions.TOGGLE_MODAL, open });
|
||||
export const singleView = () => ({ type: actions.SINGLE_VIEW });
|
||||
|
||||
// hide shortcuts note
|
||||
export const hideShortcutsNote = () => (dispatch, _, { storage }) => {
|
||||
export const hideShortcutsNote = () => (dispatch, _, { localStorage }) => {
|
||||
try {
|
||||
if (storage) {
|
||||
storage.setItem('coral:shortcutsNote', 'hide');
|
||||
if (localStorage) {
|
||||
localStorage.setItem('coral:shortcutsNote', 'hide');
|
||||
}
|
||||
} catch (e) {
|
||||
// above will fail in Safari private mode
|
||||
@@ -31,10 +31,16 @@ export const storySearchChange = value => ({
|
||||
});
|
||||
|
||||
export const clearState = () => ({
|
||||
type: actions.MODERATION_CLEAR_STATE,
|
||||
type: actions.CLEAR_STATE,
|
||||
});
|
||||
|
||||
export const selectCommentId = id => ({
|
||||
type: actions.MODERATION_SELECT_COMMENT,
|
||||
type: actions.SELECT_COMMENT,
|
||||
id,
|
||||
});
|
||||
|
||||
// Enable or disable the activity indicator subscriptions.
|
||||
export const setIndicatorTrack = track => ({
|
||||
type: actions.SET_INDICATOR_TRACK,
|
||||
track,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Layout from 'coral-admin/src/components/ui/Layout';
|
||||
import Layout from 'coral-admin/src/components/Layout';
|
||||
import styles from './NotFound.css';
|
||||
import { Button, TextField, Alert, Success } from 'coral-ui';
|
||||
import Recaptcha from 'react-recaptcha';
|
||||
|
||||
@@ -5,23 +5,22 @@
|
||||
background: white;
|
||||
padding: 10px 12px;
|
||||
box-sizing: border-box;
|
||||
vertical-align: middle;
|
||||
line-height: 24px;
|
||||
font-size: 17px;
|
||||
height: 47px;
|
||||
border-radius: 3px;
|
||||
text-transform: capitalize;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
|
||||
width: 129px;
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
margin: 0 0 .5em;
|
||||
|
||||
&:hover {
|
||||
&:not(:disabled):hover {
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
background-color: #519954;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Icon } from 'coral-ui';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const ApproveButton = ({ active, minimal, onClick, className }) => {
|
||||
const ApproveButton = ({ active, minimal, onClick, className, disabled }) => {
|
||||
const text = active ? t('modqueue.approved') : t('modqueue.approve');
|
||||
return (
|
||||
<button
|
||||
@@ -17,6 +17,7 @@ const ApproveButton = ({ active, minimal, onClick, className }) => {
|
||||
className
|
||||
)}
|
||||
onClick={onClick}
|
||||
disabled={disabled || active}
|
||||
>
|
||||
<Icon name={'done'} className={styles.icon} />
|
||||
{!minimal && text}
|
||||
@@ -28,6 +29,7 @@ ApproveButton.propTypes = {
|
||||
className: PropTypes.string,
|
||||
active: PropTypes.bool,
|
||||
minimal: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CSSTransitionGroup } from 'react-transition-group';
|
||||
import styles from './CommentAnimatedEdit.css';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const CommentBodyHighlighter = ({ children, body }) => {
|
||||
const CommentAnimatedEdit = ({ children, body }) => {
|
||||
return (
|
||||
<CSSTransitionGroup
|
||||
component={'div'}
|
||||
@@ -27,9 +27,9 @@ const CommentBodyHighlighter = ({ children, body }) => {
|
||||
);
|
||||
};
|
||||
|
||||
CommentBodyHighlighter.propTypes = {
|
||||
CommentAnimatedEdit.propTypes = {
|
||||
children: PropTypes.node,
|
||||
body: PropTypes.string,
|
||||
};
|
||||
|
||||
export default CommentBodyHighlighter;
|
||||
export default CommentAnimatedEdit;
|
||||
|
||||
+43
-11
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { matchLinks } from '../utils';
|
||||
import memoize from 'lodash/memoize';
|
||||
|
||||
@@ -62,16 +63,47 @@ function markLinks(body) {
|
||||
return content;
|
||||
}
|
||||
|
||||
export default ({ suspectWords, bannedWords, body, ...rest }) => {
|
||||
// First highlight links.
|
||||
const content = markLinks(body).map((element, index) => {
|
||||
// Keep highlighted links.
|
||||
if (typeof element !== 'string') {
|
||||
return element;
|
||||
}
|
||||
const CommentFormatter = ({
|
||||
body,
|
||||
suspectWords,
|
||||
bannedWords,
|
||||
className = 'comment',
|
||||
...rest
|
||||
}) => {
|
||||
// Breaking the body by line break
|
||||
const textbreaks = body.split('\n');
|
||||
|
||||
// Highlight suspect and banned phrase inside this part of text.
|
||||
return markPhrases(element, suspectWords, bannedWords, index);
|
||||
});
|
||||
return <div {...rest}>{content}</div>;
|
||||
return (
|
||||
<span className={`${className}-text`} {...rest}>
|
||||
{textbreaks.map((line, i) => {
|
||||
const content = markLinks(line).map((element, index) => {
|
||||
// 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 (
|
||||
<span key={i} className={`${className}-line`}>
|
||||
{content}
|
||||
{i !== textbreaks.length - 1 && (
|
||||
<br className={`${className}-linebreak`} />
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
CommentFormatter.propTypes = {
|
||||
className: PropTypes.string,
|
||||
bannedWords: PropTypes.array,
|
||||
suspectWords: PropTypes.array,
|
||||
body: PropTypes.string,
|
||||
};
|
||||
|
||||
export default CommentFormatter;
|
||||
@@ -5,8 +5,9 @@ import Slot from 'coral-framework/components/Slot';
|
||||
import FlagLabel from 'coral-ui/components/FlagLabel';
|
||||
import cn from 'classnames';
|
||||
import styles from './CommentLabels.css';
|
||||
import { ADMIN, MODERATOR, STAFF } from 'coral-framework/constants/roles';
|
||||
|
||||
const staffRoles = ['ADMIN', 'STAFF', 'MODERATOR'];
|
||||
const staffRoles = [ADMIN, MODERATOR, STAFF];
|
||||
|
||||
function isUserFlagged(actions) {
|
||||
return actions.some(
|
||||
|
||||
@@ -7,12 +7,12 @@ import t from 'coral-framework/services/i18n';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import cn from 'classnames';
|
||||
|
||||
const CoralDrawer = ({ handleLogout, auth = {} }) => (
|
||||
const CoralDrawer = ({ handleLogout, currentUser }) => (
|
||||
<Drawer className={cn('talk-admin-drawer-nav', styles.drawer)}>
|
||||
{auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? (
|
||||
{currentUser && can(currentUser, 'ACCESS_ADMIN') ? (
|
||||
<div>
|
||||
<Navigation className={styles.nav}>
|
||||
{can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
{can(currentUser, 'MODERATE_COMMENTS') && (
|
||||
<IndexLink
|
||||
className={cn('talk-admin-nav-moderate', styles.navLink)}
|
||||
to="/admin/moderate"
|
||||
@@ -35,7 +35,7 @@ const CoralDrawer = ({ handleLogout, auth = {} }) => (
|
||||
>
|
||||
{t('configure.community')}
|
||||
</Link>
|
||||
{can(auth.user, 'UPDATE_CONFIG') && (
|
||||
{can(currentUser, 'UPDATE_CONFIG') && (
|
||||
<Link
|
||||
className={cn('talk-admin-nav-configure', styles.navLink)}
|
||||
to="/admin/configure"
|
||||
@@ -44,7 +44,7 @@ const CoralDrawer = ({ handleLogout, auth = {} }) => (
|
||||
{t('configure.configure')}
|
||||
</Link>
|
||||
)}
|
||||
<a onClick={handleLogout}>Sign Out</a>
|
||||
<a onClick={handleLogout}>{t('configure.sign_out')}</a>
|
||||
<span>{`v${process.env.VERSION}`}</span>
|
||||
</Navigation>
|
||||
</div>
|
||||
@@ -55,7 +55,7 @@ const CoralDrawer = ({ handleLogout, auth = {} }) => (
|
||||
CoralDrawer.propTypes = {
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
restricted: PropTypes.bool, // hide app elements from a logged out user
|
||||
auth: PropTypes.object,
|
||||
currentUser: PropTypes.object,
|
||||
};
|
||||
|
||||
export default CoralDrawer;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
.header, .cta, .success {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.success {
|
||||
cursor: pointer;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
|
||||
.signInLink {
|
||||
color: blue;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.signInLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './ForgotPassword.css';
|
||||
import { Button, TextField, Alert, Success } from 'coral-ui';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class ForgotPassword extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
handleEmailChange = e => this.props.onEmailChange(e.target.value);
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
handleSignInLink = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSignInLink();
|
||||
};
|
||||
|
||||
renderSuccess() {
|
||||
return (
|
||||
<div className={styles.success} onClick={this.handleSignInLink}>
|
||||
{t('password_reset.mail_sent')}{' '}
|
||||
<a
|
||||
className={styles.signInLink}
|
||||
href="#"
|
||||
onClick={this.handleSignInLink}
|
||||
>
|
||||
Sign in
|
||||
</a>
|
||||
<Success />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderForm() {
|
||||
const { email, errorMessage } = this.props;
|
||||
return (
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
label="Email Address"
|
||||
value={email}
|
||||
onChange={this.handleEmailChange}
|
||||
/>
|
||||
<Button type="submit" cStyle="black" full>
|
||||
Reset Password
|
||||
</Button>
|
||||
<p className={styles.cta}>
|
||||
Go back to{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.signInLink}
|
||||
onClick={this.handleSignInLink}
|
||||
>
|
||||
Sign In
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.success ? this.renderSuccess() : this.renderForm();
|
||||
}
|
||||
}
|
||||
|
||||
ForgotPassword.propTypes = {
|
||||
success: PropTypes.bool.isRequired,
|
||||
email: PropTypes.string.isRequired,
|
||||
onEmailChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
onSignInLink: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ForgotPassword;
|
||||
+19
-16
@@ -7,22 +7,24 @@ import styles from './Header.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { Logo } from './Logo';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import Indicator from './Indicator';
|
||||
import ModerationIndicator from '../routes/Moderation/containers/Indicator';
|
||||
import CommunityIndicator from '../routes/Community/containers/Indicator';
|
||||
|
||||
const CoralHeader = ({
|
||||
handleLogout,
|
||||
showShortcuts = () => {},
|
||||
auth,
|
||||
currentUser,
|
||||
root,
|
||||
data,
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.headerWrapper}>
|
||||
<Header className={styles.header}>
|
||||
<Logo className={styles.logo} />
|
||||
<div>
|
||||
{auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? (
|
||||
{currentUser && can(currentUser, 'ACCESS_ADMIN') ? (
|
||||
<Navigation className={styles.nav}>
|
||||
{can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
{can(currentUser, 'MODERATE_COMMENTS') && (
|
||||
<IndexLink
|
||||
id="moderateNav"
|
||||
className={cn('talk-admin-nav-moderate', styles.navLink)}
|
||||
@@ -30,9 +32,7 @@ const CoralHeader = ({
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
{t('configure.moderate')}
|
||||
{(root.premodCount !== 0 || root.reportedCount !== 0) && (
|
||||
<Indicator />
|
||||
)}
|
||||
<ModerationIndicator root={root} data={data} />
|
||||
</IndexLink>
|
||||
)}
|
||||
<Link
|
||||
@@ -51,10 +51,10 @@ const CoralHeader = ({
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
{t('configure.community')}
|
||||
{root.flaggedUsernamesCount !== 0 && <Indicator />}
|
||||
<CommunityIndicator root={root} data={data} />
|
||||
</Link>
|
||||
|
||||
{can(auth.user, 'UPDATE_CONFIG') && (
|
||||
{can(currentUser, 'UPDATE_CONFIG') && (
|
||||
<Link
|
||||
id="configureNav"
|
||||
className={cn('talk-admin-nav-configure', styles.navLink)}
|
||||
@@ -97,12 +97,14 @@ const CoralHeader = ({
|
||||
Report a bug or give feedback
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={handleLogout}
|
||||
className="talk-admin-header-sign-out"
|
||||
>
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
{currentUser && (
|
||||
<MenuItem
|
||||
onClick={handleLogout}
|
||||
className="talk-admin-header-sign-out"
|
||||
>
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
@@ -116,10 +118,11 @@ const CoralHeader = ({
|
||||
};
|
||||
|
||||
CoralHeader.propTypes = {
|
||||
auth: PropTypes.object,
|
||||
currentUser: PropTypes.object,
|
||||
showShortcuts: PropTypes.func,
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default CoralHeader;
|
||||
+10
-6
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Layout as LayoutMDL } from 'react-mdl';
|
||||
import Header from '../../containers/Header';
|
||||
import Drawer from '../Drawer';
|
||||
import Header from '../containers/Header';
|
||||
import Drawer from './Drawer';
|
||||
import styles from './Layout.css';
|
||||
|
||||
const Layout = ({
|
||||
@@ -10,22 +10,26 @@ const Layout = ({
|
||||
handleLogout = () => {},
|
||||
toggleShortcutModal = () => {},
|
||||
restricted = false,
|
||||
auth,
|
||||
currentUser,
|
||||
}) => (
|
||||
<LayoutMDL className={styles.layout} fixedDrawer>
|
||||
<Header
|
||||
handleLogout={handleLogout}
|
||||
showShortcuts={toggleShortcutModal}
|
||||
auth={auth}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
<Drawer
|
||||
handleLogout={handleLogout}
|
||||
restricted={restricted}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
<Drawer handleLogout={handleLogout} restricted={restricted} auth={auth} />
|
||||
<div className={styles.layout}>{children}</div>
|
||||
</LayoutMDL>
|
||||
);
|
||||
|
||||
Layout.propTypes = {
|
||||
children: PropTypes.node,
|
||||
auth: PropTypes.object,
|
||||
currentUser: PropTypes.object,
|
||||
handleLogout: PropTypes.func,
|
||||
toggleShortcutModal: PropTypes.func,
|
||||
restricted: PropTypes.bool, // hide elements from a user that's logged out
|
||||
@@ -0,0 +1,18 @@
|
||||
.layout {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header, .cta {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.layout h1 {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import React, { Component } from 'react';
|
||||
import SignIn from '../containers/SignIn';
|
||||
import ForgotPassword from '../containers/ForgotPassword';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Login.css';
|
||||
import Layout from 'coral-admin/src/components/Layout';
|
||||
import cn from 'classnames';
|
||||
|
||||
class LoginContainer extends Component {
|
||||
renderForm() {
|
||||
return this.props.forgotPassword ? (
|
||||
<ForgotPassword onSignInLink={this.props.onSignInLink} />
|
||||
) : (
|
||||
<SignIn onForgotPasswordLink={this.props.onForgotPasswordLink} />
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Layout fixedDrawer restricted={true}>
|
||||
<div className={cn(styles.layout, 'talk-admin-login')}>
|
||||
<h1 className={styles.header}>Team sign in</h1>
|
||||
<p className={styles.cta}>Sign in to interact with your community.</p>
|
||||
{this.renderForm()}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LoginContainer.propTypes = {
|
||||
forgotPassword: PropTypes.bool.isRequired,
|
||||
onForgotPasswordLink: PropTypes.func.isRequired,
|
||||
onSignInLink: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default LoginContainer;
|
||||
@@ -23,7 +23,7 @@ export default class ModerationKeysModal extends React.Component {
|
||||
'ctrl+f': 'modqueue.toggle_search',
|
||||
t: 'modqueue.next_queue',
|
||||
[`1...${this.props.queueCount}`]: 'modqueue.jump_to_queue',
|
||||
s: 'modqueue.singleview',
|
||||
z: 'modqueue.singleview',
|
||||
'?': 'modqueue.thismenu',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,35 +3,3 @@
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loginLayout {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loginHeader, .loginCTA, .forgotPasswordCTA, .passwordRequestSuccess {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.forgotPasswordLink, .signInLink {
|
||||
color: blue;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.forgotPasswordLink:hover, .signInLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.layout h1 {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.loginHeader {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.passwordRequestSuccess {
|
||||
cursor: pointer;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
|
||||
@@ -5,23 +5,22 @@
|
||||
background: white;
|
||||
padding: 10px 11px;
|
||||
box-sizing: border-box;
|
||||
vertical-align: middle;
|
||||
line-height: 24px;
|
||||
font-size: 17px;
|
||||
height: 47px;
|
||||
border-radius: 3px;
|
||||
text-transform: capitalize;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
|
||||
width: 129px;
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
margin: 0 0 .5em;
|
||||
|
||||
&:hover {
|
||||
&:not(:disabled):hover {
|
||||
color: white;
|
||||
background-color: #D03235;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Icon } from 'coral-ui';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const RejectButton = ({ active, minimal, onClick, className }) => {
|
||||
const RejectButton = ({ active, minimal, onClick, className, disabled }) => {
|
||||
const text = active ? t('modqueue.rejected') : t('modqueue.reject');
|
||||
return (
|
||||
<button
|
||||
@@ -17,6 +17,7 @@ const RejectButton = ({ active, minimal, onClick, className }) => {
|
||||
className
|
||||
)}
|
||||
onClick={onClick}
|
||||
disabled={disabled || active}
|
||||
>
|
||||
<Icon name={'close'} className={styles.icon} />
|
||||
{!minimal && text}
|
||||
@@ -29,6 +30,7 @@ RejectButton.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
minimal: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default RejectButton;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
.forgotPasswordCTA {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.forgotPasswordLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.forgotPasswordLink {
|
||||
color: blue;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.recaptcha {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.signInButton {
|
||||
margin-top: 10px;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './SignIn.css';
|
||||
import { Button, TextField, Alert } from 'coral-ui';
|
||||
import cn from 'classnames';
|
||||
import Recaptcha from 'coral-framework/components/Recaptcha';
|
||||
|
||||
class SignIn extends React.Component {
|
||||
recaptcha = null;
|
||||
|
||||
handleForgotPasswordLink = e => {
|
||||
e.preventDefault();
|
||||
this.props.onForgotPasswordLink();
|
||||
};
|
||||
handleEmailChange = e => this.props.onEmailChange(e.target.value);
|
||||
handlePasswordChange = e => this.props.onPasswordChange(e.target.value);
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit();
|
||||
|
||||
// Reset recaptcha because each response can only
|
||||
// be used once.
|
||||
if (this.recaptcha) {
|
||||
this.recaptcha.reset();
|
||||
}
|
||||
};
|
||||
|
||||
handleRecaptchaRef = ref => {
|
||||
this.recaptcha = ref;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { email, password, errorMessage, requireRecaptcha } = this.props;
|
||||
return (
|
||||
<form className="talk-admin-login-sign-in" onSubmit={this.handleSubmit}>
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
id="email"
|
||||
label="Email Address"
|
||||
value={email}
|
||||
onChange={this.handleEmailChange}
|
||||
/>
|
||||
<TextField
|
||||
id="password"
|
||||
label="Password"
|
||||
value={password}
|
||||
onChange={this.handlePasswordChange}
|
||||
type="password"
|
||||
/>
|
||||
{requireRecaptcha && (
|
||||
<div className={styles.recaptcha}>
|
||||
<Recaptcha
|
||||
ref={this.handleRecaptchaRef}
|
||||
onVerify={this.props.onRecaptchaVerify}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
className={cn(styles.signInButton, 'talk-admin-login-sign-in-button')}
|
||||
type="submit"
|
||||
cStyle="black"
|
||||
full
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
<p className={styles.forgotPasswordCTA}>
|
||||
Forgot your password?{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.forgotPasswordLink}
|
||||
onClick={this.handleForgotPasswordLink}
|
||||
>
|
||||
Request a new one.
|
||||
</a>
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SignIn.propTypes = {
|
||||
email: PropTypes.string.isRequired,
|
||||
password: PropTypes.string.isRequired,
|
||||
onEmailChange: PropTypes.func.isRequired,
|
||||
onPasswordChange: PropTypes.func.isRequired,
|
||||
onForgotPasswordLink: PropTypes.func.isRequired,
|
||||
onRecaptchaVerify: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
requireRecaptcha: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default SignIn;
|
||||
@@ -2,7 +2,6 @@ import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import { getErrorMessages } from 'coral-framework/utils';
|
||||
import styles from './UserDetail.css';
|
||||
import AccountHistory from './AccountHistory';
|
||||
import { Slot } from 'coral-framework/components';
|
||||
@@ -26,46 +25,27 @@ import {
|
||||
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
|
||||
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
import UserInfoTooltip from './UserInfoTooltip';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class UserDetail extends React.Component {
|
||||
rejectThenReload = async info => {
|
||||
try {
|
||||
await this.props.rejectComment(info);
|
||||
this.props.data.refetch();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
await this.props.rejectComment(info);
|
||||
this.props.data.refetch();
|
||||
};
|
||||
|
||||
acceptThenReload = async info => {
|
||||
try {
|
||||
await this.props.acceptComment(info);
|
||||
this.props.data.refetch();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
await this.props.acceptComment(info);
|
||||
this.props.data.refetch();
|
||||
};
|
||||
|
||||
bulkAcceptThenReload = async () => {
|
||||
try {
|
||||
await this.props.bulkAccept();
|
||||
this.props.data.refetch();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
await this.props.bulkAccept();
|
||||
this.props.data.refetch();
|
||||
};
|
||||
|
||||
bulkRejectThenReload = async () => {
|
||||
try {
|
||||
await this.props.bulkReject();
|
||||
this.props.data.refetch();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
await this.props.bulkReject();
|
||||
this.props.data.refetch();
|
||||
};
|
||||
|
||||
changeTab = tab => {
|
||||
@@ -94,6 +74,16 @@ class UserDetail extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderError() {
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.props.hideUserDetail}>
|
||||
<Drawer onClose={this.props.hideUserDetail}>
|
||||
<div>{this.props.data.error.message}</div>
|
||||
</Drawer>
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
|
||||
getActionMenuLabel() {
|
||||
const { root: { user } } = this.props;
|
||||
|
||||
@@ -163,27 +153,27 @@ class UserDetail extends React.Component {
|
||||
>
|
||||
{suspended ? (
|
||||
<ActionsMenuItem onClick={() => unsuspendUser({ id: user.id })}>
|
||||
Remove Suspension
|
||||
{t('user_detail.remove_suspension')}
|
||||
</ActionsMenuItem>
|
||||
) : (
|
||||
<ActionsMenuItem
|
||||
disabled={me.id === user.id}
|
||||
onClick={this.showSuspenUserDialog}
|
||||
>
|
||||
Suspend User
|
||||
{t('user_detail.suspend')}
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
|
||||
{banned ? (
|
||||
<ActionsMenuItem onClick={() => unbanUser({ id: user.id })}>
|
||||
Remove Ban
|
||||
{t('user_detail.remove_ban')}
|
||||
</ActionsMenuItem>
|
||||
) : (
|
||||
<ActionsMenuItem
|
||||
disabled={me.id === user.id}
|
||||
onClick={this.showBanUserDialog}
|
||||
>
|
||||
Ban User
|
||||
{t('user_detail.ban')}
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
</ActionsMenu>
|
||||
@@ -201,14 +191,18 @@ class UserDetail extends React.Component {
|
||||
<ul className={styles.userDetailList}>
|
||||
<li>
|
||||
<Icon name="assignment_ind" />
|
||||
<span className={styles.userDetailItem}>Member Since:</span>
|
||||
<span className={styles.userDetailItem}>
|
||||
{t('user_detail.member_since')}:
|
||||
</span>
|
||||
{new Date(user.created_at).toLocaleString()}
|
||||
</li>
|
||||
|
||||
{user.profiles.map(({ id }) => (
|
||||
<li key={id}>
|
||||
<Icon name="email" />
|
||||
<span className={styles.userDetailItem}>Email:</span>
|
||||
<span className={styles.userDetailItem}>
|
||||
{t('user_detail.email')}:
|
||||
</span>
|
||||
{id}{' '}
|
||||
<ButtonCopyToClipboard
|
||||
className={styles.copyButton}
|
||||
@@ -221,17 +215,23 @@ class UserDetail extends React.Component {
|
||||
|
||||
<ul className={styles.stats}>
|
||||
<li className={styles.stat}>
|
||||
<span className={styles.statItem}>Total Comments</span>
|
||||
<span className={styles.statItem}>
|
||||
{t('user_detail.total_comments')}
|
||||
</span>
|
||||
<span className={styles.statResult}>{totalComments}</span>
|
||||
</li>
|
||||
<li className={styles.stat}>
|
||||
<span className={styles.statItem}>Reject Rate</span>
|
||||
<span className={styles.statItem}>
|
||||
{t('user_detail.reject_rate')}
|
||||
</span>
|
||||
<span className={styles.statResult}>
|
||||
{rejectedPercent.toFixed(1)}%
|
||||
</span>
|
||||
</li>
|
||||
<li className={styles.stat}>
|
||||
<span className={styles.statItem}>Reports</span>
|
||||
<span className={styles.statItem}>
|
||||
{t('user_detail.reports')}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
styles.statReportResult,
|
||||
@@ -270,13 +270,13 @@ class UserDetail extends React.Component {
|
||||
'talk-admin-user-detail-all-tab'
|
||||
)}
|
||||
>
|
||||
All
|
||||
{t('user_detail.all')}
|
||||
</Tab>
|
||||
<Tab
|
||||
tabId={'rejected'}
|
||||
className={cn(styles.tab, 'talk-admin-user-detail-rejected-tab')}
|
||||
>
|
||||
Rejected
|
||||
{t('user_detail.rejected')}
|
||||
</Tab>
|
||||
<Tab
|
||||
tabId={'history'}
|
||||
@@ -286,7 +286,7 @@ class UserDetail extends React.Component {
|
||||
'talk-admin-user-detail-history-tab'
|
||||
)}
|
||||
>
|
||||
Account History
|
||||
{t('user_detail.account_history')}
|
||||
</Tab>
|
||||
</TabBar>
|
||||
|
||||
@@ -345,6 +345,10 @@ class UserDetail extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.data.error) {
|
||||
return this.renderError();
|
||||
}
|
||||
|
||||
if (this.props.loading) {
|
||||
return this.renderLoading();
|
||||
}
|
||||
@@ -371,7 +375,6 @@ UserDetail.propTypes = {
|
||||
selectedCommentIds: PropTypes.array.isRequired,
|
||||
viewUserDetail: PropTypes.any.isRequired,
|
||||
loadMore: PropTypes.any.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
showSuspendUserDialog: PropTypes.func,
|
||||
showBanUserDialog: PropTypes.func,
|
||||
unbanUser: PropTypes.func.isRequired,
|
||||
|
||||
@@ -81,6 +81,8 @@
|
||||
}
|
||||
|
||||
.sideActions {
|
||||
/* IE 11 needs this... */
|
||||
width: 105px;
|
||||
}
|
||||
|
||||
.editedMarker {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Link } from 'react-router';
|
||||
import { Icon } from 'coral-ui';
|
||||
import CommentDetails from './CommentDetails';
|
||||
import styles from './UserDetailComment.css';
|
||||
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
|
||||
import CommentFormatter from 'coral-admin/src/components/CommentFormatter';
|
||||
import IfHasLink from 'coral-admin/src/components/IfHasLink';
|
||||
import cn from 'classnames';
|
||||
import CommentAnimatedEdit from './CommentAnimatedEdit';
|
||||
@@ -78,11 +78,12 @@ class UserDetailComment extends React.Component {
|
||||
<CommentAnimatedEdit body={comment.body}>
|
||||
<div className={styles.bodyContainer}>
|
||||
<div className={styles.body}>
|
||||
<CommentBodyHighlighter
|
||||
<CommentFormatter
|
||||
suspectWords={suspect}
|
||||
bannedWords={banned}
|
||||
body={comment.body}
|
||||
/>{' '}
|
||||
className="talk-admin-user-detail-comment"
|
||||
/>
|
||||
<a
|
||||
className={styles.external}
|
||||
href={`${comment.asset.url}?commentId=${comment.id}`}
|
||||
|
||||
@@ -18,3 +18,5 @@ export const SHOW_REJECT_USERNAME_DIALOG = `${prefix}_SHOW_REJECT_USERNAME_DIALO
|
||||
export const HIDE_REJECT_USERNAME_DIALOG = `${prefix}_HIDE_REJECT_USERNAME_DIALOG`;
|
||||
|
||||
export const SET_SEARCH_VALUE = `${prefix}_SET_SEARCH_VALUE`;
|
||||
|
||||
export const SET_INDICATOR_TRACK = `${prefix}_SET_INDICATOR_TRACK`;
|
||||
|
||||
@@ -2,8 +2,8 @@ export const NEXT_STEP = 'NEXT_STEP';
|
||||
export const GO_TO_STEP = 'GO_TO_STEP';
|
||||
export const PREVIOUS_STEP = 'PREVIOUS_STEP';
|
||||
export const ADD_ERROR = 'ADD_ERROR';
|
||||
export const HAS_ERROR = 'HAS_ERROR';
|
||||
export const SHOW_ERRORS = 'SHOW_ERRORS';
|
||||
export const HAS_ERROR = 'HAS_ERROR';
|
||||
export const SHOW_ERRORS = 'SHOW_ERRORS';
|
||||
export const CLEAR_ERRORS = 'CLEAR_ERRORS';
|
||||
export const INSTALL_REQUEST = 'INSTALL_REQUEST';
|
||||
export const INSTALL_SUCCESS = 'INSTALL_SUCCESS';
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
export const TOGGLE_MODAL = 'TOGGLE_MODAL';
|
||||
export const SINGLE_VIEW = 'SINGLE_VIEW';
|
||||
export const HIDE_SHORTCUTS_NOTE = 'HIDE_SHORTCUTS_NOTE';
|
||||
export const SET_SORT_ORDER = 'MODERATION_SET_SORT_ORDER';
|
||||
export const SHOW_STORY_SEARCH = 'SHOW_STORY_SEARCH';
|
||||
export const HIDE_STORY_SEARCH = 'HIDE_STORY_SEARCH';
|
||||
export const STORY_SEARCH_CHANGE_VALUE = 'STORY_SEARCH_CHANGE_VALUE';
|
||||
export const MODERATION_CLEAR_STATE = 'MODERATION_CLEAR_STATE';
|
||||
export const MODERATION_SELECT_COMMENT = 'MODERATION_SELECT_COMMENT';
|
||||
const prefix = `MODERATION`;
|
||||
|
||||
export const TOGGLE_MODAL = `${prefix}_TOGGLE_MODAL`;
|
||||
export const SINGLE_VIEW = `${prefix}_SINGLE_VIEW`;
|
||||
export const HIDE_SHORTCUTS_NOTE = `${prefix}_HIDE_SHORTCUTS_NOTE`;
|
||||
export const SET_SORT_ORDER = `${prefix}_SET_SORT_ORDER`;
|
||||
export const SHOW_STORY_SEARCH = `${prefix}_SHOW_STORY_SEARCH`;
|
||||
export const HIDE_STORY_SEARCH = `${prefix}_HIDE_STORY_SEARCH`;
|
||||
export const STORY_SEARCH_CHANGE_VALUE = `${prefix}_STORY_SEARCH_CHANGE_VALUE`;
|
||||
export const CLEAR_STATE = `${prefix}_CLEAR_STATE`;
|
||||
export const SELECT_COMMENT = `${prefix}_SELECT_COMMENT`;
|
||||
export const SET_INDICATOR_TRACK = `${prefix}_SET_INDICATOR_TRACK`;
|
||||
|
||||
@@ -10,8 +10,6 @@ import {
|
||||
} 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 () => {
|
||||
@@ -22,16 +20,11 @@ class BanUserDialogContainer extends Component {
|
||||
banUser,
|
||||
setCommentStatus,
|
||||
hideBanUserDialog,
|
||||
notify,
|
||||
} = this.props;
|
||||
try {
|
||||
await banUser({ id: userId, message: '' });
|
||||
hideBanUserDialog();
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
await setCommentStatus({ commentId, status: 'REJECTED' });
|
||||
}
|
||||
} catch (err) {
|
||||
notify('error', getErrorMessages(err));
|
||||
await banUser({ id: userId, message: '' });
|
||||
hideBanUserDialog();
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
await setCommentStatus({ commentId, status: 'REJECTED' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -78,14 +71,13 @@ const mapDispatchToProps = dispatch => ({
|
||||
...bindActionCreators(
|
||||
{
|
||||
hideBanUserDialog,
|
||||
notify,
|
||||
},
|
||||
dispatch
|
||||
),
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withBanUser,
|
||||
withSetCommentStatus,
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
withSetCommentStatus
|
||||
)(BanUserDialogContainer);
|
||||
|
||||
@@ -25,6 +25,7 @@ export default withFragments({
|
||||
id
|
||||
role
|
||||
}
|
||||
created_at
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'comment')}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withForgotPassword } from 'coral-framework/hocs';
|
||||
import { compose } from 'recompose';
|
||||
import ForgotPassword from '../components/ForgotPassword';
|
||||
|
||||
class ForgotPasswordContainer extends Component {
|
||||
state = {
|
||||
email: '',
|
||||
};
|
||||
|
||||
handleSubmit = () => {
|
||||
this.props.forgotPassword(this.state.email);
|
||||
};
|
||||
|
||||
handleEmailChange = email => {
|
||||
this.setState({ email });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ForgotPassword
|
||||
onSubmit={this.handleSubmit}
|
||||
onEmailChange={this.handleEmailChange}
|
||||
email={this.state.email}
|
||||
errorMessage={this.props.errorMessage}
|
||||
success={this.props.success}
|
||||
onSignInLink={this.props.onSignInLink}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ForgotPasswordContainer.propTypes = {
|
||||
success: PropTypes.bool.isRequired,
|
||||
forgotPassword: PropTypes.func.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
onSignInLink: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default compose(withForgotPassword)(ForgotPasswordContainer);
|
||||
@@ -1,26 +1,22 @@
|
||||
import { gql } from 'react-apollo';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import Header from '../components/ui/Header';
|
||||
import Header from '../components/Header';
|
||||
import CommunityIndicator from '../routes/Community/containers/Indicator';
|
||||
import ModerationIndicator from '../routes/Moderation/containers/Indicator';
|
||||
import { getDefinitionName } from 'coral-framework/utils';
|
||||
|
||||
export default withQuery(
|
||||
gql`
|
||||
query TalkAdmin_Header {
|
||||
__typename
|
||||
premodCount: commentCount(query: { statuses: [PREMOD] })
|
||||
reportedCount: commentCount(
|
||||
query: { statuses: [NONE, PREMOD, SYSTEM_WITHHELD], action_type: FLAG }
|
||||
)
|
||||
flaggedUsernamesCount: userCount(
|
||||
query: {
|
||||
action_type: FLAG
|
||||
state: { status: { username: [SET, CHANGED] } }
|
||||
}
|
||||
)
|
||||
query TalkAdmin_Header($nullID: ID) {
|
||||
...${getDefinitionName(ModerationIndicator.fragments.root)}
|
||||
...${getDefinitionName(CommunityIndicator.fragments.root)}
|
||||
}
|
||||
${ModerationIndicator.fragments.root}
|
||||
${CommunityIndicator.fragments.root}
|
||||
`,
|
||||
{
|
||||
options: {
|
||||
pollInterval: 10000,
|
||||
variables: { nullID: null },
|
||||
},
|
||||
}
|
||||
)(Header);
|
||||
|
||||
@@ -1,87 +1,59 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import Layout from '../components/ui/Layout';
|
||||
import { fetchConfig } from '../actions/config';
|
||||
import AdminLogin from '../components/AdminLogin';
|
||||
import Layout from '../components/Layout';
|
||||
import Login from '../containers/Login';
|
||||
import { FullLoading } from '../components/FullLoading';
|
||||
import BanUserDialog from './BanUserDialog';
|
||||
import SuspendUserDialog from './SuspendUserDialog';
|
||||
import { toggleModal as toggleShortcutModal } from '../actions/moderation';
|
||||
import {
|
||||
checkLogin,
|
||||
handleLogin,
|
||||
requestPasswordReset,
|
||||
logout,
|
||||
} from '../actions/auth';
|
||||
import { logout } from 'coral-framework/actions/auth';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import UserDetail from 'coral-admin/src/containers/UserDetail';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class LayoutContainer extends React.Component {
|
||||
componentWillMount() {
|
||||
const { checkLogin, fetchConfig } = this.props;
|
||||
|
||||
checkLogin();
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
user,
|
||||
loggedIn,
|
||||
loadingUser,
|
||||
loginError,
|
||||
loginMaxExceeded,
|
||||
passwordRequestSuccess,
|
||||
} = this.props.auth;
|
||||
|
||||
const {
|
||||
currentUser,
|
||||
checkedInitialLogin,
|
||||
children,
|
||||
logout,
|
||||
toggleShortcutModal,
|
||||
TALK_RECAPTCHA_PUBLIC,
|
||||
} = this.props;
|
||||
|
||||
if (loadingUser) {
|
||||
if (!checkedInitialLogin) {
|
||||
return <FullLoading />;
|
||||
}
|
||||
|
||||
if (!loggedIn) {
|
||||
return (
|
||||
<AdminLogin
|
||||
loginMaxExceeded={loginMaxExceeded}
|
||||
handleLogin={this.props.handleLogin}
|
||||
requestPasswordReset={this.props.requestPasswordReset}
|
||||
passwordRequestSuccess={passwordRequestSuccess}
|
||||
recaptchaPublic={TALK_RECAPTCHA_PUBLIC}
|
||||
errorMessage={loginError}
|
||||
/>
|
||||
);
|
||||
if (!currentUser) {
|
||||
return <Login />;
|
||||
}
|
||||
|
||||
if (can(user, 'ACCESS_ADMIN') && loggedIn) {
|
||||
return (
|
||||
<Layout
|
||||
handleLogout={logout}
|
||||
toggleShortcutModal={toggleShortcutModal}
|
||||
auth={this.props.auth}
|
||||
>
|
||||
<BanUserDialog />
|
||||
<SuspendUserDialog />
|
||||
<UserDetail />
|
||||
{children}
|
||||
</Layout>
|
||||
);
|
||||
} else if (loggedIn) {
|
||||
return (
|
||||
<Layout {...this.props}>
|
||||
<p>
|
||||
This page is for team use only. Please contact an administrator if
|
||||
you want to join this team.
|
||||
</p>
|
||||
</Layout>
|
||||
);
|
||||
if (currentUser) {
|
||||
if (can(currentUser, 'ACCESS_ADMIN')) {
|
||||
return (
|
||||
<Layout
|
||||
handleLogout={logout}
|
||||
toggleShortcutModal={toggleShortcutModal}
|
||||
currentUser={this.props.currentUser}
|
||||
>
|
||||
<BanUserDialog />
|
||||
<SuspendUserDialog />
|
||||
<UserDetail />
|
||||
{children}
|
||||
</Layout>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Layout {...this.props} handleLogout={logout}>
|
||||
<p>
|
||||
This page is for team use only. Please contact an administrator if
|
||||
you want to join this team.
|
||||
</p>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
return <FullLoading />;
|
||||
}
|
||||
@@ -89,29 +61,20 @@ class LayoutContainer extends React.Component {
|
||||
|
||||
LayoutContainer.propTypes = {
|
||||
children: PropTypes.node,
|
||||
requestPasswordReset: PropTypes.func,
|
||||
handleLogin: PropTypes.func,
|
||||
auth: PropTypes.object,
|
||||
handleLogout: PropTypes.func,
|
||||
currentUser: PropTypes.object,
|
||||
checkedInitialLogin: PropTypes.bool,
|
||||
logout: PropTypes.func,
|
||||
toggleShortcutModal: PropTypes.func,
|
||||
TALK_RECAPTCHA_PUBLIC: PropTypes.string,
|
||||
checkLogin: PropTypes.func,
|
||||
fetchConfig: PropTypes.func,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
auth: state.auth,
|
||||
TALK_RECAPTCHA_PUBLIC: state.config.data.TALK_RECAPTCHA_PUBLIC,
|
||||
currentUser: state.auth.user,
|
||||
checkedInitialLogin: state.auth.checkedInitialLogin,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
checkLogin,
|
||||
fetchConfig,
|
||||
handleLogin,
|
||||
requestPasswordReset,
|
||||
toggleShortcutModal,
|
||||
logout,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import React, { Component } from 'react';
|
||||
import Login from '../components/Login';
|
||||
|
||||
class LoginContainer extends Component {
|
||||
state = {
|
||||
forgotPassword: false,
|
||||
};
|
||||
|
||||
switchToForgotPassword = () => {
|
||||
this.setState({ forgotPassword: true });
|
||||
};
|
||||
|
||||
switchToSignIn = () => {
|
||||
this.setState({ forgotPassword: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Login
|
||||
forgotPassword={this.state.forgotPassword}
|
||||
onForgotPasswordLink={this.switchToForgotPassword}
|
||||
onSignInLink={this.switchToSignIn}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LoginContainer.propTypes = {};
|
||||
|
||||
export default LoginContainer;
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withSignIn } from 'coral-framework/hocs';
|
||||
import { compose } from 'recompose';
|
||||
import SignIn from '../components/SignIn';
|
||||
|
||||
class SignInContainer extends Component {
|
||||
state = {
|
||||
email: '',
|
||||
password: '',
|
||||
recaptchaResponse: null,
|
||||
};
|
||||
|
||||
handleSubmit = () => {
|
||||
this.props.signIn(
|
||||
this.state.email,
|
||||
this.state.password,
|
||||
this.state.recaptchaResponse
|
||||
);
|
||||
};
|
||||
|
||||
handleEmailChange = email => {
|
||||
this.setState({ email });
|
||||
};
|
||||
|
||||
handlePasswordChange = password => {
|
||||
this.setState({ password });
|
||||
};
|
||||
|
||||
handleRecaptchaVerify = recaptchaResponse => {
|
||||
this.setState({ recaptchaResponse });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SignIn
|
||||
onSubmit={this.handleSubmit}
|
||||
onEmailChange={this.handleEmailChange}
|
||||
onPasswordChange={this.handlePasswordChange}
|
||||
email={this.state.email}
|
||||
password={this.state.password}
|
||||
errorMessage={this.props.errorMessage}
|
||||
onForgotPasswordLink={this.props.onForgotPasswordLink}
|
||||
onRecaptchaVerify={this.handleRecaptchaVerify}
|
||||
requireRecaptcha={this.props.requireRecaptcha}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SignInContainer.propTypes = {
|
||||
signIn: PropTypes.func.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
onForgotPasswordLink: PropTypes.func.isRequired,
|
||||
requireRecaptcha: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default compose(withSignIn)(SignInContainer);
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import t, { timeago } from 'coral-framework/services/i18n';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import { getErrorMessages } from 'coral-framework/utils';
|
||||
import get from 'lodash/get';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
|
||||
@@ -28,17 +27,13 @@ class SuspendUserDialogContainer extends Component {
|
||||
notify,
|
||||
} = this.props;
|
||||
hideSuspendUserDialog();
|
||||
try {
|
||||
await suspendUser({ id: userId, message, until });
|
||||
notify(
|
||||
'success',
|
||||
t('suspenduser.notify_suspend_until', username, timeago(until))
|
||||
);
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
await setCommentStatus({ commentId, status: 'REJECTED' });
|
||||
}
|
||||
} catch (err) {
|
||||
notify('error', getErrorMessages(err));
|
||||
await suspendUser({ id: userId, message, until });
|
||||
notify(
|
||||
'success',
|
||||
t('suspenduser.notify_suspend_until', username, timeago(until))
|
||||
);
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
await setCommentStatus({ commentId, status: 'REJECTED' });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
} from 'coral-framework/graphql/mutations';
|
||||
import UserDetailComment from './UserDetailComment';
|
||||
import update from 'immutability-helper';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
import { showBanUserDialog } from 'actions/banUserDialog';
|
||||
import { showSuspendUserDialog } from 'actions/suspendUserDialog';
|
||||
|
||||
@@ -130,6 +129,7 @@ class UserDetailContainer extends React.Component {
|
||||
acceptComment={this.acceptComment}
|
||||
rejectComment={this.rejectComment}
|
||||
loading={loading}
|
||||
error={this.props.data && this.props.data.error}
|
||||
loadMore={this.loadMore}
|
||||
{...this.props}
|
||||
/>
|
||||
@@ -271,7 +271,6 @@ const mapDispatchToProps = dispatch => ({
|
||||
viewUserDetail,
|
||||
hideUserDetail,
|
||||
toggleSelectAllCommentInUserDetail,
|
||||
notify,
|
||||
},
|
||||
dispatch
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import update from 'immutability-helper';
|
||||
import { mapLeaves } from 'coral-framework/utils';
|
||||
import { gql } from 'react-apollo';
|
||||
import get from 'lodash/get';
|
||||
|
||||
const userStatusFragment = gql`
|
||||
fragment Talk_UpdateUserStatus on User {
|
||||
@@ -163,9 +164,26 @@ export default {
|
||||
},
|
||||
}),
|
||||
ApproveUsername: ({ variables: { id } }) => ({
|
||||
optimisticResponse: {
|
||||
approveUsername: {
|
||||
__typename: 'ApproveUsernameResponse',
|
||||
errors: null,
|
||||
isOptimistic: true,
|
||||
},
|
||||
},
|
||||
updateQueries: {
|
||||
TalkAdmin_Community: prev => {
|
||||
TalkAdmin_Community_FlaggedAccounts: (prev, { mutationResult }) => {
|
||||
const decrement = {
|
||||
flaggedUsernamesCount: { $apply: count => count - 1 },
|
||||
};
|
||||
|
||||
// Remove from list after the mutation was "really" completed.
|
||||
if (get(mutationResult, 'data.approveUsername.isOptimistic')) {
|
||||
return update(prev, decrement);
|
||||
}
|
||||
|
||||
const updated = update(prev, {
|
||||
...decrement,
|
||||
flaggedUsers: {
|
||||
nodes: { $apply: nodes => nodes.filter(node => node.id !== id) },
|
||||
},
|
||||
@@ -173,20 +191,95 @@ export default {
|
||||
return updated;
|
||||
},
|
||||
},
|
||||
update: proxy => {
|
||||
proxy.writeFragment({
|
||||
fragment: gql`
|
||||
fragment Talk_ApproveUsername on User {
|
||||
state {
|
||||
status {
|
||||
username {
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
id: `User_${id}`,
|
||||
data: {
|
||||
__typename: 'User',
|
||||
state: {
|
||||
__typename: 'UserState',
|
||||
status: {
|
||||
__typename: 'UserStatus',
|
||||
username: {
|
||||
__typename: 'UsernameStatus',
|
||||
status: 'APPROVED',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
RejectUsername: ({ variables: { id: userId } }) => ({
|
||||
RejectUsername: ({ variables: { id } }) => ({
|
||||
optimisticResponse: {
|
||||
rejectUsername: {
|
||||
__typename: 'RejectUsernameResponse',
|
||||
errors: null,
|
||||
isOptimistic: true,
|
||||
},
|
||||
},
|
||||
updateQueries: {
|
||||
TalkAdmin_Community: prev => {
|
||||
TalkAdmin_Community_FlaggedAccounts: (prev, { mutationResult }) => {
|
||||
const decrement = {
|
||||
flaggedUsernamesCount: { $apply: count => count - 1 },
|
||||
};
|
||||
|
||||
// Remove from list after the mutation was "really" completed.
|
||||
if (get(mutationResult, 'data.rejectUsername.isOptimistic')) {
|
||||
return update(prev, decrement);
|
||||
}
|
||||
|
||||
const updated = update(prev, {
|
||||
...decrement,
|
||||
flaggedUsers: {
|
||||
nodes: {
|
||||
$apply: nodes => nodes.filter(node => node.id !== userId),
|
||||
$apply: nodes => nodes.filter(node => node.id !== id),
|
||||
},
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
},
|
||||
},
|
||||
update: proxy => {
|
||||
proxy.writeFragment({
|
||||
fragment: gql`
|
||||
fragment Talk_RejectUsername on User {
|
||||
state {
|
||||
status {
|
||||
username {
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
id: `User_${id}`,
|
||||
data: {
|
||||
__typename: 'User',
|
||||
state: {
|
||||
__typename: 'UserState',
|
||||
status: {
|
||||
__typename: 'UserStatus',
|
||||
username: {
|
||||
__typename: 'UsernameStatus',
|
||||
status: 'REJECTED',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
UpdateSettings: ({ variables: { input } }) => ({
|
||||
updateQueries: {
|
||||
|
||||
@@ -14,8 +14,8 @@ import { hideShortcutsNote } from './actions/moderation';
|
||||
|
||||
smoothscroll.polyfill();
|
||||
|
||||
function init({ store, storage }) {
|
||||
if (storage && storage.getItem('coral:shortcutsNote') === 'hide') {
|
||||
function init({ store, localStorage }) {
|
||||
if (localStorage && localStorage.getItem('coral:shortcutsNote') === 'hide') {
|
||||
store.dispatch(hideShortcutsNote());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import * as actions from '../constants/auth';
|
||||
|
||||
const initialState = {
|
||||
loggedIn: false,
|
||||
user: null,
|
||||
loginError: null,
|
||||
loginMaxExceeded: false,
|
||||
passwordRequestSuccess: null,
|
||||
};
|
||||
|
||||
export default function auth(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.CHECK_LOGIN_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
loadingUser: true,
|
||||
};
|
||||
case actions.CHECK_LOGIN_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
loggedIn: false,
|
||||
loadingUser: false,
|
||||
user: null,
|
||||
};
|
||||
case actions.CHECK_LOGIN_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
loggedIn: true,
|
||||
loadingUser: false,
|
||||
user: action.user,
|
||||
};
|
||||
case actions.LOGOUT:
|
||||
return initialState;
|
||||
case actions.LOGIN_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
loginMaxExceeded: false,
|
||||
loginError: null,
|
||||
};
|
||||
case actions.LOGIN_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
loginError: action.message,
|
||||
};
|
||||
case actions.FETCH_FORGOT_PASSWORD_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
passwordRequestSuccess: null,
|
||||
};
|
||||
case actions.FETCH_FORGOT_PASSWORD_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
passwordRequestSuccess:
|
||||
'If you have a registered account, a password reset link was sent to that email.',
|
||||
};
|
||||
case actions.LOGIN_MAXIMUM_EXCEEDED:
|
||||
return {
|
||||
...state,
|
||||
loginMaxExceeded: true,
|
||||
loginError: action.message,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
HIDE_BANUSER_DIALOG,
|
||||
SHOW_REJECT_USERNAME_DIALOG,
|
||||
HIDE_REJECT_USERNAME_DIALOG,
|
||||
SET_INDICATOR_TRACK,
|
||||
} from '../constants/community';
|
||||
|
||||
const initialState = {
|
||||
@@ -24,6 +25,10 @@ const initialState = {
|
||||
user: {},
|
||||
banDialog: false,
|
||||
rejectUsernameDialog: false,
|
||||
// If true the activity indicator will track flagged account changes
|
||||
// in order to determine the current queue count. Set this to false
|
||||
// if the queue count is determined by other means.
|
||||
indicatorTrack: true,
|
||||
};
|
||||
|
||||
export default function community(state = initialState, action) {
|
||||
@@ -91,6 +96,11 @@ export default function community(state = initialState, action) {
|
||||
...state,
|
||||
searchValue: action.value,
|
||||
};
|
||||
case SET_INDICATOR_TRACK:
|
||||
return {
|
||||
...state,
|
||||
indicatorTrack: action.track,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import auth from './auth';
|
||||
import stories from './stories';
|
||||
import configure from './configure';
|
||||
import community from './community';
|
||||
import moderation from './moderation';
|
||||
import install from './install';
|
||||
import config from './config';
|
||||
import banUserDialog from './banUserDialog';
|
||||
import suspendUserDialog from './suspendUserDialog';
|
||||
import userDetail from './userDetail';
|
||||
import ui from './ui';
|
||||
|
||||
export default {
|
||||
auth,
|
||||
banUserDialog,
|
||||
configure,
|
||||
suspendUserDialog,
|
||||
@@ -20,6 +17,5 @@ export default {
|
||||
community,
|
||||
moderation,
|
||||
install,
|
||||
config,
|
||||
ui,
|
||||
};
|
||||
|
||||
@@ -8,14 +8,19 @@ const initialState = {
|
||||
shortcutsNoteVisible: 'show',
|
||||
sortOrder: 'DESC',
|
||||
selectedCommentId: '',
|
||||
// If true the activity indicator will turn on subscriptions
|
||||
// in order to determine queue counts. Set this to false
|
||||
// if the queue count is determined by other means.
|
||||
indicatorTrack: true,
|
||||
};
|
||||
|
||||
export default function moderation(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.MODERATION_CLEAR_STATE:
|
||||
case actions.CLEAR_STATE:
|
||||
return {
|
||||
...initialState,
|
||||
shortcutsNoteVisible: state.shortcutsNoteVisible,
|
||||
indicatorTrack: state.indicatorTrack,
|
||||
};
|
||||
case actions.TOGGLE_MODAL:
|
||||
return {
|
||||
@@ -52,11 +57,16 @@ export default function moderation(state = initialState, action) {
|
||||
...state,
|
||||
sortOrder: action.order,
|
||||
};
|
||||
case actions.MODERATION_SELECT_COMMENT:
|
||||
case actions.SELECT_COMMENT:
|
||||
return {
|
||||
...state,
|
||||
selectedCommentId: action.id,
|
||||
};
|
||||
case actions.SET_INDICATOR_TRACK:
|
||||
return {
|
||||
...state,
|
||||
indicatorTrack: action.track,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -2,44 +2,31 @@ import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Community.css';
|
||||
import People from '../containers/People';
|
||||
import CommunityMenu from './CommunityMenu';
|
||||
import RejectUsernameDialog from './RejectUsernameDialog';
|
||||
import CommunityMenu from '../containers/CommunityMenu';
|
||||
import RejectUsernameDialog from '../containers/RejectUsernameDialog';
|
||||
import FlaggedAccounts from '../containers/FlaggedAccounts';
|
||||
|
||||
class Community extends Component {
|
||||
renderTab() {
|
||||
const { route, community, ...props } = this.props;
|
||||
const { route } = this.props;
|
||||
const activeTab = route.path === ':id' ? 'flagged' : route.path;
|
||||
|
||||
if (activeTab === 'people') {
|
||||
return (
|
||||
<People
|
||||
community={community}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
/>
|
||||
);
|
||||
return <People />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FlaggedAccounts data={this.props.data} root={this.props.root} />
|
||||
<RejectUsernameDialog
|
||||
user={community.user}
|
||||
open={community.rejectUsernameDialog}
|
||||
rejectUsername={props.rejectUsername}
|
||||
handleClose={props.hideRejectUsernameDialog}
|
||||
/>
|
||||
<FlaggedAccounts />
|
||||
<RejectUsernameDialog />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { root: { flaggedUsernamesCount } } = this.props;
|
||||
|
||||
return (
|
||||
<div className="talk-admin-community">
|
||||
<CommunityMenu flaggedUsernamesCount={flaggedUsernamesCount} />
|
||||
<CommunityMenu />
|
||||
<div className={styles.container}>{this.renderTab()}</div>
|
||||
</div>
|
||||
);
|
||||
@@ -48,11 +35,6 @@ class Community extends Component {
|
||||
|
||||
Community.propTypes = {
|
||||
route: PropTypes.object,
|
||||
community: PropTypes.object,
|
||||
rejectUsername: PropTypes.func.isRequired,
|
||||
hideRejectUsernameDialog: PropTypes.func.isRequired,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
};
|
||||
|
||||
export default Community;
|
||||
|
||||
@@ -17,6 +17,7 @@ class FlaggedAccounts extends React.Component {
|
||||
approveUser,
|
||||
me,
|
||||
viewUserDetail,
|
||||
hasMore,
|
||||
} = this.props;
|
||||
|
||||
const hasResults = users.nodes && !!users.nodes.length;
|
||||
@@ -60,7 +61,7 @@ class FlaggedAccounts extends React.Component {
|
||||
) : (
|
||||
<EmptyCard>{t('community.no_flagged_accounts')}</EmptyCard>
|
||||
)}
|
||||
<LoadMore loadMore={loadMore} showLoadMore={users.hasNextPage} />
|
||||
<LoadMore loadMore={loadMore} showLoadMore={hasMore} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -70,6 +71,7 @@ class FlaggedAccounts extends React.Component {
|
||||
FlaggedAccounts.propTypes = {
|
||||
users: PropTypes.object,
|
||||
loadMore: PropTypes.func,
|
||||
hasMore: PropTypes.bool,
|
||||
showRejectUsernameDialog: PropTypes.func,
|
||||
approveUser: PropTypes.func,
|
||||
me: PropTypes.object,
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
min-width: 400px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
transition: box-shadow 200ms, margin-bottom 200ms;
|
||||
padding: 10px 0 0;
|
||||
transition: background 200ms, box-shadow 200ms, margin-bottom 200ms;
|
||||
padding: 10px 0 10px;
|
||||
min-height: 220px;
|
||||
|
||||
&:hover {
|
||||
@@ -18,6 +18,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.dangling {
|
||||
background-color: #efefef;
|
||||
}
|
||||
|
||||
.rootSelected {
|
||||
composes: mdl-shadow--16dp from global;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ import styles from './FlaggedUser.css';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { username } from 'talk-plugin-flags/helpers/flagReasons';
|
||||
import { username } from 'coral-framework/graphql/flagReasons';
|
||||
import ApproveButton from 'coral-admin/src/components/ApproveButton';
|
||||
import RejectButton from 'coral-admin/src/components/RejectButton';
|
||||
import { isFlaggedUserDangling } from '../utils';
|
||||
|
||||
const shortReasons = {
|
||||
[username.other]: t('community.other'),
|
||||
@@ -28,12 +29,14 @@ class User extends React.Component {
|
||||
|
||||
render() {
|
||||
const { user, viewUserDetail, selected, className } = this.props;
|
||||
const dangling = isFlaggedUserDangling(user);
|
||||
|
||||
return (
|
||||
<li
|
||||
tabIndex={0}
|
||||
className={cn(className, styles.root, {
|
||||
[styles.rootSelected]: selected,
|
||||
[styles.dangling]: dangling,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
@@ -110,11 +113,15 @@ class User extends React.Component {
|
||||
<div className={styles.actions}>
|
||||
<ApproveButton
|
||||
className="talk-admin-flagged-user-approve-button"
|
||||
active={user.state.status.username.status === 'APPROVED'}
|
||||
onClick={this.approveUser}
|
||||
disabled={dangling}
|
||||
/>
|
||||
<RejectButton
|
||||
className="talk-admin-flagged-user-reject-button"
|
||||
active={user.state.status.username.status === 'REJECTED'}
|
||||
onClick={this.showRejectUsernameDialog}
|
||||
disabled={dangling}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,12 @@ import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
|
||||
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
import { isSuspended, isBanned } from 'coral-framework/utils/user';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
ADMIN,
|
||||
COMMENTER,
|
||||
MODERATOR,
|
||||
STAFF,
|
||||
} from 'coral-framework/constants/roles';
|
||||
|
||||
const headers = [
|
||||
{
|
||||
@@ -200,19 +206,19 @@ class People extends React.Component {
|
||||
onChange={role => setUserRole(user.id, role)}
|
||||
>
|
||||
<Option
|
||||
value={'COMMENTER'}
|
||||
value={COMMENTER}
|
||||
label={t('community.commenter')}
|
||||
/>
|
||||
<Option
|
||||
value={'STAFF'}
|
||||
value={STAFF}
|
||||
label={t('community.staff')}
|
||||
/>
|
||||
<Option
|
||||
value={'MODERATOR'}
|
||||
value={MODERATOR}
|
||||
label={t('community.moderator')}
|
||||
/>
|
||||
<Option
|
||||
value={'ADMIN'}
|
||||
value={ADMIN}
|
||||
label={t('community.admin')}
|
||||
/>
|
||||
</Dropdown>
|
||||
|
||||
@@ -94,7 +94,7 @@ class RejectUsernameDialog extends Component {
|
||||
|
||||
{/* {
|
||||
|
||||
// Suspension Message: This functionality it's not entirely done on the BE - It will be released soon.
|
||||
// Suspension Message: This functionality it's not entirely done on the BE - It will be released soon.
|
||||
stage === 1 &&
|
||||
<div className={styles.writeContainer}>
|
||||
<div className={styles.emailMessage}>{t('reject_username.write_message')}</div>
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import { getDefinitionName } from 'coral-framework/utils';
|
||||
import { withRejectUsername } from 'coral-framework/graphql/mutations';
|
||||
import FlaggedAccounts from '../containers/FlaggedAccounts';
|
||||
import FlaggedUser from '../containers/FlaggedUser';
|
||||
import People from '../containers/People';
|
||||
import { hideRejectUsernameDialog } from '../../../actions/community';
|
||||
import Community from '../components/Community';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
community: state.community,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
hideRejectUsernameDialog,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
const withData = withQuery(
|
||||
gql`
|
||||
query TalkAdmin_Community {
|
||||
flaggedUsernamesCount: userCount(
|
||||
query:{
|
||||
action_type: FLAG,
|
||||
state: {
|
||||
status: {
|
||||
username: [SET, CHANGED]
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
...${getDefinitionName(FlaggedAccounts.fragments.root)}
|
||||
...${getDefinitionName(FlaggedUser.fragments.root)}
|
||||
...${getDefinitionName(People.fragments.root)}
|
||||
me {
|
||||
...${getDefinitionName(FlaggedUser.fragments.me)}
|
||||
__typename
|
||||
}
|
||||
}
|
||||
${People.fragments.root}
|
||||
${FlaggedAccounts.fragments.root}
|
||||
${FlaggedUser.fragments.root}
|
||||
${FlaggedUser.fragments.me}
|
||||
`,
|
||||
{
|
||||
options: {
|
||||
fetchPolicy: 'network-only',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withRejectUsername,
|
||||
withData
|
||||
)(Community);
|
||||
@@ -0,0 +1,4 @@
|
||||
import withCachedFlaggedUsernamesCount from '../hocs/withCachedFlaggedUsernamesCount';
|
||||
import CommunityMenu from '../components/CommunityMenu';
|
||||
|
||||
export default withCachedFlaggedUsernamesCount(CommunityMenu);
|
||||
@@ -2,22 +2,136 @@ 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 withQuery from 'coral-framework/hocs/withQuery';
|
||||
import { Spinner } from 'coral-ui';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withApproveUsername } from 'coral-framework/graphql/mutations';
|
||||
import { showRejectUsernameDialog } from '../../../actions/community';
|
||||
import {
|
||||
showRejectUsernameDialog,
|
||||
setIndicatorTrack,
|
||||
} 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 { handleFlaggedAccountsChange } from '../graphql';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
import { isFlaggedUserDangling } from '../utils';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
import FlaggedAccounts from '../components/FlaggedAccounts';
|
||||
import FlaggedUser from '../containers/FlaggedUser';
|
||||
|
||||
function whoChangedTheStatus(statusObject) {
|
||||
return statusObject.history[statusObject.history.length - 1].assigned_by
|
||||
.username;
|
||||
}
|
||||
|
||||
function whoFlagged(user) {
|
||||
return user.actions[user.actions.length - 1].user.username;
|
||||
}
|
||||
|
||||
class FlaggedAccountsContainer extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
subscriptions = [];
|
||||
|
||||
getCountWithoutDangling() {
|
||||
return this.props.root.flaggedUsers.nodes.filter(
|
||||
node => !isFlaggedUserDangling(node)
|
||||
).length;
|
||||
}
|
||||
|
||||
subscribeToUpdates() {
|
||||
const parameters = [
|
||||
{
|
||||
document: USERNAME_FLAGGED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameFlagged: user } } }
|
||||
) => {
|
||||
return handleFlaggedAccountsChange(prev, user, () => {
|
||||
const msg = t(
|
||||
'flagged_usernames.notify_flagged',
|
||||
whoFlagged(user),
|
||||
user.username
|
||||
);
|
||||
this.props.notify('info', msg);
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
document: USERNAME_APPROVED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameApproved: user } } }
|
||||
) => {
|
||||
return handleFlaggedAccountsChange(prev, user, () => {
|
||||
const msg = t(
|
||||
'flagged_usernames.notify_approved',
|
||||
whoChangedTheStatus(user.state.status.username),
|
||||
user.username
|
||||
);
|
||||
this.props.notify('info', msg);
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
document: USERNAME_REJECTED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameRejected: user } } }
|
||||
) => {
|
||||
return handleFlaggedAccountsChange(prev, user, () => {
|
||||
const msg = t(
|
||||
'flagged_usernames.notify_rejected',
|
||||
whoChangedTheStatus(user.state.status.username),
|
||||
user.username
|
||||
);
|
||||
this.props.notify('info', msg);
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
document: USERNAME_CHANGED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { usernameChanged: { previousUsername, user } },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return handleFlaggedAccountsChange(prev, user, () => {
|
||||
const msg = t(
|
||||
'flagged_usernames.notify_changed',
|
||||
previousUsername,
|
||||
user.username
|
||||
);
|
||||
this.props.notify('info', msg);
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
this.subscriptions = parameters.map(param =>
|
||||
this.props.data.subscribeToMore(param)
|
||||
);
|
||||
}
|
||||
|
||||
unsubscribe() {
|
||||
this.subscriptions.forEach(unsubscribe => unsubscribe());
|
||||
this.subscriptions = [];
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
// Stop activity indicator tracking, as we'll handle it here.
|
||||
this.props.setIndicatorTrack(false);
|
||||
this.subscribeToUpdates();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Restart activity indicator tracking.
|
||||
this.props.setIndicatorTrack(true);
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
approveUser = ({ userId: id }) => {
|
||||
@@ -68,6 +182,9 @@ class FlaggedAccountsContainer extends Component {
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
users={this.props.root.flaggedUsers}
|
||||
hasMore={
|
||||
this.getCountWithoutDangling() < this.props.root.flaggedUsernamesCount
|
||||
}
|
||||
me={this.props.root.me}
|
||||
/>
|
||||
);
|
||||
@@ -77,9 +194,12 @@ class FlaggedAccountsContainer extends Component {
|
||||
FlaggedAccountsContainer.propTypes = {
|
||||
showRejectUsernameDialog: PropTypes.func,
|
||||
viewUserDetail: PropTypes.func,
|
||||
notify: PropTypes.func,
|
||||
flaggedUsernamesCount: PropTypes.number,
|
||||
approveUsername: PropTypes.func,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
setIndicatorTrack: PropTypes.func,
|
||||
};
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
@@ -105,11 +225,73 @@ const LOAD_MORE_QUERY = gql`
|
||||
${FlaggedUser.fragments.user}
|
||||
`;
|
||||
|
||||
const historyFields = `
|
||||
state {
|
||||
status {
|
||||
username {
|
||||
history {
|
||||
status
|
||||
assigned_by {
|
||||
id
|
||||
username
|
||||
}
|
||||
created_at
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const USERNAME_FLAGGED_SUBSCRIPTION = gql`
|
||||
subscription TalkAdmin_UsernameFlagged {
|
||||
usernameFlagged {
|
||||
...${getDefinitionName(FlaggedUser.fragments.user)}
|
||||
${historyFields}
|
||||
}
|
||||
}
|
||||
${FlaggedUser.fragments.user}
|
||||
`;
|
||||
|
||||
const USERNAME_APPROVED_SUBSCRIPTION = gql`
|
||||
subscription TalkAdmin_UsernameApproved {
|
||||
usernameApproved {
|
||||
...${getDefinitionName(FlaggedUser.fragments.user)}
|
||||
${historyFields}
|
||||
}
|
||||
}
|
||||
${FlaggedUser.fragments.user}
|
||||
`;
|
||||
|
||||
const USERNAME_REJECTED_SUBSCRIPTION = gql`
|
||||
subscription TalkAdmin_UsernameRejected {
|
||||
usernameRejected {
|
||||
...${getDefinitionName(FlaggedUser.fragments.user)}
|
||||
${historyFields}
|
||||
}
|
||||
}
|
||||
${FlaggedUser.fragments.user}
|
||||
`;
|
||||
|
||||
const USERNAME_CHANGED_SUBSCRIPTION = gql`
|
||||
subscription TalkAdmin_UsernameChanged {
|
||||
usernameChanged {
|
||||
previousUsername
|
||||
user {
|
||||
...${getDefinitionName(FlaggedUser.fragments.user)}
|
||||
${historyFields}
|
||||
}
|
||||
}
|
||||
}
|
||||
${FlaggedUser.fragments.user}
|
||||
`;
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
showRejectUsernameDialog,
|
||||
viewUserDetail,
|
||||
notify,
|
||||
setIndicatorTrack,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
@@ -117,9 +299,15 @@ const mapDispatchToProps = dispatch =>
|
||||
export default compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
withApproveUsername,
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment TalkAdminCommunity_FlaggedAccounts_root on RootQuery {
|
||||
withQuery(
|
||||
gql`
|
||||
query TalkAdmin_Community_FlaggedAccounts {
|
||||
flaggedUsernamesCount: userCount(
|
||||
query: {
|
||||
action_type: FLAG
|
||||
state: { status: { username: [SET, CHANGED] } }
|
||||
}
|
||||
)
|
||||
flaggedUsers: users(query:{
|
||||
action_type: FLAG,
|
||||
state: {
|
||||
@@ -138,9 +326,16 @@ export default compose(
|
||||
}
|
||||
me {
|
||||
id
|
||||
...${getDefinitionName(FlaggedUser.fragments.me)}
|
||||
}
|
||||
}
|
||||
${FlaggedUser.fragments.user}
|
||||
${FlaggedUser.fragments.me}
|
||||
`,
|
||||
})
|
||||
{
|
||||
options: {
|
||||
fetchPolicy: 'network-only',
|
||||
},
|
||||
}
|
||||
)
|
||||
)(FlaggedAccountsContainer);
|
||||
|
||||
@@ -4,19 +4,20 @@ import { withFragments } from 'plugin-api/beta/client/hocs';
|
||||
|
||||
export default withFragments({
|
||||
root: gql`
|
||||
fragment TalkAdminCommunity_FlaggedUser_root on RootQuery {
|
||||
fragment TalkAdmin_Community_FlaggedUser_root on RootQuery {
|
||||
__typename
|
||||
}
|
||||
`,
|
||||
me: gql`
|
||||
fragment TalkAdminCommunity_FlaggedUser_me on User {
|
||||
fragment TalkAdmin_Community_FlaggedUser_me on User {
|
||||
id
|
||||
}
|
||||
`,
|
||||
user: gql`
|
||||
fragment TalkAdminCommunity_FlaggedUser_user on User {
|
||||
fragment TalkAdmin_Community_FlaggedUser_user on User {
|
||||
id
|
||||
username
|
||||
created_at
|
||||
state {
|
||||
status {
|
||||
username {
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import React, { Component } from 'react';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import Indicator from '../../../components/Indicator';
|
||||
import { withFragments } from 'plugin-api/beta/client/hocs';
|
||||
import { handleIndicatorChange } from '../graphql';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
class IndicatorContainer extends Component {
|
||||
subscriptions = [];
|
||||
|
||||
subscribeToUpdates() {
|
||||
const parameters = [
|
||||
{
|
||||
document: USERNAME_FLAGGED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameFlagged: user } } }
|
||||
) => {
|
||||
return handleIndicatorChange(prev, user);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: USERNAME_APPROVED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameApproved: user } } }
|
||||
) => {
|
||||
return handleIndicatorChange(prev, user);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: USERNAME_REJECTED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameRejected: user } } }
|
||||
) => {
|
||||
return handleIndicatorChange(prev, user);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: USERNAME_CHANGED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameChanged: { user } } } }
|
||||
) => {
|
||||
return handleIndicatorChange(prev, user);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
this.subscriptions = parameters.map(param =>
|
||||
this.props.data.subscribeToMore(param)
|
||||
);
|
||||
}
|
||||
|
||||
unsubscribe() {
|
||||
this.subscriptions.forEach(unsubscribe => unsubscribe());
|
||||
this.subscriptions = [];
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (this.props.track) {
|
||||
this.subscribeToUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.props.track && nextProps.track) {
|
||||
this.subscribeToUpdates();
|
||||
}
|
||||
if (this.props.track && !nextProps.track) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.root || !this.props.root.flaggedUsernamesCount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Indicator />;
|
||||
}
|
||||
}
|
||||
|
||||
IndicatorContainer.propTypes = {
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
track: PropTypes.bool,
|
||||
};
|
||||
|
||||
const fields = `
|
||||
state {
|
||||
status {
|
||||
username {
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const USERNAME_FLAGGED_SUBSCRIPTION = gql`
|
||||
subscription TalkAdmin_CommunityIndicator_UsernameFlagged {
|
||||
usernameFlagged {
|
||||
${fields}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const USERNAME_APPROVED_SUBSCRIPTION = gql`
|
||||
subscription TalkAdmin_ComunityIndicator_UsernameApproved {
|
||||
usernameApproved {
|
||||
${fields}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const USERNAME_REJECTED_SUBSCRIPTION = gql`
|
||||
subscription TalkAdmin_CommunityIndicator_UsernameRejected {
|
||||
usernameRejected {
|
||||
${fields}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const USERNAME_CHANGED_SUBSCRIPTION = gql`
|
||||
subscription TalkAdmin_ComunityIndicator_UsernameChanged {
|
||||
usernameChanged {
|
||||
previousUsername
|
||||
user {
|
||||
${fields}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
track: state.community.indicatorTrack,
|
||||
});
|
||||
|
||||
const enhance = compose(
|
||||
connect(mapStateToProps),
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment TalkAdmin_CommunityIndicator_root on RootQuery {
|
||||
flaggedUsernamesCount: userCount(
|
||||
query: {
|
||||
action_type: FLAG
|
||||
state: { status: { username: [SET, CHANGED] } }
|
||||
}
|
||||
)
|
||||
}
|
||||
`,
|
||||
})
|
||||
);
|
||||
|
||||
export default enhance(IndicatorContainer);
|
||||
@@ -4,7 +4,6 @@ import { bindActionCreators } from 'redux';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import People from '../components/People';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withFragments } from 'plugin-api/beta/client/hocs';
|
||||
import {
|
||||
withUnbanUser,
|
||||
withUnsuspendUser,
|
||||
@@ -16,6 +15,7 @@ import { viewUserDetail } from '../../../actions/userDetail';
|
||||
import { appendNewNodes } from 'plugin-api/beta/client/utils';
|
||||
import update from 'immutability-helper';
|
||||
import { Spinner } from 'coral-ui';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
|
||||
class PeopleContainer extends React.Component {
|
||||
timer = null;
|
||||
@@ -39,7 +39,7 @@ class PeopleContainer extends React.Component {
|
||||
query: SEARCH_QUERY,
|
||||
variables: {
|
||||
value,
|
||||
limit: 5,
|
||||
limit: 10,
|
||||
},
|
||||
updateQuery: (previous, { fetchMoreResult: { users } }) => {
|
||||
const updated = update(previous, {
|
||||
@@ -115,7 +115,6 @@ class PeopleContainer extends React.Component {
|
||||
}
|
||||
|
||||
PeopleContainer.propTypes = {
|
||||
community: PropTypes.object,
|
||||
setUserRole: PropTypes.func.isRequired,
|
||||
unbanUser: PropTypes.func.isRequired,
|
||||
unsuspendUser: PropTypes.func.isRequired,
|
||||
@@ -137,7 +136,7 @@ const mapDispatchToProps = dispatch =>
|
||||
);
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query TalkAdminCommunity_People_LoadMoreUsers(
|
||||
query TalkAdmin_Community_People_LoadMoreUsers(
|
||||
$limit: Int
|
||||
$cursor: Cursor
|
||||
$value: String
|
||||
@@ -171,7 +170,7 @@ const LOAD_MORE_QUERY = gql`
|
||||
`;
|
||||
|
||||
const SEARCH_QUERY = gql`
|
||||
query TalkAdminCommunity_People_SearchUsers($value: String, $limit: Int) {
|
||||
query TalkAdmin_Community_People_SearchUsers($value: String, $limit: Int) {
|
||||
users(query: { value: $value, limit: $limit }) {
|
||||
hasNextPage
|
||||
endCursor
|
||||
@@ -205,9 +204,9 @@ export default compose(
|
||||
withSetUserRole,
|
||||
withUnsuspendUser,
|
||||
withUnbanUser,
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment TalkAdminCommunity_People_root on RootQuery {
|
||||
withQuery(
|
||||
gql`
|
||||
query TalkAdmin_Community_People {
|
||||
users(query: {}) {
|
||||
hasNextPage
|
||||
endCursor
|
||||
@@ -235,5 +234,10 @@ export default compose(
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
{
|
||||
options: {
|
||||
fetchPolicy: 'network-only',
|
||||
},
|
||||
}
|
||||
)
|
||||
)(PeopleContainer);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import RejectUsernameDialog from '../components/RejectUsernameDialog';
|
||||
import { withRejectUsername } from 'coral-framework/graphql/mutations';
|
||||
import { hideRejectUsernameDialog } from '../../../actions/community';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { compose } from 'react-apollo';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
user: state.community.user,
|
||||
open: state.community.rejectUsernameDialog,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
handleClose: hideRejectUsernameDialog,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withRejectUsername
|
||||
)(RejectUsernameDialog);
|
||||
@@ -0,0 +1,125 @@
|
||||
import update from 'immutability-helper';
|
||||
|
||||
function shouldAddFlaggedUser(root, user) {
|
||||
const isEmpty = !root.flaggedUsers.nodes.length;
|
||||
if (isEmpty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasFlaggedUser(root, user)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const oldest = root.flaggedUsers.nodes.reduce((cur, node) => {
|
||||
const createdAt = new Date(node.created_at);
|
||||
return createdAt < cur ? createdAt : cur;
|
||||
}, new Date());
|
||||
|
||||
return new Date(user.created_at) >= oldest;
|
||||
}
|
||||
|
||||
function hasFlaggedUser(root, user) {
|
||||
return root.flaggedUsers.nodes.find(u => u.id === user.id);
|
||||
}
|
||||
|
||||
function applyUserChanges(root, user) {
|
||||
const index = root.flaggedUsers.nodes.findIndex(({ id }) => id === user.id);
|
||||
if (index > -1) {
|
||||
return update(root, {
|
||||
flaggedUsers: {
|
||||
nodes: {
|
||||
[index]: { $merge: user },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
function incrementFlaggedUserCount(root) {
|
||||
return update(root, {
|
||||
flaggedUsernamesCount: { $apply: count => count + 1 },
|
||||
});
|
||||
}
|
||||
|
||||
function decrementFlaggedUserCount(root) {
|
||||
return update(root, {
|
||||
flaggedUsernamesCount: { $apply: count => count - 1 },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Assimilate flagged acount changes into current store.
|
||||
* @param {Object} root current state of the store
|
||||
* @param {Object} user user that was changed
|
||||
* @param {function} notify callback to show notification
|
||||
* @return {Object} next state of the store
|
||||
*/
|
||||
export function handleFlaggedAccountsChange(root, user, notify) {
|
||||
if (user.state.status.username.status !== 'SET') {
|
||||
// Check if change came from current user, if so ignore it.
|
||||
const lastChange =
|
||||
user.state.status.username.history[
|
||||
user.state.status.username.history.length - 1
|
||||
];
|
||||
if (lastChange.assigned_by.id === root.me.id) {
|
||||
return root;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasFlaggedUser(root, user)) {
|
||||
switch (user.state.status.username.status) {
|
||||
case 'SET':
|
||||
case 'CHANGED':
|
||||
root = incrementFlaggedUserCount(root);
|
||||
|
||||
if (!shouldAddFlaggedUser(root, user)) {
|
||||
return root;
|
||||
}
|
||||
|
||||
notify();
|
||||
|
||||
return update(root, {
|
||||
flaggedUsers: {
|
||||
nodes: { $push: [user] },
|
||||
},
|
||||
});
|
||||
break;
|
||||
case 'APPROVED':
|
||||
case 'REJECTED':
|
||||
return decrementFlaggedUserCount(root);
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
if (hasFlaggedUser(root, user)) {
|
||||
switch (user.state.status.username.status) {
|
||||
case 'SET':
|
||||
case 'CHANGED':
|
||||
return root;
|
||||
case 'APPROVED':
|
||||
case 'REJECTED':
|
||||
notify();
|
||||
return applyUserChanges(decrementFlaggedUserCount(root), user);
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Track indicator status
|
||||
* @param {Object} root current state of the store
|
||||
* @param {Object} user user that was changed
|
||||
* @return {Object} next state of the store
|
||||
*/
|
||||
export function handleIndicatorChange(root, user) {
|
||||
switch (user.state.status.username.status) {
|
||||
case 'SET':
|
||||
case 'CHANGED':
|
||||
return incrementFlaggedUserCount(root);
|
||||
case 'APPROVED':
|
||||
case 'REJECTED':
|
||||
return decrementFlaggedUserCount(root);
|
||||
default:
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import { mapProps } from 'recompose';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import get from 'lodash/get';
|
||||
|
||||
const withData = withQuery(
|
||||
gql`
|
||||
query TalkAdmin_CommunityMenu {
|
||||
flaggedUsernamesCount: userCount(
|
||||
query: {
|
||||
action_type: FLAG
|
||||
state: { status: { username: [SET, CHANGED] } }
|
||||
}
|
||||
)
|
||||
}
|
||||
`,
|
||||
{
|
||||
options: {
|
||||
fetchPolicy: 'cache-only',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default compose(
|
||||
withData,
|
||||
mapProps(ownProps => ({
|
||||
flaggedUsernamesCount: get(ownProps, 'root.flaggedUsernamesCount'),
|
||||
}))
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './components/Community';
|
||||
@@ -0,0 +1,3 @@
|
||||
export function isFlaggedUserDangling(user) {
|
||||
return ['APPROVED', 'REJECTED'].includes(user.state.status.username.status);
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export default class Configure extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
auth: { user },
|
||||
currentUser,
|
||||
canSave,
|
||||
savePending,
|
||||
setActiveSection,
|
||||
@@ -32,7 +32,7 @@ export default class Configure extends Component {
|
||||
} = this.props;
|
||||
const SectionComponent = this.getSectionComponent(activeSection);
|
||||
|
||||
if (!can(user, 'UPDATE_CONFIG')) {
|
||||
if (!can(currentUser, 'UPDATE_CONFIG')) {
|
||||
return (
|
||||
<p>
|
||||
You must be an administrator to access config settings. Please find
|
||||
@@ -86,9 +86,8 @@ export default class Configure extends Component {
|
||||
}
|
||||
|
||||
Configure.propTypes = {
|
||||
notify: PropTypes.func.isRequired,
|
||||
savePending: PropTypes.func.isRequired,
|
||||
auth: PropTypes.object.isRequired,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
|
||||
@@ -4,10 +4,9 @@ import { bindActionCreators } from 'redux';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import { withQuery, withMergedSettings } from 'coral-framework/hocs';
|
||||
import { Spinner } from 'coral-ui';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withUpdateSettings } from 'coral-framework/graphql/mutations';
|
||||
import { getErrorMessages, getDefinitionName } from 'coral-framework/utils';
|
||||
import { getDefinitionName } from 'coral-framework/utils';
|
||||
import StreamSettings from './StreamSettings';
|
||||
import TechSettings from './TechSettings';
|
||||
import ModerationSettings from './ModerationSettings';
|
||||
@@ -16,23 +15,22 @@ import Configure from '../components/Configure';
|
||||
|
||||
class ConfigureContainer extends Component {
|
||||
savePending = async () => {
|
||||
try {
|
||||
await this.props.updateSettings(this.props.pending);
|
||||
this.props.clearPending();
|
||||
} catch (err) {
|
||||
this.props.notify('error', getErrorMessages(err));
|
||||
}
|
||||
await this.props.updateSettings(this.props.pending);
|
||||
this.props.clearPending();
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.props.data.error) {
|
||||
return <div>{this.props.data.error.message}</div>;
|
||||
}
|
||||
|
||||
if (this.props.data.loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Configure
|
||||
notify={this.props.notify}
|
||||
auth={this.props.auth}
|
||||
currentUser={this.props.currentUser}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
settings={this.props.mergedSettings}
|
||||
@@ -67,12 +65,13 @@ const withConfigureQuery = withQuery(
|
||||
{
|
||||
options: () => ({
|
||||
variables: {},
|
||||
fetchPolicy: 'network-only',
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
auth: state.auth,
|
||||
currentUser: state.auth.user,
|
||||
pending: state.configure.pending,
|
||||
canSave: state.configure.canSave,
|
||||
activeSection: state.configure.activeSection,
|
||||
@@ -81,7 +80,6 @@ const mapStateToProps = state => ({
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
notify,
|
||||
clearPending,
|
||||
setActiveSection,
|
||||
},
|
||||
@@ -89,9 +87,9 @@ const mapDispatchToProps = dispatch =>
|
||||
);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withUpdateSettings,
|
||||
withConfigureQuery,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withMergedSettings('root.settings', 'pending', 'mergedSettings')
|
||||
)(ConfigureContainer);
|
||||
|
||||
@@ -99,8 +97,7 @@ ConfigureContainer.propTypes = {
|
||||
updateSettings: PropTypes.func.isRequired,
|
||||
clearPending: PropTypes.func.isRequired,
|
||||
setActiveSection: PropTypes.func.isRequired,
|
||||
notify: PropTypes.func.isRequired,
|
||||
auth: PropTypes.object.isRequired,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
canSave: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import styles from './Install.css';
|
||||
import { Wizard, WizardNav } from 'coral-ui';
|
||||
import Layout from 'coral-admin/src/components/ui/Layout';
|
||||
import Layout from 'coral-admin/src/components/Layout';
|
||||
|
||||
import InitialStep from './Steps/InitialStep';
|
||||
import AddOrganizationName from './Steps/AddOrganizationName';
|
||||
|
||||
@@ -143,7 +143,6 @@
|
||||
|
||||
i {
|
||||
font-size: 12px;
|
||||
top: 2px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import styles from './Comment.css';
|
||||
import CommentLabels from 'coral-admin/src/components/CommentLabels';
|
||||
import CommentAnimatedEdit from 'coral-admin/src/components/CommentAnimatedEdit';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
|
||||
import CommentFormatter from 'coral-admin/src/components/CommentFormatter';
|
||||
import IfHasLink from 'coral-admin/src/components/IfHasLink';
|
||||
import cn from 'classnames';
|
||||
import ApproveButton from 'coral-admin/src/components/ApproveButton';
|
||||
@@ -126,11 +126,12 @@ class Comment extends React.Component {
|
||||
<CommentAnimatedEdit body={comment.body}>
|
||||
<div className={styles.itemBody}>
|
||||
<div className={styles.body}>
|
||||
<CommentBodyHighlighter
|
||||
<CommentFormatter
|
||||
suspectWords={settings.wordlist.suspect}
|
||||
bannedWords={settings.wordlist.banned}
|
||||
className="talk-admin-comment"
|
||||
body={comment.body}
|
||||
/>{' '}
|
||||
/>
|
||||
<a
|
||||
className={styles.external}
|
||||
href={`${comment.asset.url}?commentId=${comment.id}`}
|
||||
|
||||
@@ -19,7 +19,7 @@ class Moderation extends Component {
|
||||
componentWillMount() {
|
||||
const { toggleModal, singleView } = this.props;
|
||||
|
||||
key('s', () => singleView());
|
||||
key('z', () => singleView());
|
||||
key('shift+/', () => toggleModal(true));
|
||||
key('esc', () => toggleModal(false));
|
||||
key('ctrl+f', () => this.openSearch());
|
||||
@@ -113,7 +113,7 @@ class Moderation extends Component {
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
key.unbind('s');
|
||||
key.unbind('z');
|
||||
key.unbind('shift+/');
|
||||
key.unbind('esc');
|
||||
key.unbind('ctrl+f');
|
||||
@@ -185,7 +185,7 @@ class Moderation extends Component {
|
||||
commentBelongToQueue={this.props.commentBelongToQueue}
|
||||
isLoadingMore={this.state.isLoadingMore}
|
||||
commentCount={activeTabCount}
|
||||
currentUserId={this.props.auth.user.id}
|
||||
currentUserId={this.props.currentUser.id}
|
||||
viewUserDetail={viewUserDetail}
|
||||
selectCommentId={props.selectCommentId}
|
||||
cleanUpQueue={props.cleanUpQueue}
|
||||
@@ -225,7 +225,7 @@ Moderation.propTypes = {
|
||||
cleanUpQueue: PropTypes.func.isRequired,
|
||||
storySearchChange: PropTypes.func.isRequired,
|
||||
moderation: PropTypes.object.isRequired,
|
||||
auth: PropTypes.object.isRequired,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
queueConfig: PropTypes.object.isRequired,
|
||||
commentBelongToQueue: PropTypes.func.isRequired,
|
||||
handleCommentChange: PropTypes.func.isRequired,
|
||||
|
||||
@@ -397,6 +397,13 @@ class ModerationQueue extends React.Component {
|
||||
const index = comments.findIndex(
|
||||
comment => comment.id === selectedCommentId
|
||||
);
|
||||
|
||||
// This can happen temporarily when we call redux to change the selected comment
|
||||
// but it didn't fully take effect yet.
|
||||
if (index === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const comment = comments[index];
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
@@ -414,7 +421,7 @@ class ModerationQueue extends React.Component {
|
||||
dangling={
|
||||
!this.props.commentBelongToQueue(this.props.activeTab, comment)
|
||||
}
|
||||
/>;
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,12 +19,12 @@ class ViewOptions extends React.Component {
|
||||
'talk-admin-moderation-view-options-headline'
|
||||
)}
|
||||
>
|
||||
View Options
|
||||
{t('admin_sidebar.view_options')}
|
||||
</h2>
|
||||
<div className={styles.viewOptionsContent}>
|
||||
<ul className={styles.viewOptionsList}>
|
||||
<li className={styles.viewOptionsItem}>
|
||||
Sort Comments
|
||||
{t('admin_sidebar.sort_comments')}
|
||||
<Dropdown
|
||||
containerClassName={styles.dropdownContainer}
|
||||
toggleClassName={styles.dropdownToggle}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
import React, { Component } from 'react';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import Indicator from '../../../components/Indicator';
|
||||
import { withFragments } from 'plugin-api/beta/client/hocs';
|
||||
import { handleIndicatorChange, subscriptionFields } from '../graphql';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import withQueueConfig from '../hoc/withQueueConfig';
|
||||
import baseQueueConfig from '../queueConfig';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
|
||||
class IndicatorContainer extends Component {
|
||||
subscriptions = [];
|
||||
|
||||
handleCommentChange = (root, comment) => {
|
||||
return handleIndicatorChange(root, comment, this.props.queueConfig);
|
||||
};
|
||||
|
||||
subscribeToUpdates() {
|
||||
const parameters = [
|
||||
{
|
||||
document: COMMENT_ADDED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentAdded: comment } } }
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: COMMENT_FLAGGED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentFlagged: comment } } }
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: COMMENT_EDITED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentEdited: comment } } }
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: COMMENT_ACCEPTED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentAccepted: comment } } }
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: COMMENT_REJECTED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentRejected: comment } } }
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
},
|
||||
{
|
||||
document: COMMENT_RESET_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentReset: comment } } }
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
this.subscriptions = parameters.map(param =>
|
||||
this.props.data.subscribeToMore(param)
|
||||
);
|
||||
}
|
||||
|
||||
unsubscribe() {
|
||||
this.subscriptions.forEach(unsubscribe => unsubscribe());
|
||||
this.subscriptions = [];
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (this.props.track) {
|
||||
this.subscribeToUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.props.track && nextProps.track) {
|
||||
this.subscribeToUpdates();
|
||||
}
|
||||
if (this.props.track && !nextProps.track) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (
|
||||
!this.props.root ||
|
||||
(!this.props.root.premodCount && !this.props.root.reportedCount)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Indicator />
|
||||
<Slot
|
||||
data={this.props.data}
|
||||
handleCommentChange={this.handleCommentChange}
|
||||
fill="adminModerationIndicator"
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IndicatorContainer.propTypes = {
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
track: PropTypes.bool,
|
||||
queueConfig: PropTypes.object,
|
||||
};
|
||||
|
||||
const COMMENT_ADDED_SUBSCRIPTION = gql`
|
||||
subscription TalkAdmin_ModerationIndicator_CommentAdded {
|
||||
commentAdded(statuses: null) {
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const COMMENT_FLAGGED_SUBSCRIPTION = gql`
|
||||
subscription TalkAdmin_ModerationIndicator_CommentFlagged {
|
||||
commentFlagged {
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const COMMENT_EDITED_SUBSCRIPTION = gql`
|
||||
subscription TalkAdmin_ModerationIndicator_CommentEdited {
|
||||
commentEdited {
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const COMMENT_ACCEPTED_SUBSCRIPTION = gql`
|
||||
subscription TalkAdmin_ModerationIndicator_CommentAccepted {
|
||||
commentAccepted {
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const COMMENT_REJECTED_SUBSCRIPTION = gql`
|
||||
subscription TalkAdmin_ModerationIndicator_CommentRejected {
|
||||
commentRejected {
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const COMMENT_RESET_SUBSCRIPTION = gql`
|
||||
subscription TalkAdmin_ModerationIndicator_CommentReset {
|
||||
commentReset {
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
track: state.moderation.indicatorTrack,
|
||||
});
|
||||
|
||||
const enhance = compose(
|
||||
connect(mapStateToProps),
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment TalkAdmin_Moderation_Indicator_root on RootQuery {
|
||||
premodCount: commentCount(
|
||||
query: { statuses: [PREMOD], asset_id: $nullID }
|
||||
)
|
||||
reportedCount: commentCount(
|
||||
query: {
|
||||
statuses: [NONE, PREMOD, SYSTEM_WITHHELD]
|
||||
action_type: FLAG
|
||||
asset_id: $nullID
|
||||
}
|
||||
)
|
||||
}
|
||||
`,
|
||||
}),
|
||||
withQueueConfig(baseQueueConfig)
|
||||
);
|
||||
|
||||
export default enhance(IndicatorContainer);
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
handleCommentChange,
|
||||
commentBelongToQueue,
|
||||
cleanUpQueue,
|
||||
subscriptionFields,
|
||||
} from '../graphql';
|
||||
|
||||
import { viewUserDetail } from '../../../actions/userDetail';
|
||||
@@ -27,6 +28,7 @@ import {
|
||||
storySearchChange,
|
||||
clearState,
|
||||
selectCommentId,
|
||||
setIndicatorTrack,
|
||||
} from 'actions/moderation';
|
||||
import withQueueConfig from '../hoc/withQueueConfig';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
@@ -108,7 +110,7 @@ class ModerationContainer extends Component {
|
||||
comment.status_history[comment.status_history.length - 1]
|
||||
.assigned_by;
|
||||
const notifyText =
|
||||
this.props.auth.user.id === user.id
|
||||
this.props.currentUser.id === user.id
|
||||
? ''
|
||||
: t(
|
||||
'modqueue.notify_accepted',
|
||||
@@ -129,7 +131,7 @@ class ModerationContainer extends Component {
|
||||
comment.status_history[comment.status_history.length - 1]
|
||||
.assigned_by;
|
||||
const notifyText =
|
||||
this.props.auth.user.id === user.id
|
||||
this.props.currentUser.id === user.id
|
||||
? ''
|
||||
: t(
|
||||
'modqueue.notify_rejected',
|
||||
@@ -150,7 +152,7 @@ class ModerationContainer extends Component {
|
||||
comment.status_history[comment.status_history.length - 1]
|
||||
.assigned_by;
|
||||
const notifyText =
|
||||
this.props.auth.user.id === user.id
|
||||
this.props.currentUser.id === user.id
|
||||
? ''
|
||||
: t(
|
||||
'modqueue.notify_reset',
|
||||
@@ -198,21 +200,42 @@ class ModerationContainer extends Component {
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (!this.props.data.variables.asset_id) {
|
||||
// Stop activity indicator tracking, as we'll handle it here.
|
||||
this.props.setIndicatorTrack(false);
|
||||
}
|
||||
this.props.clearState();
|
||||
this.subscribeToUpdates();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (!this.props.data.variables.asset_id) {
|
||||
// Restart activity indicator tracking.
|
||||
this.props.setIndicatorTrack(true);
|
||||
}
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const currentAssetId = this.props.data.variables.asset_id;
|
||||
const nextAssetId = nextProps.data.variables.asset_id;
|
||||
|
||||
// Resubscribe when we change between assets.
|
||||
if (
|
||||
this.props.data.variables.asset_id !== nextProps.data.variables.asset_id
|
||||
) {
|
||||
if (currentAssetId !== nextAssetId) {
|
||||
this.resubscribe(nextProps.data.variables);
|
||||
}
|
||||
|
||||
// We are only subscribing to a specific asset_id, so activity indicator
|
||||
// needs to do its own tracking.
|
||||
if (!currentAssetId && nextAssetId) {
|
||||
this.props.setIndicatorTrack(true);
|
||||
}
|
||||
|
||||
// We are subscribing to all comment changes, and as such there is no
|
||||
// need for the activity indicator to do the same.
|
||||
if (currentAssetId && !nextAssetId) {
|
||||
this.props.setIndicatorTrack(false);
|
||||
}
|
||||
}
|
||||
|
||||
cleanUpQueue = queue => {
|
||||
@@ -269,10 +292,6 @@ class ModerationContainer extends Component {
|
||||
const { root, root: { asset, settings }, data } = this.props;
|
||||
const assetId = getAssetId(this.props);
|
||||
|
||||
if (data.error) {
|
||||
return <div>Error</div>;
|
||||
}
|
||||
|
||||
if (assetId) {
|
||||
if (asset === null) {
|
||||
// Not found.
|
||||
@@ -280,7 +299,11 @@ class ModerationContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
if (data.loading) {
|
||||
if (data.error) {
|
||||
return <div>{data.error.message}</div>;
|
||||
}
|
||||
|
||||
if (data.loading && data.networkStatus !== 3) {
|
||||
// loading.
|
||||
return <Spinner />;
|
||||
}
|
||||
@@ -316,10 +339,12 @@ class ModerationContainer extends Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const COMMENT_ADDED_SUBSCRIPTION = gql`
|
||||
subscription CommentAdded($asset_id: ID){
|
||||
commentAdded(asset_id: $asset_id, statuses: null){
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
${Comment.fragments.comment}
|
||||
@@ -329,6 +354,7 @@ const COMMENT_EDITED_SUBSCRIPTION = gql`
|
||||
subscription CommentEdited($asset_id: ID){
|
||||
commentEdited(asset_id: $asset_id){
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
${Comment.fragments.comment}
|
||||
@@ -338,6 +364,7 @@ const COMMENT_FLAGGED_SUBSCRIPTION = gql`
|
||||
subscription CommentFlagged($asset_id: ID){
|
||||
commentFlagged(asset_id: $asset_id){
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
${Comment.fragments.comment}
|
||||
@@ -347,14 +374,7 @@ const COMMENT_ACCEPTED_SUBSCRIPTION = gql`
|
||||
subscription CommentAccepted($asset_id: ID){
|
||||
commentAccepted(asset_id: $asset_id){
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
status_history {
|
||||
type
|
||||
created_at
|
||||
assigned_by {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
${Comment.fragments.comment}
|
||||
@@ -364,14 +384,7 @@ const COMMENT_REJECTED_SUBSCRIPTION = gql`
|
||||
subscription CommentRejected($asset_id: ID){
|
||||
commentRejected(asset_id: $asset_id){
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
status_history {
|
||||
type
|
||||
created_at
|
||||
assigned_by {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
${Comment.fragments.comment}
|
||||
@@ -381,14 +394,7 @@ const COMMENT_RESET_SUBSCRIPTION = gql`
|
||||
subscription CommentReset($asset_id: ID){
|
||||
commentReset(asset_id: $asset_id){
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
status_history {
|
||||
type
|
||||
created_at
|
||||
assigned_by {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
${subscriptionFields}
|
||||
}
|
||||
}
|
||||
${Comment.fragments.comment}
|
||||
@@ -509,7 +515,7 @@ const withModQueueQuery = withQuery(
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
moderation: state.moderation,
|
||||
auth: state.auth,
|
||||
currentUser: state.auth.user,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
@@ -525,6 +531,7 @@ const mapDispatchToProps = dispatch => ({
|
||||
clearState,
|
||||
notify,
|
||||
selectCommentId,
|
||||
setIndicatorTrack,
|
||||
},
|
||||
dispatch
|
||||
),
|
||||
|
||||
@@ -20,20 +20,12 @@ function queueHasComment(root, queue, id) {
|
||||
return root[queue].nodes.find(c => c.id === id);
|
||||
}
|
||||
|
||||
function removeCommentFromQueue(root, queue, id, dangling = false) {
|
||||
if (!queueHasComment(root, queue, id)) {
|
||||
return root;
|
||||
}
|
||||
function removeCommentFromQueue(root, queue, id) {
|
||||
const changes = {
|
||||
[`${queue}Count`]: { $set: root[`${queue}Count`] - 1 },
|
||||
};
|
||||
|
||||
if (!dangling) {
|
||||
changes[queue] = {
|
||||
[queue]: {
|
||||
nodes: { $apply: nodes => nodes.filter(c => c.id !== id) },
|
||||
};
|
||||
}
|
||||
|
||||
},
|
||||
};
|
||||
return update(root, changes);
|
||||
}
|
||||
|
||||
@@ -48,19 +40,21 @@ function shouldCommentBeAdded(root, queue, comment, sortOrder) {
|
||||
: new Date(comment.created_at) >= cursor;
|
||||
}
|
||||
|
||||
function addCommentToQueue(root, queue, comment, sortOrder, cleanup) {
|
||||
if (queueHasComment(root, queue, comment.id)) {
|
||||
return root;
|
||||
}
|
||||
|
||||
function increaseCommentCount(root, queue) {
|
||||
const changes = {
|
||||
[`${queue}Count`]: { $set: root[`${queue}Count`] + 1 },
|
||||
};
|
||||
return update(root, changes);
|
||||
}
|
||||
|
||||
if (!shouldCommentBeAdded(root, queue, comment, sortOrder)) {
|
||||
return update(root, changes);
|
||||
}
|
||||
function decreaseCommentCount(root, queue) {
|
||||
const changes = {
|
||||
[`${queue}Count`]: { $set: root[`${queue}Count`] - 1 },
|
||||
};
|
||||
return update(root, changes);
|
||||
}
|
||||
|
||||
function addCommentToQueue(root, queue, comment, sortOrder) {
|
||||
const cursor = new Date(root[queue].startCursor);
|
||||
const date = new Date(comment.created_at);
|
||||
|
||||
@@ -70,17 +64,13 @@ function addCommentToQueue(root, queue, comment, sortOrder, cleanup) {
|
||||
? root[queue].nodes.concat(comment)
|
||||
: [comment].concat(...root[queue].nodes);
|
||||
|
||||
changes[queue] = {
|
||||
nodes: { $set: nodes },
|
||||
const changes = {
|
||||
[queue]: {
|
||||
nodes: { $set: nodes },
|
||||
},
|
||||
};
|
||||
|
||||
const next = update(root, changes);
|
||||
|
||||
if (!cleanup) {
|
||||
return next;
|
||||
}
|
||||
|
||||
return cleanUpQueue(next, queue, sortOrder);
|
||||
return update(root, changes);
|
||||
}
|
||||
|
||||
function sortComments(nodes, sortOrder) {
|
||||
@@ -101,6 +91,87 @@ function getCommentQueues(comment, queueConfig) {
|
||||
return queues;
|
||||
}
|
||||
|
||||
function getOlderDate(a, b) {
|
||||
if (a) {
|
||||
a = new Date(a);
|
||||
}
|
||||
if (b) {
|
||||
b = new Date(b);
|
||||
}
|
||||
|
||||
if (!b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
if (!a) {
|
||||
return b;
|
||||
}
|
||||
return a < b ? b : a;
|
||||
}
|
||||
|
||||
function determineLatestChange(comment) {
|
||||
let lc = null;
|
||||
|
||||
comment.body_history.forEach(item => {
|
||||
lc = getOlderDate(lc, item.created_at);
|
||||
});
|
||||
|
||||
comment.status_history.forEach(item => {
|
||||
lc = getOlderDate(lc, item.created_at);
|
||||
});
|
||||
|
||||
comment.actions.forEach(item => {
|
||||
lc = getOlderDate(lc, item.created_at);
|
||||
});
|
||||
|
||||
return lc;
|
||||
}
|
||||
|
||||
function reconstructPreviousCommentState(comment) {
|
||||
const statusHistory = comment.status_history;
|
||||
const bodyHistory = comment.body_history;
|
||||
const actions = comment.actions;
|
||||
const lastChangeDate = determineLatestChange(comment);
|
||||
const previousComment = {
|
||||
...comment,
|
||||
body_history: bodyHistory.filter(
|
||||
item => new Date(item.created_at) < lastChangeDate
|
||||
),
|
||||
status_history: statusHistory.filter(
|
||||
item => new Date(item.created_at) < lastChangeDate
|
||||
),
|
||||
actions: actions.filter(item => new Date(item.created_at) < lastChangeDate),
|
||||
};
|
||||
|
||||
// Comment did not exist previously.
|
||||
if (!previousComment.status_history.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
previousComment.status =
|
||||
previousComment.status_history[
|
||||
previousComment.status_history.length - 1
|
||||
].type;
|
||||
|
||||
previousComment.body =
|
||||
previousComment.body_history[previousComment.body_history.length - 1].body;
|
||||
|
||||
return previousComment;
|
||||
}
|
||||
|
||||
/**
|
||||
* getPreviousCommentQueues determines queues that this comment previously belonged to.
|
||||
*/
|
||||
function getPreviousCommentQueues(comment, queueConfig) {
|
||||
const previousCommentState = reconstructPreviousCommentState(comment);
|
||||
|
||||
if (!previousCommentState) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return getCommentQueues(previousCommentState, queueConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether or not the comment belongs to the queue.
|
||||
*/
|
||||
@@ -194,13 +265,13 @@ export function cleanUpQueue(root, queue, sortOrder, queueConfig) {
|
||||
|
||||
/**
|
||||
* Assimilate comment changes into current store.
|
||||
* @param {Object} root current state of the store
|
||||
* @param {Object} comment comment that was changed
|
||||
* @param {string} sortOrder current sort order of the queues
|
||||
* @param {string} notify callback to show notification
|
||||
* in the current active queue besides the 'all' queue.
|
||||
* @param {Object} queueConfig queue configuration
|
||||
* @return {Object} next state of the store
|
||||
* @param {Object} root current state of the store
|
||||
* @param {Object} comment comment that was changed
|
||||
* @param {string} sortOrder current sort order of the queues
|
||||
* @param {function} notify callback to show notification
|
||||
* in the current active queue besides the 'all' queue.
|
||||
* @param {Object} queueConfig queue configuration
|
||||
* @return {Object} next state of the store
|
||||
*/
|
||||
export function handleCommentChange(
|
||||
root,
|
||||
@@ -212,6 +283,10 @@ export function handleCommentChange(
|
||||
) {
|
||||
let next = root;
|
||||
|
||||
// Queues that this comment previously belonged to.
|
||||
const prevQueues = getPreviousCommentQueues(comment, queueConfig);
|
||||
|
||||
// Queues that this comment needs to be placed.
|
||||
const nextQueues = getCommentQueues(comment, queueConfig);
|
||||
|
||||
let notificationShown = false;
|
||||
@@ -224,15 +299,26 @@ export function handleCommentChange(
|
||||
};
|
||||
|
||||
Object.keys(queueConfig).forEach(queue => {
|
||||
// Comment should be placed in queue.
|
||||
if (nextQueues.indexOf(queue) >= 0) {
|
||||
// Comment was not previously in this queue.
|
||||
if (prevQueues.indexOf(queue) === -1) {
|
||||
// Increase count.
|
||||
next = increaseCommentCount(next, queue);
|
||||
}
|
||||
|
||||
if (!queueHasComment(next, queue, comment.id)) {
|
||||
next = addCommentToQueue(
|
||||
next,
|
||||
queue,
|
||||
comment,
|
||||
sortOrder,
|
||||
activeQueue !== queue
|
||||
);
|
||||
// Check if comment would be in the current view.
|
||||
if (shouldCommentBeAdded(root, queue, comment, sortOrder)) {
|
||||
next = addCommentToQueue(next, queue, comment, sortOrder);
|
||||
|
||||
// This will limit queue sizes.
|
||||
if (activeQueue !== queue) {
|
||||
cleanUpQueue(next, queue, sortOrder);
|
||||
}
|
||||
}
|
||||
|
||||
// Show notification if end of list is visible.
|
||||
if (
|
||||
notify &&
|
||||
activeQueue === queue &&
|
||||
@@ -241,17 +327,30 @@ export function handleCommentChange(
|
||||
showNotificationOnce();
|
||||
}
|
||||
}
|
||||
} else if (queueHasComment(next, queue, comment.id)) {
|
||||
} else if (prevQueues.indexOf(queue) >= 0) {
|
||||
// Comment previously was in this queue, but not anymore.
|
||||
|
||||
// If action was performed by another user, keep a dangling comment.
|
||||
const dangling =
|
||||
activeQueue === queue &&
|
||||
comment.status_history[comment.status_history.length - 1].assigned_by
|
||||
.id !== root.me.id;
|
||||
next = removeCommentFromQueue(next, queue, comment.id, dangling);
|
||||
if (notify && isVisible(comment.id)) {
|
||||
showNotificationOnce();
|
||||
|
||||
// Otherwise remove it
|
||||
if (!dangling) {
|
||||
next = removeCommentFromQueue(next, queue, comment.id);
|
||||
}
|
||||
|
||||
// In any case decrease comment count.
|
||||
next = decreaseCommentCount(next, queue);
|
||||
} else if (queueHasComment(next, queue, comment.id)) {
|
||||
// Comment does not belong to his queue and did not belong to this queue previously.
|
||||
// Must be a dangling comment that the current user took action on.
|
||||
// Remove it completely from the queue.
|
||||
next = removeCommentFromQueue(next, queue, comment.id);
|
||||
}
|
||||
|
||||
// Show notification if operation affected a currently visible comment.
|
||||
if (notify && isVisible(comment.id)) {
|
||||
showNotificationOnce();
|
||||
}
|
||||
@@ -263,3 +362,55 @@ export function handleCommentChange(
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
const indicatorQueues = ['premod', 'reported'];
|
||||
|
||||
/**
|
||||
* Track indicator status
|
||||
* @param {Object} root current state of the store
|
||||
* @param {Object} comment comment that was changed
|
||||
* @return {Object} next state of the store
|
||||
*/
|
||||
export function handleIndicatorChange(root, comment, queueConfig) {
|
||||
let next = root;
|
||||
|
||||
// Queues that this comment previously belonged to.
|
||||
const prevQueues = getPreviousCommentQueues(comment, queueConfig);
|
||||
|
||||
// Queues that this comment needs to be placed.
|
||||
const nextQueues = getCommentQueues(comment, queueConfig);
|
||||
|
||||
for (const queue of indicatorQueues) {
|
||||
if (prevQueues.indexOf(queue) === -1 && nextQueues.indexOf(queue) >= 0) {
|
||||
next = increaseCommentCount(next, queue);
|
||||
}
|
||||
if (prevQueues.indexOf(queue) >= 0 && nextQueues.indexOf(queue) === -1) {
|
||||
next = decreaseCommentCount(next, queue);
|
||||
}
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
export const subscriptionFields = `
|
||||
status
|
||||
body
|
||||
body_history {
|
||||
body
|
||||
created_at
|
||||
}
|
||||
actions {
|
||||
__typename
|
||||
created_at
|
||||
}
|
||||
status_history {
|
||||
type
|
||||
assigned_by {
|
||||
id
|
||||
username
|
||||
}
|
||||
created_at
|
||||
}
|
||||
updated_at
|
||||
created_at
|
||||
`;
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
width: 100%;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
white-space: pre-wrap;
|
||||
|
||||
a {
|
||||
color: rgb(44, 44, 44);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Get the auth element and parse it as JSON by decoding it.
|
||||
const auth = document.getElementById('auth');
|
||||
const doc = document.implementation.createHTMLDocument('');
|
||||
doc.body.innerHTML = auth.innerText;
|
||||
|
||||
// Set the item in localStorage.
|
||||
localStorage.setItem('auth', doc.body.textContent);
|
||||
|
||||
// Close the window.
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
}, 50);
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Router, Route } from 'react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Embed from './containers/Embed';
|
||||
import { LoginContainer } from 'coral-sign-in/containers/LoginContainer';
|
||||
|
||||
const routes = (
|
||||
<div>
|
||||
<Route exact path="/embed/stream/login" component={LoginContainer} />
|
||||
<Route path="*" component={Embed} />
|
||||
</div>
|
||||
);
|
||||
|
||||
class AppRouter extends React.Component {
|
||||
static contextTypes = {
|
||||
history: PropTypes.object,
|
||||
};
|
||||
|
||||
render() {
|
||||
return <Router history={this.context.history} routes={routes} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default AppRouter;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user