mirror of
https://github.com/wassname/talk.git
synced 2026-09-14 11:36:51 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eda9a0cadf | ||
|
|
763e091e3f | ||
|
|
02d5a503c4 | ||
|
|
13ffeb5889 | ||
|
|
b50fde619e | ||
|
|
7348bfc7c9 | ||
|
|
b27bfda759 | ||
|
|
1815bd4d75 | ||
|
|
8c29b0f1d4 | ||
|
|
eb66cef4fc | ||
|
|
17a0297911 | ||
|
|
a1edbcf974 | ||
|
|
8c304c762d | ||
|
|
d64af05093 | ||
|
|
8aff2f69b3 | ||
|
|
4e9cf2b912 | ||
|
|
b10141716f | ||
|
|
4973a2021d | ||
|
|
7b86ec7d0a | ||
|
|
e14af46c68 | ||
|
|
74dbfda633 | ||
|
|
44ba77b202 | ||
|
|
9cee164a15 | ||
|
|
231dfe7997 | ||
|
|
d02bb20333 | ||
|
|
e9c18faf32 | ||
|
|
2f94674b78 | ||
|
|
b7b60e6436 | ||
|
|
778064d756 | ||
|
|
57917335cb | ||
|
|
7926c5ccbf | ||
|
|
7aad40b0e3 | ||
|
|
a7c9f01125 | ||
|
|
e74b8c9099 | ||
|
|
c6a76990f4 | ||
|
|
08a857e07c | ||
|
|
10a99aab88 | ||
|
|
15c3a96291 | ||
|
|
7b0a7f7d9a | ||
|
|
926364ad72 | ||
|
|
0e7961aa1c | ||
|
|
890730a26d | ||
|
|
a2f5af69b4 | ||
|
|
0d1f3d1da3 | ||
|
|
1c7606b651 | ||
|
|
7a32bf855d | ||
|
|
cb50a1e709 | ||
|
|
86595b4dd0 | ||
|
|
fa2730a661 | ||
|
|
01039aa411 | ||
|
|
732ff98f9b | ||
|
|
3024300a3f | ||
|
|
7f543f543b | ||
|
|
c9957add72 | ||
|
|
2b1108cd88 | ||
|
|
5953ed711a | ||
|
|
8c2733734a | ||
|
|
66bb61ed13 | ||
|
|
f2ea3bdd42 | ||
|
|
71bda42cfb | ||
|
|
030b805290 | ||
|
|
1ca7fd58bd | ||
|
|
7aa31b7cb2 | ||
|
|
4fb511e488 | ||
|
|
e3fa99100b | ||
|
|
5aeb16fc99 | ||
|
|
21e92b92f3 | ||
|
|
ecf05d76c0 | ||
|
|
0f47378ae3 | ||
|
|
46e5efbd55 | ||
|
|
06abccabb4 | ||
|
|
565ba2af06 | ||
|
|
802a3bd656 | ||
|
|
413b6a2a60 | ||
|
|
e37346f544 | ||
|
|
ce92765218 | ||
|
|
8b5cb9d96d | ||
|
|
7cf991d672 | ||
|
|
4312b8d024 | ||
|
|
910beb0f40 | ||
|
|
810cb222f9 | ||
|
|
7a7dcdd7c6 | ||
|
|
66f1fd7d8a | ||
|
|
7362c6c993 | ||
|
|
a0fbfe4ff4 | ||
|
|
103543f830 | ||
|
|
f2d8e49d5b | ||
|
|
ade9b99a09 | ||
|
|
6bad9e5306 | ||
|
|
2a5075f434 | ||
|
|
d41984f171 | ||
|
|
4bc8a167a4 | ||
|
|
9a8f0e98a4 | ||
|
|
05ec8eaee2 | ||
|
|
eabcbc36c6 | ||
|
|
822193bba5 | ||
|
|
a008503b52 | ||
|
|
401593afde | ||
|
|
7f9c71a802 | ||
|
|
f3a3746d6b | ||
|
|
e51736040f | ||
|
|
9d47d3ff91 | ||
|
|
15df20e720 | ||
|
|
00b6f0816c | ||
|
|
949925586f | ||
|
|
3fbd655cb8 | ||
|
|
848ec34e99 | ||
|
|
fa66f07db6 | ||
|
|
ea71b779d2 | ||
|
|
b5958d5879 | ||
|
|
35cefd5570 | ||
|
|
31cb70fa77 | ||
|
|
aee963ce39 | ||
|
|
bc3ceac61a | ||
|
|
ae9732c9b4 | ||
|
|
c783d15df3 | ||
|
|
e9fdaa1b5a | ||
|
|
8bfe344c39 | ||
|
|
a20e050559 | ||
|
|
58b73a4d0a | ||
|
|
2e7bad3508 | ||
|
|
bfda6a4ce8 | ||
|
|
9d448d4185 | ||
|
|
68dfb9cad7 | ||
|
|
3e9d835eb2 | ||
|
|
f5099d94bc | ||
|
|
d9806c0ecc | ||
|
|
edf32a911d | ||
|
|
903b74cb31 | ||
|
|
070ecbaafc | ||
|
|
f9f7855775 | ||
|
|
438c515d2f | ||
|
|
65aa90904a | ||
|
|
140ef3eb5e | ||
|
|
467aecf0ee | ||
|
|
10dbce55ba | ||
|
|
6cfebb408f | ||
|
|
29a840fe06 | ||
|
|
690e6752ee | ||
|
|
788cf25fa5 | ||
|
|
687d0555c8 | ||
|
|
bc9e76c889 | ||
|
|
f173f495a5 | ||
|
|
2b52f07c45 | ||
|
|
8a9af89699 | ||
|
|
64cb665124 | ||
|
|
df29e610b3 | ||
|
|
20c5804559 | ||
|
|
06b08756e7 | ||
|
|
e23933e9df | ||
|
|
e1f6dc49a8 | ||
|
|
8caf995517 | ||
|
|
287cff289b | ||
|
|
368b6fb847 | ||
|
|
3b5a45ec01 | ||
|
|
0e20ddb2fd | ||
|
|
f6d3f4cb14 | ||
|
|
375f1bb6b1 | ||
|
|
6485b1f080 | ||
|
|
8b35f2300a | ||
|
|
0caa82c1c4 | ||
|
|
94b3ab4ac8 | ||
|
|
8fee07a3f8 | ||
|
|
12701942c6 | ||
|
|
bab9347d06 | ||
|
|
b995da8301 | ||
|
|
51847cf955 | ||
|
|
ee0f734705 | ||
|
|
87d0c99558 | ||
|
|
225a04fed1 | ||
|
|
a42135c3bb | ||
|
|
a9132c1353 | ||
|
|
fff6e1c231 | ||
|
|
0791a2731b | ||
|
|
aa6c9890b2 | ||
|
|
a649bd8252 | ||
|
|
59cac9db8b | ||
|
|
478770f6c6 | ||
|
|
262c68ca8f | ||
|
|
e409695979 | ||
|
|
ccb62263c1 | ||
|
|
74acf73396 | ||
|
|
cdae2796a5 | ||
|
|
09d20257ac | ||
|
|
1bf440c4a6 | ||
|
|
3b2deeb007 | ||
|
|
b04fc1faf8 | ||
|
|
db11dc4928 | ||
|
|
bfb4b790d4 | ||
|
|
c7db780582 | ||
|
|
45c75f3c48 | ||
|
|
c1492d1065 | ||
|
|
0159918049 | ||
|
|
cb9b02c180 | ||
|
|
c8e5678c9c | ||
|
|
48a78735bd | ||
|
|
8dcaf8ea4c | ||
|
|
58cc078d25 | ||
|
|
c57acddfc0 | ||
|
|
780bd42ceb | ||
|
|
b2f37733e8 | ||
|
|
6c0c76328f | ||
|
|
7c16ae2220 | ||
|
|
26438d0b90 | ||
|
|
ddaa8eb858 | ||
|
|
72ca9b38bb | ||
|
|
26b9dcdaa9 | ||
|
|
9dc040f6d2 | ||
|
|
018addc6ad | ||
|
|
c3d01be0df | ||
|
|
d6828c5ef4 | ||
|
|
17a63c0bf9 | ||
|
|
a8dd1ec38b | ||
|
|
d12310026b | ||
|
|
864e06222c | ||
|
|
d5fa7455e5 | ||
|
|
49fcc1ea43 | ||
|
|
ff7ee2aefc | ||
|
|
5eead8b7ee | ||
|
|
9b4d650427 | ||
|
|
5c806174bb | ||
|
|
3bb91f6833 | ||
|
|
b014225b73 | ||
|
|
92d32aaa8d | ||
|
|
7717462300 | ||
|
|
06682225c8 | ||
|
|
3145411921 | ||
|
|
bd21d9a1a1 | ||
|
|
f4cb16e56e | ||
|
|
867523140c | ||
|
|
a12e7a0e90 | ||
|
|
9c809f99c5 | ||
|
|
16a21a0562 | ||
|
|
e0dad39600 | ||
|
|
74821a4d4c | ||
|
|
41b3245bf4 | ||
|
|
40b157206a | ||
|
|
71cdd146fa | ||
|
|
d07b77b8f7 | ||
|
|
b2988e540e | ||
|
|
68b1670a55 | ||
|
|
2af3a94ce4 | ||
|
|
fd3f5e5116 | ||
|
|
e8cd938b65 | ||
|
|
9310a4734d | ||
|
|
8d1d96884e | ||
|
|
876eb1e413 | ||
|
|
720089d3c2 | ||
|
|
bbd02dda81 | ||
|
|
f5563c80b1 | ||
|
|
4cd7ad9c33 | ||
|
|
227e52c16e | ||
|
|
5dbf379bde | ||
|
|
cdb392cd0e | ||
|
|
ad342c4bb3 | ||
|
|
4212f0b014 | ||
|
|
b42d325cb0 | ||
|
|
7a0c9c85bc | ||
|
|
31f0cb12b3 | ||
|
|
457f305062 | ||
|
|
b31b01bc50 | ||
|
|
743327a53b | ||
|
|
691bfa4e30 | ||
|
|
58108f257f | ||
|
|
ec8f42496d | ||
|
|
35e8c005f3 | ||
|
|
a5f94360b3 | ||
|
|
1574dabd69 | ||
|
|
be86b70b24 | ||
|
|
05a6399638 | ||
|
|
dbb9686074 | ||
|
|
4e10a23ad0 | ||
|
|
e48f07f4dd | ||
|
|
1143f74ba8 | ||
|
|
bc5a045886 | ||
|
|
8df9a1e66e | ||
|
|
4130ad5217 | ||
|
|
1e8c3de221 | ||
|
|
fdf5ca1d7a | ||
|
|
612230ad0c | ||
|
|
e67d68267c | ||
|
|
9419b67fff | ||
|
|
edbd21f25f | ||
|
|
f42e9dab4c | ||
|
|
90c235d9d1 | ||
|
|
3dbe59fd27 | ||
|
|
885f186e39 | ||
|
|
12d48d6248 | ||
|
|
878cbb9195 | ||
|
|
b8d435fb07 | ||
|
|
a7e09352ef | ||
|
|
0a0bbbb3ce | ||
|
|
ad5c51c486 | ||
|
|
ab02b1b6ca | ||
|
|
2d6a6d652e | ||
|
|
287d0bf538 | ||
|
|
8643b1fbf2 | ||
|
|
ba7de6bf81 | ||
|
|
6495c0a16b | ||
|
|
2db9489646 | ||
|
|
8f6691f7a4 | ||
|
|
dd9a9d5882 | ||
|
|
cbdd1921ec | ||
|
|
aefac2056e | ||
|
|
2fa81111ba | ||
|
|
2048a2cc7f | ||
|
|
5a0a30fc12 | ||
|
|
358d56c2cf | ||
|
|
6376fe21cb | ||
|
|
a79d71da7d | ||
|
|
754b5c5fbb | ||
|
|
2bc6687957 | ||
|
|
3bd2d0b3fc | ||
|
|
93f102e9c1 | ||
|
|
d483b13aa8 | ||
|
|
89114ecd13 | ||
|
|
47a860c09d | ||
|
|
01ad397561 | ||
|
|
15817712c0 | ||
|
|
7f506f3fea | ||
|
|
9df9b9fa46 | ||
|
|
f910d7023c | ||
|
|
37919a11ab | ||
|
|
f83d6246cb | ||
|
|
ea96b7b7ce | ||
|
|
a444d98733 | ||
|
|
eaf0df9573 | ||
|
|
5e3fc2b22a | ||
|
|
1d2be0cef0 | ||
|
|
493c4689c2 | ||
|
|
51e4bcfef9 | ||
|
|
76ede5c541 | ||
|
|
f4f0400c4d | ||
|
|
9bb3a9f7cb | ||
|
|
89711b2010 | ||
|
|
61ec6c886c | ||
|
|
6dd0dd41e0 | ||
|
|
c936a6ff2c | ||
|
|
18e8f9c3dc | ||
|
|
c103a0c481 | ||
|
|
21f942a219 | ||
|
|
89546c4066 | ||
|
|
b5dd63aec1 | ||
|
|
bc71366fd5 | ||
|
|
c27c60346e | ||
|
|
9471026162 | ||
|
|
8e21857a52 | ||
|
|
10d27357f4 | ||
|
|
7d644115d1 | ||
|
|
7fa10b1a9a | ||
|
|
ea0be250c1 | ||
|
|
685bd02758 | ||
|
|
aa80f47b1c | ||
|
|
84ebd1b66f | ||
|
|
2a3b8e511c | ||
|
|
80f219513b | ||
|
|
7d4102e3aa | ||
|
|
3b657e120f | ||
|
|
a477af3b4f | ||
|
|
d4fdb20436 | ||
|
|
5a88fda511 | ||
|
|
e0ebbf5119 | ||
|
|
3129a6615f | ||
|
|
2e013c33ac | ||
|
|
5c5cde535e | ||
|
|
16838f0fa1 | ||
|
|
68461308c6 | ||
|
|
99cb9da0b1 | ||
|
|
84b373ed95 | ||
|
|
67405937ed | ||
|
|
b60e62f976 | ||
|
|
b4e2712dfb | ||
|
|
43fa0d2b58 | ||
|
|
8e34fd1925 | ||
|
|
d55d914fb4 | ||
|
|
31a8de852c | ||
|
|
825851f603 | ||
|
|
d9a2b10afd | ||
|
|
06c2f328d1 | ||
|
|
5aea8d71da | ||
|
|
71d413004e | ||
|
|
59658fcb29 | ||
|
|
6d2c4d215d | ||
|
|
fe23b83044 | ||
|
|
c4cbefeb81 | ||
|
|
d822d8343a | ||
|
|
3e8abf554e | ||
|
|
431e9d8b09 | ||
|
|
2adec1bc34 | ||
|
|
0e2a37ac91 | ||
|
|
84360bfddf | ||
|
|
ea2552e173 | ||
|
|
ca2070eada | ||
|
|
3ae1051148 | ||
|
|
6b441291c0 | ||
|
|
786c91c4ff | ||
|
|
b6299c2d40 | ||
|
|
a257fdb623 | ||
|
|
a6344d8e71 | ||
|
|
ec54db58dd | ||
|
|
dcf10643a0 | ||
|
|
bc7c37241e | ||
|
|
360dbdf02d | ||
|
|
6c8b281a82 | ||
|
|
fb1d431948 | ||
|
|
87a97d4053 | ||
|
|
d17c704f38 | ||
|
|
ba9f034bfb | ||
|
|
ce291448f1 | ||
|
|
8dbfa8f274 | ||
|
|
0ccb8b6217 | ||
|
|
0fcb0be360 | ||
|
|
f23f962adb | ||
|
|
276cb03953 | ||
|
|
12bb886032 | ||
|
|
1f719e3aa5 | ||
|
|
e5a2152cfd | ||
|
|
392d7177f6 | ||
|
|
6553096bc7 | ||
|
|
5178529083 | ||
|
|
10d11c1615 | ||
|
|
68473e5a29 | ||
|
|
d481dc8651 | ||
|
|
322740d1a9 | ||
|
|
86385e0d86 | ||
|
|
412fd34cc1 | ||
|
|
e4cc49cda5 | ||
|
|
9e07618a26 | ||
|
|
46ef214bd3 | ||
|
|
5c2d8c4b62 | ||
|
|
1ceb1b4014 | ||
|
|
afa592aa78 | ||
|
|
8a7d4c4fcc | ||
|
|
adb7e97a91 | ||
|
|
43e3cb8f10 | ||
|
|
333ef98378 | ||
|
|
5941bfac7c | ||
|
|
6d4623f8bf | ||
|
|
91aa95d3f2 | ||
|
|
841738c34c | ||
|
|
f27c41b83c | ||
|
|
071942faca | ||
|
|
9bbcb6a073 | ||
|
|
48bfc2699b | ||
|
|
ad214a8e76 | ||
|
|
d0652d0583 | ||
|
|
0c6ee2fe99 | ||
|
|
c29b12688c | ||
|
|
ccfa7aff5f | ||
|
|
0b6b85b2a5 | ||
|
|
a6a0b3e931 | ||
|
|
06f8926964 | ||
|
|
ef10ab8088 | ||
|
|
1a39727e38 | ||
|
|
bb7f1a57f0 | ||
|
|
c904d0f0c0 | ||
|
|
64d8d53987 | ||
|
|
133984ab36 | ||
|
|
bcbaf25365 | ||
|
|
6c815c1132 | ||
|
|
b32d1ef717 | ||
|
|
94d12dfbd4 | ||
|
|
6a22cd54ec | ||
|
|
480428b4a2 | ||
|
|
5650b7cf3a | ||
|
|
a808cdc195 | ||
|
|
ee5316e6ef |
+18
-1
@@ -1,12 +1,27 @@
|
||||
|
||||
# job_environment will setup the environment for any job being executed.
|
||||
job_environment: &job_environment
|
||||
NODE_ENV: test
|
||||
DISABLE_CREATE_MONGO_INDEXES: TRUE
|
||||
|
||||
# job_defaults applies all the defaults for each job.
|
||||
job_defaults: &job_defaults
|
||||
working_directory: ~/coralproject/talk
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
environment:
|
||||
<<: *job_environment
|
||||
|
||||
# create_indexes will create the mongo indexes and wait until they have been
|
||||
# built.
|
||||
create_indexes: &create_indexes
|
||||
run:
|
||||
name: Create the database indexes and wait until they are built
|
||||
command: ./bin/cli db createIndexes
|
||||
|
||||
# integration_environment is the environment that configures the tests.
|
||||
integration_environment: &integration_environment
|
||||
NODE_ENV: test
|
||||
<<: *job_environment
|
||||
CIRCLE_TEST_REPORTS: /tmp/circleci-test-results
|
||||
E2E_MAX_RETRIES: 3
|
||||
|
||||
@@ -25,6 +40,7 @@ integration_job: &integration_job
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/coralproject/talk
|
||||
- <<: *create_indexes
|
||||
- run:
|
||||
name: Setup the database with defaults
|
||||
command: ./bin/cli setup --defaults
|
||||
@@ -117,6 +133,7 @@ jobs:
|
||||
environment:
|
||||
JEST_JUNIT_OUTPUT: /tmp/circleci-test-results/jest/test-results.xml
|
||||
JEST_REPORTER: jest-junit
|
||||
- <<: *create_indexes
|
||||
- run:
|
||||
name: Run the server unit tests
|
||||
command: yarn test:server
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
{
|
||||
"env": {
|
||||
"jest": true
|
||||
},
|
||||
"extends": "@coralproject/eslint-config-talk"
|
||||
}
|
||||
|
||||
+3
-2
@@ -38,6 +38,7 @@ plugins/*
|
||||
!plugins/talk-plugin-google-auth
|
||||
!plugins/talk-plugin-ignore-user
|
||||
!plugins/talk-plugin-like
|
||||
!plugins/talk-plugin-local-auth
|
||||
!plugins/talk-plugin-love
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-mod
|
||||
@@ -50,9 +51,10 @@ plugins/*
|
||||
!plugins/talk-plugin-notifications-digest-hourly
|
||||
!plugins/talk-plugin-offtopic
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-profile-settings
|
||||
!plugins/talk-plugin-profile-data
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-respect
|
||||
!plugins/talk-plugin-rich-text
|
||||
!plugins/talk-plugin-slack-notifications
|
||||
!plugins/talk-plugin-sort-most-downvoted
|
||||
!plugins/talk-plugin-sort-most-liked
|
||||
@@ -66,7 +68,6 @@ plugins/*
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-upvote
|
||||
!plugins/talk-plugin-viewing-options
|
||||
!plugins/talk-plugin-rich-text
|
||||
|
||||
**/node_modules/*
|
||||
yarn-error.log
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
"exceptions": [
|
||||
"https://nodesecurity.io/advisories/531",
|
||||
"https://nodesecurity.io/advisories/532",
|
||||
"https://nodesecurity.io/advisories/566"
|
||||
"https://nodesecurity.io/advisories/566",
|
||||
"https://nodesecurity.io/advisories/577",
|
||||
"https://nodesecurity.io/advisories/594",
|
||||
"https://nodesecurity.io/advisories/603",
|
||||
"https://nodesecurity.io/advisories/611",
|
||||
"https://nodesecurity.io/advisories/612"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ You’ve installed Talk on your server, and you’re preparing to launch it on y
|
||||
|
||||
## End-to-End Testing
|
||||
|
||||
Talk uses [Nightwatch](https://nightwatchjs.org/) as our e2e testing framework. The testing infrastructure that allows us to run our tests in real browsers is provided with love by our friends at [Browserstack](https://browserstack.com).
|
||||
Talk uses [Nightwatch](http://nightwatchjs.org/) as our e2e testing framework. The testing infrastructure that allows us to run our tests in real browsers is provided with love by our friends at [Browserstack](https://browserstack.com).
|
||||
|
||||
[](https://browserstack.com)
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
require('./util');
|
||||
const program = require('commander');
|
||||
const { head, map } = require('lodash');
|
||||
const Matcher = require('did-you-mean');
|
||||
|
||||
// We're requiring this here so it'll setup some promise rejection hooks to log
|
||||
// out.
|
||||
require('./util');
|
||||
|
||||
// Setup the program.
|
||||
program
|
||||
.command('serve', 'serve the application')
|
||||
.command('db', 'run database commands')
|
||||
.command('settings', 'interact with the application settings')
|
||||
.command('assets', 'interact with assets')
|
||||
.command('setup', 'setup the application')
|
||||
@@ -23,20 +22,3 @@ program
|
||||
'provides utilities for interacting with the plugin system'
|
||||
)
|
||||
.parse(process.argv);
|
||||
|
||||
// If the command wasn't found, output help.
|
||||
const commands = map(program.commands, '_name');
|
||||
const command = head(program.args);
|
||||
if (!commands.includes(command)) {
|
||||
const m = new Matcher(commands);
|
||||
const similarCommands = m.list(command);
|
||||
|
||||
console.error(
|
||||
`cli '${command}' is not a talk cli command. See 'cli --help'.`
|
||||
);
|
||||
if (similarCommands.length > 0) {
|
||||
const sc = similarCommands.map(({ value }) => `\t${value}\n`).join('');
|
||||
console.error(`\nThe most similar commands are\n${sc}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
Executable
+53
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const config = require('../config');
|
||||
|
||||
async function createIndexes() {
|
||||
try {
|
||||
// Ensure we enable the index creation.
|
||||
config.CREATE_MONGO_INDEXES = true;
|
||||
|
||||
// TODO: handle the plugin index creation?
|
||||
|
||||
// Let's register the shutdown hooks.
|
||||
util.onshutdown([() => require('../services/mongoose').disconnect()]);
|
||||
|
||||
// Lets create all the database indexes for the application and wait for all
|
||||
// them to finish their indexing.
|
||||
const models = [
|
||||
require('../models/action'),
|
||||
require('../models/asset'),
|
||||
require('../models/comment'),
|
||||
require('../models/setting'),
|
||||
require('../models/user'),
|
||||
require('../models/migration'),
|
||||
];
|
||||
|
||||
// Call the `.init()` method to setup all the indexes on each model.
|
||||
// `init()` returns a promise that resolves when the indexes have finished
|
||||
// building successfully. The `init()` function is idempotent, so we don't
|
||||
// have to worry about triggering an index rebuild.
|
||||
await Promise.all(models.map(Model => Model.init()));
|
||||
|
||||
console.log('Indexes created');
|
||||
util.shutdown(0);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
program
|
||||
.command('createIndexes')
|
||||
.description('creates the database indexes and waits until they are created')
|
||||
.action(createIndexes);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
if (process.argv.length <= 2) {
|
||||
program.outputHelp();
|
||||
util.shutdown();
|
||||
}
|
||||
+11
-8
@@ -4,7 +4,8 @@ const util = require('./util');
|
||||
const program = require('commander');
|
||||
const inquirer = require('inquirer');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const SettingsService = require('../services/settings');
|
||||
const Settings = require('../services/settings');
|
||||
const cache = require('../services/cache');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
@@ -14,9 +15,12 @@ util.onshutdown([() => mongoose.disconnect()]);
|
||||
*/
|
||||
async function changeOrgName() {
|
||||
try {
|
||||
let settings = await SettingsService.retrieve();
|
||||
await cache.init();
|
||||
|
||||
let { organizationName } = await inquirer.prompt([
|
||||
// Get the original settings.
|
||||
const settings = await Settings.select('organizationName');
|
||||
|
||||
const { organizationName } = await inquirer.prompt([
|
||||
{
|
||||
name: 'organizationName',
|
||||
message: 'Organization Name',
|
||||
@@ -25,9 +29,8 @@ async function changeOrgName() {
|
||||
]);
|
||||
|
||||
if (settings.organizationName !== organizationName) {
|
||||
settings.organizationName = organizationName;
|
||||
|
||||
await SettingsService.update(settings);
|
||||
// Set the organization name if there was a mutation to it.
|
||||
await Settings.update({ organizationName });
|
||||
|
||||
console.log('Settings were updated.');
|
||||
} else {
|
||||
@@ -36,9 +39,9 @@ async function changeOrgName() {
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
} finally {
|
||||
util.shutdown();
|
||||
}
|
||||
|
||||
util.shutdown();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
||||
+8
-1
@@ -287,8 +287,15 @@ async function createUser() {
|
||||
|
||||
const { email, username, password, role } = answers;
|
||||
|
||||
const ctx = Context.forSystem();
|
||||
|
||||
// Create the user.
|
||||
const user = await UsersService.createLocalUser(email, password, username);
|
||||
const user = await UsersService.createLocalUser(
|
||||
ctx,
|
||||
email,
|
||||
password,
|
||||
username
|
||||
);
|
||||
|
||||
// Set the role.
|
||||
await UsersService.setRole(user.id, role);
|
||||
|
||||
+5
-2
@@ -4,7 +4,8 @@ require('../services/env');
|
||||
const debug = require('debug')('talk:util');
|
||||
const { uniq } = require('lodash');
|
||||
|
||||
const util = (module.exports = {});
|
||||
// Setup the utilities.
|
||||
const util = {};
|
||||
|
||||
/**
|
||||
* Stores an array of functions that should be executed in the event that the
|
||||
@@ -15,7 +16,7 @@ util.toshutdown = [];
|
||||
|
||||
/**
|
||||
* Calls all the shutdown functions and then ends the process.
|
||||
* @param {Number} [defaultCode=0] default return code upon sucesfull shutdown.
|
||||
* @param {Number} [defaultCode=0] default return code upon successful shutdown.
|
||||
*/
|
||||
util.shutdown = (defaultCode = 0, signal = null) => {
|
||||
if (signal) {
|
||||
@@ -63,3 +64,5 @@ process.on('unhandledRejection', err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
module.exports = util;
|
||||
|
||||
@@ -10,6 +10,7 @@ import Configure from 'routes/Configure';
|
||||
import StreamSettings from './routes/Configure/containers/StreamSettings';
|
||||
import ModerationSettings from './routes/Configure/containers/ModerationSettings';
|
||||
import TechSettings from './routes/Configure/containers/TechSettings';
|
||||
import OrganizationSettings from './routes/Configure/containers/OrganizationSettings';
|
||||
|
||||
import { ModerationLayout, Moderation } from 'routes/Moderation';
|
||||
|
||||
@@ -25,6 +26,7 @@ const routes = (
|
||||
<Route path="stream" component={StreamSettings} />
|
||||
<Route path="moderation" component={ModerationSettings} />
|
||||
<Route path="tech" component={TechSettings} />
|
||||
<Route path="organization" component={OrganizationSettings} />
|
||||
<IndexRedirect to="stream" />
|
||||
</Route>
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ const CommentAnimatedEdit = ({ children, body }) => {
|
||||
|
||||
CommentAnimatedEdit.propTypes = {
|
||||
children: PropTypes.node,
|
||||
body: PropTypes.string,
|
||||
body: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CommentAnimatedEdit;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.tombstone {
|
||||
background-color: #f0f0f0;
|
||||
padding: 1em;
|
||||
color: #1a212f;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import styles from './CommentDeletedTombstone.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const CommentDeletedTombstone = () => (
|
||||
<div className={styles.tombstone}>{t('framework.comment_is_deleted')}</div>
|
||||
);
|
||||
|
||||
export default CommentDeletedTombstone;
|
||||
@@ -3,7 +3,7 @@ import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import styles from './UserDetail.css';
|
||||
import AccountHistory from './AccountHistory';
|
||||
import UserHistory from './UserHistory';
|
||||
import { Slot } from 'coral-framework/components';
|
||||
import UserDetailCommentList from '../components/UserDetailCommentList';
|
||||
import {
|
||||
@@ -28,26 +28,6 @@ import UserInfoTooltip from './UserInfoTooltip';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class UserDetail extends React.Component {
|
||||
rejectThenReload = async info => {
|
||||
await this.props.rejectComment(info);
|
||||
this.props.data.refetch();
|
||||
};
|
||||
|
||||
acceptThenReload = async info => {
|
||||
await this.props.acceptComment(info);
|
||||
this.props.data.refetch();
|
||||
};
|
||||
|
||||
bulkAcceptThenReload = async () => {
|
||||
await this.props.bulkAccept();
|
||||
this.props.data.refetch();
|
||||
};
|
||||
|
||||
bulkRejectThenReload = async () => {
|
||||
await this.props.bulkReject();
|
||||
this.props.data.refetch();
|
||||
};
|
||||
|
||||
changeTab = tab => {
|
||||
this.props.changeTab(tab);
|
||||
};
|
||||
@@ -110,6 +90,10 @@ class UserDetail extends React.Component {
|
||||
unbanUser,
|
||||
unsuspendUser,
|
||||
modal,
|
||||
acceptComment,
|
||||
rejectComment,
|
||||
bulkAccept,
|
||||
bulkReject,
|
||||
} = this.props;
|
||||
|
||||
// if totalComments is 0, you're dividing by zero
|
||||
@@ -286,7 +270,7 @@ class UserDetail extends React.Component {
|
||||
'talk-admin-user-detail-history-tab'
|
||||
)}
|
||||
>
|
||||
{t('user_detail.account_history')}
|
||||
{t('user_detail.user_history')}
|
||||
</Tab>
|
||||
</TabBar>
|
||||
|
||||
@@ -304,12 +288,12 @@ class UserDetail extends React.Component {
|
||||
loadMore={loadMore}
|
||||
toggleSelect={toggleSelect}
|
||||
viewUserDetail={viewUserDetail}
|
||||
acceptComment={this.acceptThenReload}
|
||||
rejectComment={this.rejectThenReload}
|
||||
acceptComment={acceptComment}
|
||||
rejectComment={rejectComment}
|
||||
selectedCommentIds={selectedCommentIds}
|
||||
toggleSelectAll={toggleSelectAll}
|
||||
bulkAcceptThenReload={this.bulkAcceptThenReload}
|
||||
bulkRejectThenReload={this.bulkRejectThenReload}
|
||||
bulkAcceptThenReload={bulkAccept}
|
||||
bulkRejectThenReload={bulkReject}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane
|
||||
@@ -322,19 +306,19 @@ class UserDetail extends React.Component {
|
||||
loadMore={loadMore}
|
||||
toggleSelect={toggleSelect}
|
||||
viewUserDetail={viewUserDetail}
|
||||
acceptComment={this.acceptThenReload}
|
||||
rejectComment={this.rejectThenReload}
|
||||
acceptComment={acceptComment}
|
||||
rejectComment={rejectComment}
|
||||
selectedCommentIds={selectedCommentIds}
|
||||
toggleSelectAll={toggleSelectAll}
|
||||
bulkAcceptThenReload={this.bulkAcceptThenReload}
|
||||
bulkRejectThenReload={this.bulkRejectThenReload}
|
||||
bulkAcceptThenReload={bulkAccept}
|
||||
bulkRejectThenReload={bulkReject}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane
|
||||
tabId={'history'}
|
||||
className={'talk-admin-user-detail-history-tab-pane'}
|
||||
>
|
||||
<AccountHistory user={user} />
|
||||
<UserHistory user={user} />
|
||||
</TabPane>
|
||||
</TabContent>
|
||||
</Drawer>
|
||||
|
||||
@@ -12,6 +12,7 @@ import CommentAnimatedEdit from './CommentAnimatedEdit';
|
||||
import CommentLabels from '../containers/CommentLabels';
|
||||
import ApproveButton from './ApproveButton';
|
||||
import RejectButton from 'coral-admin/src/components/RejectButton';
|
||||
import CommentDeletedTombstone from './CommentDeletedTombstone';
|
||||
|
||||
import t, { timeago } from 'coral-framework/services/i18n';
|
||||
|
||||
@@ -43,6 +44,19 @@ class UserDetailComment extends React.Component {
|
||||
body: comment.body,
|
||||
};
|
||||
|
||||
if (!comment.body) {
|
||||
return (
|
||||
<li
|
||||
tabIndex={0}
|
||||
className={cn(className, styles.root, {
|
||||
[styles.rootSelected]: selected,
|
||||
})}
|
||||
>
|
||||
<CommentDeletedTombstone />
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
tabIndex={0}
|
||||
@@ -152,7 +166,7 @@ UserDetailComment.propTypes = {
|
||||
comment: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
body: PropTypes.string.isRequired,
|
||||
body: PropTypes.string,
|
||||
actions: PropTypes.array,
|
||||
created_at: PropTypes.string.isRequired,
|
||||
asset: PropTypes.shape({
|
||||
|
||||
+19
-21
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { murmur3 } from 'murmurhash-js';
|
||||
import styles from './AccountHistory.css';
|
||||
import styles from './UserHistory.css';
|
||||
import cn from 'classnames';
|
||||
import flatten from 'lodash/flatten';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
@@ -43,15 +43,15 @@ const readableDuration = (startDate, endDate) => {
|
||||
const buildActionResponse = (typename, created_at, until, status) => {
|
||||
switch (typename) {
|
||||
case 'UsernameStatusHistory':
|
||||
return t('account_history.username_status', status);
|
||||
return t('user_history.username_status', status);
|
||||
case 'BannedStatusHistory':
|
||||
return status
|
||||
? t('account_history.user_banned')
|
||||
: t('account_history.ban_removed');
|
||||
? t('user_history.user_banned')
|
||||
: t('user_history.ban_removed');
|
||||
case 'SuspensionStatusHistory':
|
||||
return until
|
||||
? t('account_history.suspended', readableDuration(created_at, until))
|
||||
: t('account_history.suspension_removed');
|
||||
? t('user_history.suspended', readableDuration(created_at, until))
|
||||
: t('user_history.suspension_removed');
|
||||
default:
|
||||
return '-';
|
||||
}
|
||||
@@ -62,43 +62,41 @@ const getModerationValue = assignedBy =>
|
||||
assignedBy.username
|
||||
) : (
|
||||
<span>
|
||||
<Icon name="computer" /> {t('account_history.system')}
|
||||
<Icon name="computer" /> {t('user_history.system')}
|
||||
</span>
|
||||
);
|
||||
|
||||
class AccountHistory extends React.Component {
|
||||
class UserHistory extends React.Component {
|
||||
render() {
|
||||
const { user } = this.props;
|
||||
const userHistory = buildUserHistory(user.state);
|
||||
return (
|
||||
<div>
|
||||
<div className={cn(styles.table, 'talk-admin-account-history')}>
|
||||
<div className={cn(styles.table, 'talk-admin-user-history')}>
|
||||
<div
|
||||
className={cn(
|
||||
styles.headerRow,
|
||||
'talk-admin-account-history-header-row'
|
||||
'talk-admin-user-history-header-row'
|
||||
)}
|
||||
>
|
||||
<div className={styles.headerRowItem}>{t('user_history.date')}</div>
|
||||
<div className={styles.headerRowItem}>
|
||||
{t('account_history.date')}
|
||||
{t('user_history.action')}
|
||||
</div>
|
||||
<div className={styles.headerRowItem}>
|
||||
{t('account_history.action')}
|
||||
</div>
|
||||
<div className={styles.headerRowItem}>
|
||||
{t('account_history.taken_by')}
|
||||
{t('user_history.taken_by')}
|
||||
</div>
|
||||
</div>
|
||||
{userHistory.map(
|
||||
({ __typename, created_at, assigned_by, until, status }) => (
|
||||
<div
|
||||
className={cn(styles.row, 'talk-admin-account-history-row')}
|
||||
className={cn(styles.row, 'talk-admin-user-history-row')}
|
||||
key={`${__typename}_${murmur3(created_at)}`}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
styles.item,
|
||||
'talk-admin-account-history-row-date'
|
||||
'talk-admin-user-history-row-date'
|
||||
)}
|
||||
>
|
||||
{moment(new Date(created_at)).format('MMM DD, YYYY')}
|
||||
@@ -107,7 +105,7 @@ class AccountHistory extends React.Component {
|
||||
className={cn(
|
||||
styles.item,
|
||||
styles.action,
|
||||
'talk-admin-account-history-row-status'
|
||||
'talk-admin-user-history-row-status'
|
||||
)}
|
||||
>
|
||||
{buildActionResponse(__typename, created_at, until, status)}
|
||||
@@ -116,7 +114,7 @@ class AccountHistory extends React.Component {
|
||||
className={cn(
|
||||
styles.item,
|
||||
styles.username,
|
||||
'talk-admin-account-history-row-assigned-by'
|
||||
'talk-admin-user-history-row-assigned-by'
|
||||
)}
|
||||
>
|
||||
{getModerationValue(assigned_by)}
|
||||
@@ -130,8 +128,8 @@ class AccountHistory extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
AccountHistory.propTypes = {
|
||||
UserHistory.propTypes = {
|
||||
user: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default AccountHistory;
|
||||
export default UserHistory;
|
||||
@@ -148,6 +148,7 @@ UserDetailContainer.propTypes = {
|
||||
selectedCommentIds: PropTypes.array,
|
||||
unbanUser: PropTypes.func.isRequired,
|
||||
unsuspendUser: PropTypes.func.isRequired,
|
||||
userId: PropTypes.string,
|
||||
};
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
@@ -245,7 +246,6 @@ export const withUserDetailQuery = withQuery(
|
||||
options: ({ userId, statuses }) => {
|
||||
return {
|
||||
variables: { author_id: userId, statuses },
|
||||
fetchPolicy: 'network-only',
|
||||
};
|
||||
},
|
||||
skip: ownProps => !ownProps.userId,
|
||||
|
||||
@@ -291,5 +291,36 @@ export default {
|
||||
},
|
||||
},
|
||||
}),
|
||||
SetCommentStatus: ({ variables: { status } }) => ({
|
||||
updateQueries: {
|
||||
CoralAdmin_UserDetail: prev => {
|
||||
const increment = {
|
||||
rejectedComments: {
|
||||
$apply: count => (count < prev.totalComments ? count + 1 : count),
|
||||
},
|
||||
};
|
||||
|
||||
const decrement = {
|
||||
rejectedComments: {
|
||||
$apply: count => (count > 0 ? count - 1 : 0),
|
||||
},
|
||||
};
|
||||
|
||||
// If rejected then increment rejectedComments by one
|
||||
if (status === 'REJECTED') {
|
||||
const updated = update(prev, increment);
|
||||
return updated;
|
||||
}
|
||||
|
||||
// If approved then decrement rejectedComments by one
|
||||
if (status === 'ACCEPTED') {
|
||||
const updated = update(prev, decrement);
|
||||
return updated;
|
||||
}
|
||||
|
||||
return prev;
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ const initialState = {
|
||||
isLoading: false,
|
||||
data: {
|
||||
settings: {
|
||||
organizationContactEmail: '',
|
||||
organizationName: '',
|
||||
domains: {
|
||||
whitelist: [],
|
||||
@@ -19,6 +20,7 @@ const initialState = {
|
||||
},
|
||||
errors: {
|
||||
organizationName: '',
|
||||
organizationContactEmail: '',
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
|
||||
@@ -8,7 +8,14 @@ import SaveChangesDialog from './SaveChangesDialog';
|
||||
|
||||
class Configure extends React.Component {
|
||||
render() {
|
||||
const { canSave, currentUser, root, savePending, settings } = this.props;
|
||||
const {
|
||||
canSave,
|
||||
currentUser,
|
||||
root,
|
||||
savePending,
|
||||
settings,
|
||||
clearPending,
|
||||
} = this.props;
|
||||
|
||||
if (!can(currentUser, 'UPDATE_CONFIG')) {
|
||||
return <p>{t('configure.access_message')}</p>;
|
||||
@@ -17,6 +24,9 @@ class Configure extends React.Component {
|
||||
const passProps = {
|
||||
root,
|
||||
settings,
|
||||
savePending,
|
||||
clearPending,
|
||||
canSave,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -41,6 +51,9 @@ class Configure extends React.Component {
|
||||
<Item itemId="tech" icon="code">
|
||||
{t('configure.tech_settings')}
|
||||
</Item>
|
||||
<Item itemId="organization" icon="people">
|
||||
{t('configure.organization_information')}
|
||||
</Item>
|
||||
</List>
|
||||
<div className={styles.saveBox}>
|
||||
{canSave ? (
|
||||
@@ -81,6 +94,7 @@ Configure.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
saveDialog: PropTypes.bool,
|
||||
hideSaveDialog: PropTypes.func.isRequired,
|
||||
clearPending: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Configure;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
.label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.detailList {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.detailLabel {
|
||||
color: #000;
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.detailValue {
|
||||
padding: 6px 0;
|
||||
border: solid 1px transparent;
|
||||
display: block;
|
||||
font-size: 1.1em;
|
||||
border-radius: 2px;
|
||||
color: #424242;
|
||||
box-sizing: border-box;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.editable {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
border-color: grey;
|
||||
}
|
||||
|
||||
.detailItem {
|
||||
margin-bottom: 16px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.button, .button:disabled {
|
||||
background: white;
|
||||
border: solid 1px grey;
|
||||
}
|
||||
|
||||
.actionBox {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.cancelButton {
|
||||
padding: 10px;
|
||||
display: block;
|
||||
color: #4f5c67;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.changedSave {
|
||||
background-color: #00796B;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.errorList {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.errorItem {
|
||||
padding: 5px 10px;
|
||||
margin-bottom: 20px;
|
||||
color: #b71c1c;
|
||||
border-radius: 2px;
|
||||
display: inline-block;
|
||||
background: #F9D3CE;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex-grow: 1;
|
||||
padding-right: 40px;
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Button } from 'coral-ui';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './OrganizationSettings.css';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import ConfigurePage from './ConfigurePage';
|
||||
import ConfigureCard from 'coral-framework/components/ConfigureCard';
|
||||
import validate from 'coral-framework/helpers/validate';
|
||||
import errorMsj from 'coral-framework/helpers/error';
|
||||
|
||||
class OrganizationSettings extends React.Component {
|
||||
state = { editing: false, errors: [] };
|
||||
|
||||
addError = err => {
|
||||
if (this.state.errors.indexOf(err) === -1) {
|
||||
this.setState(({ errors }) => ({
|
||||
errors: errors.concat(err),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
removeError = err => {
|
||||
this.setState(({ errors }) => ({
|
||||
errors: errors.filter(i => i !== err),
|
||||
}));
|
||||
};
|
||||
|
||||
toggleEditing = () => {
|
||||
this.setState(({ editing }) => ({
|
||||
editing: !editing,
|
||||
}));
|
||||
};
|
||||
|
||||
disableEditing = () => {
|
||||
this.setState(() => ({
|
||||
editing: false,
|
||||
}));
|
||||
};
|
||||
|
||||
updateName = event => {
|
||||
const updater = { organizationName: { $set: event.target.value } };
|
||||
this.props.updatePending({ updater });
|
||||
};
|
||||
|
||||
updateEmail = event => {
|
||||
let error = null;
|
||||
const email = event.target.value;
|
||||
|
||||
// Add a blocker error
|
||||
if (!validate.email(email)) {
|
||||
error = true;
|
||||
this.addError('email');
|
||||
} else {
|
||||
this.removeError('email');
|
||||
}
|
||||
|
||||
const updater = { organizationContactEmail: { $set: email } };
|
||||
const errorUpdater = { organizationContactEmail: { $set: error } };
|
||||
|
||||
this.props.updatePending({ updater, errorUpdater });
|
||||
};
|
||||
|
||||
cancelEditing = () => {
|
||||
this.disableEditing();
|
||||
this.props.clearPending();
|
||||
};
|
||||
|
||||
save = async () => {
|
||||
await this.props.savePending();
|
||||
this.disableEditing();
|
||||
};
|
||||
displayErrors = (errors = []) => (
|
||||
<ul className={styles.errorList}>
|
||||
{errors.map((errKey, i) => (
|
||||
<li key={`${i}_${errKey}`} className={styles.errorItem}>
|
||||
{errorMsj[errKey]}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
||||
render() {
|
||||
const { settings, slotPassthrough, canSave } = this.props;
|
||||
const hasErrors = this.state.errors.length;
|
||||
return (
|
||||
<ConfigurePage title={t('configure.organization_information')}>
|
||||
<p>{t('configure.organization_info_copy')}</p>
|
||||
<p>{t('configure.organization_info_copy_2')}</p>
|
||||
<ConfigureCard>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
{this.displayErrors(this.state.errors)}
|
||||
<ul className={styles.detailList}>
|
||||
<li className={styles.detailItem}>
|
||||
<label
|
||||
className={styles.detailLabel}
|
||||
id={t('configure.organization_name')}
|
||||
>
|
||||
{t('configure.organization_name')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className={cn(styles.detailValue, {
|
||||
[styles.editable]: this.state.editing,
|
||||
})}
|
||||
onChange={this.updateName}
|
||||
value={settings.organizationName}
|
||||
id={t('configure.organization_name')}
|
||||
readOnly={!this.state.editing}
|
||||
/>
|
||||
</li>
|
||||
<li className={styles.detailItem}>
|
||||
<label
|
||||
className={styles.detailLabel}
|
||||
id={t('configure.organization_contact_email')}
|
||||
>
|
||||
{t('configure.organization_contact_email')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className={cn(styles.detailValue, {
|
||||
[styles.editable]: this.state.editing,
|
||||
})}
|
||||
onChange={this.updateEmail}
|
||||
value={settings.organizationContactEmail || ''}
|
||||
id={t('configure.organization_contact_email')}
|
||||
readOnly={!this.state.editing}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{!this.state.editing ? (
|
||||
<div className={styles.actionBox}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
icon="settings"
|
||||
onClick={this.toggleEditing}
|
||||
full
|
||||
>
|
||||
{t('configure.edit_info')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.actionBox}>
|
||||
{canSave && !hasErrors ? (
|
||||
<Button
|
||||
raised
|
||||
onClick={this.save}
|
||||
className={styles.changedSave}
|
||||
icon="check"
|
||||
full
|
||||
>
|
||||
{t('configure.save')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button className={styles.button} disabled icon="check" full>
|
||||
{t('configure.save')}
|
||||
</Button>
|
||||
)}
|
||||
<a className={styles.cancelButton} onClick={this.cancelEditing}>
|
||||
{t('cancel')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ConfigureCard>
|
||||
<Slot fill="adminOrganizationSettings" passthrough={slotPassthrough} />
|
||||
</ConfigurePage>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
OrganizationSettings.propTypes = {
|
||||
savePending: PropTypes.func.isRequired,
|
||||
clearPending: PropTypes.func.isRequired,
|
||||
updatePending: PropTypes.func.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
slotPassthrough: PropTypes.object.isRequired,
|
||||
canSave: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default OrganizationSettings;
|
||||
@@ -16,10 +16,12 @@ import {
|
||||
hideSaveDialog,
|
||||
} from '../../../actions/configure';
|
||||
import Configure from '../components/Configure';
|
||||
import OrganizationSettings from './OrganizationSettings';
|
||||
import { withRouter } from 'react-router';
|
||||
|
||||
class ConfigureContainer extends React.Component {
|
||||
state = { nextRoute: '' };
|
||||
nextRoute = '';
|
||||
unregisterLeaveHook = null;
|
||||
|
||||
savePending = async () => {
|
||||
await this.props.updateSettings(this.props.pending);
|
||||
@@ -39,18 +41,16 @@ class ConfigureContainer extends React.Component {
|
||||
};
|
||||
|
||||
gotoNextRoute = () => {
|
||||
const { nextRoute } = this.state;
|
||||
if (nextRoute) {
|
||||
this.props.router.push(nextRoute);
|
||||
this.setState({ nextRoute: '' });
|
||||
if (this.nextRoute) {
|
||||
this.props.router.push(this.nextRoute);
|
||||
this.nextRoute = '';
|
||||
}
|
||||
};
|
||||
|
||||
handleSectionChange = async section => {
|
||||
const nextRoute = `/admin/configure/${section}`;
|
||||
|
||||
if (this.shouldShowSaveDialog()) {
|
||||
await this.setState({ nextRoute });
|
||||
if (this.hasPendingData()) {
|
||||
this.nextRoute = nextRoute;
|
||||
this.props.showSaveDialog();
|
||||
} else {
|
||||
// Just go to the section
|
||||
@@ -58,20 +58,37 @@ class ConfigureContainer extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
shouldShowSaveDialog = () => {
|
||||
navigationPrompt = e => {
|
||||
if (this.hasPendingData()) {
|
||||
const confirmationMessage = 'Changes that you made may not be saved.';
|
||||
e.returnValue = confirmationMessage; // Gecko, Trident, Chrome 34+
|
||||
return confirmationMessage; // Gecko, WebKit, Chrome <34
|
||||
}
|
||||
};
|
||||
|
||||
hasPendingData = () => {
|
||||
return !!Object.keys(this.props.pending).length;
|
||||
};
|
||||
|
||||
routeLeave = ({ pathname }) => {
|
||||
if (this.shouldShowSaveDialog()) {
|
||||
this.setState({ nextRoute: pathname });
|
||||
if (this.hasPendingData()) {
|
||||
this.nextRoute = pathname;
|
||||
this.props.showSaveDialog();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.router.setRouteLeaveHook(this.props.route, this.routeLeave);
|
||||
this.unregisterLeaveHook = this.props.router.setRouteLeaveHook(
|
||||
this.props.route,
|
||||
this.routeLeave
|
||||
);
|
||||
window.addEventListener('beforeunload', this.navigationPrompt);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unregisterLeaveHook();
|
||||
window.removeEventListener('beforeunload', this.navigationPrompt);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -83,18 +100,21 @@ class ConfigureContainer extends React.Component {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const activeSection = this.props.routes[3].path;
|
||||
|
||||
return (
|
||||
<Configure
|
||||
saveChanges={this.saveChanges}
|
||||
discardChanges={this.discardChanges}
|
||||
saveDialog={this.props.saveDialog}
|
||||
activeSection={this.props.routes[3].path}
|
||||
activeSection={activeSection}
|
||||
hideSaveDialog={this.props.hideSaveDialog}
|
||||
canSave={this.props.canSave}
|
||||
currentUser={this.props.currentUser}
|
||||
root={this.props.root}
|
||||
settings={this.props.mergedSettings}
|
||||
handleSectionChange={this.handleSectionChange}
|
||||
clearPending={this.props.clearPending}
|
||||
savePending={this.savePending}
|
||||
>
|
||||
{this.props.children}
|
||||
@@ -110,10 +130,12 @@ const withConfigureQuery = withQuery(
|
||||
...${getDefinitionName(StreamSettings.fragments.settings)}
|
||||
...${getDefinitionName(TechSettings.fragments.settings)}
|
||||
...${getDefinitionName(ModerationSettings.fragments.settings)}
|
||||
...${getDefinitionName(OrganizationSettings.fragments.settings)}
|
||||
}
|
||||
...${getDefinitionName(StreamSettings.fragments.root)}
|
||||
...${getDefinitionName(TechSettings.fragments.root)}
|
||||
...${getDefinitionName(ModerationSettings.fragments.root)}
|
||||
...${getDefinitionName(OrganizationSettings.fragments.root)}
|
||||
}
|
||||
${StreamSettings.fragments.root}
|
||||
${StreamSettings.fragments.settings}
|
||||
@@ -121,6 +143,8 @@ const withConfigureQuery = withQuery(
|
||||
${TechSettings.fragments.settings}
|
||||
${ModerationSettings.fragments.root}
|
||||
${ModerationSettings.fragments.settings}
|
||||
${OrganizationSettings.fragments.root}
|
||||
${OrganizationSettings.fragments.settings}
|
||||
`,
|
||||
{
|
||||
options: () => ({
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import OrganizationSettings from '../components/OrganizationSettings';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
import { getSlotFragmentSpreads } from 'coral-framework/utils';
|
||||
import { updatePending } from '../../../actions/configure';
|
||||
import { mapProps } from 'recompose';
|
||||
|
||||
const slots = ['adminOrganizationSettings'];
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
errors: state.configure.errors,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
updatePending,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
export default compose(
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment TalkAdmin_OrganizationSettings_root on RootQuery {
|
||||
__typename
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
}
|
||||
`,
|
||||
settings: gql`
|
||||
fragment TalkAdmin_OrganizationSettings_settings on Settings {
|
||||
organizationName
|
||||
organizationContactEmail
|
||||
${getSlotFragmentSpreads(slots, 'settings')}
|
||||
}
|
||||
`,
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
|
||||
slotPassthrough: {
|
||||
root,
|
||||
settings,
|
||||
updatePending,
|
||||
errors,
|
||||
},
|
||||
updatePending,
|
||||
settings,
|
||||
errors,
|
||||
...rest,
|
||||
}))
|
||||
)(OrganizationSettings);
|
||||
@@ -1,15 +1,16 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import styles from './Install.css';
|
||||
import { Wizard, WizardNav } from 'coral-ui';
|
||||
import Layout from 'coral-admin/src/components/Layout';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import InitialStep from './Steps/InitialStep';
|
||||
import AddOrganizationName from './Steps/AddOrganizationName';
|
||||
import OrganizationDetails from './Steps/OrganizationDetails';
|
||||
import CreateYourAccount from './Steps/CreateYourAccount';
|
||||
import PermittedDomainsStep from './Steps/PermittedDomainsStep';
|
||||
import FinalStep from './Steps/FinalStep';
|
||||
|
||||
export default class Install extends Component {
|
||||
class Install extends React.Component {
|
||||
handleDomainsChange = value => {
|
||||
this.props.updatePermittedDomains(value);
|
||||
};
|
||||
@@ -55,7 +56,7 @@ export default class Install extends Component {
|
||||
goToStep={this.props.goToStep}
|
||||
>
|
||||
<InitialStep />
|
||||
<AddOrganizationName
|
||||
<OrganizationDetails
|
||||
install={install}
|
||||
handleSettingsChange={this.handleSettingsChange}
|
||||
handleSettingsSubmit={this.handleSettingsSubmit}
|
||||
@@ -81,3 +82,18 @@ export default class Install extends Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Install.propTypes = {
|
||||
updatePermittedDomains: PropTypes.func.isRequired,
|
||||
updateSettingsFormData: PropTypes.func.isRequired,
|
||||
updateUserFormData: PropTypes.func.isRequired,
|
||||
submitSettings: PropTypes.func.isRequired,
|
||||
submitUser: PropTypes.func.isRequired,
|
||||
install: PropTypes.object.isRequired,
|
||||
nextStep: PropTypes.func.isRequired,
|
||||
previousStep: PropTypes.func.isRequired,
|
||||
goToStep: PropTypes.func.isRequired,
|
||||
finishInstall: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Install;
|
||||
|
||||
+11
@@ -21,6 +21,17 @@ const AddOrganizationName = props => {
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.organizationName}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className={styles.TextField}
|
||||
id="organizationContactEmail"
|
||||
type="email"
|
||||
label={t('install.create.organization_contact_email')}
|
||||
onChange={handleSettingsChange}
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.organizationContactEmail}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="talk-install-step-2-save-button"
|
||||
type="submit"
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { compose } from 'react-apollo';
|
||||
import Install from '../components/Install';
|
||||
|
||||
import {
|
||||
@@ -18,7 +17,7 @@ import {
|
||||
updatePermittedDomains,
|
||||
} from '../../../actions/install';
|
||||
|
||||
class InstallContainer extends Component {
|
||||
class InstallContainer extends React.Component {
|
||||
componentDidMount() {
|
||||
const { checkInstall } = this.props;
|
||||
checkInstall(() => {
|
||||
@@ -27,7 +26,21 @@ class InstallContainer extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Install {...this.props} />;
|
||||
return (
|
||||
<Install
|
||||
install={this.props.install}
|
||||
goToStep={this.props.goToStep}
|
||||
nextStep={this.props.nextStep}
|
||||
submitUser={this.props.submitUser}
|
||||
checkInstall={this.props.checkInstall}
|
||||
previousStep={this.props.previousStep}
|
||||
finishInstall={this.props.finishInstall}
|
||||
submitSettings={this.props.submitSettings}
|
||||
updateUserFormData={this.props.updateUserFormData}
|
||||
updateSettingsFormData={this.props.updateSettingsFormData}
|
||||
updatePermittedDomains={this.props.updatePermittedDomains}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +48,20 @@ InstallContainer.contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
InstallContainer.propTypes = {
|
||||
install: PropTypes.object.isRequired,
|
||||
goToStep: PropTypes.func.isRequired,
|
||||
nextStep: PropTypes.func.isRequired,
|
||||
submitUser: PropTypes.func.isRequired,
|
||||
checkInstall: PropTypes.func.isRequired,
|
||||
previousStep: PropTypes.func.isRequired,
|
||||
finishInstall: PropTypes.func.isRequired,
|
||||
submitSettings: PropTypes.func.isRequired,
|
||||
updateUserFormData: PropTypes.func.isRequired,
|
||||
updateSettingsFormData: PropTypes.func.isRequired,
|
||||
updatePermittedDomains: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
install: state.install,
|
||||
});
|
||||
@@ -56,6 +83,4 @@ const mapDispatchToProps = dispatch =>
|
||||
dispatch
|
||||
);
|
||||
|
||||
export default compose(connect(mapStateToProps, mapDispatchToProps))(
|
||||
InstallContainer
|
||||
);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(InstallContainer);
|
||||
|
||||
@@ -85,6 +85,10 @@
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.deleted {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.moderateArticle {
|
||||
font-size: 14px;
|
||||
margin: 10px 0;
|
||||
|
||||
@@ -13,6 +13,7 @@ import IfHasLink from 'coral-admin/src/components/IfHasLink';
|
||||
import cn from 'classnames';
|
||||
import ApproveButton from 'coral-admin/src/components/ApproveButton';
|
||||
import RejectButton from 'coral-admin/src/components/RejectButton';
|
||||
import CommentDeletedTombstone from '../../../components/CommentDeletedTombstone';
|
||||
|
||||
import t, { timeago } from 'coral-framework/services/i18n';
|
||||
|
||||
@@ -75,6 +76,27 @@ class Comment extends React.Component {
|
||||
asset: comment.asset,
|
||||
};
|
||||
|
||||
if (!comment.body) {
|
||||
return (
|
||||
<li
|
||||
tabIndex={0}
|
||||
className={cn(
|
||||
className,
|
||||
'mdl-card',
|
||||
selectionStateCSS,
|
||||
styles.root,
|
||||
{ [styles.selected]: selected, [styles.dangling]: dangling },
|
||||
'talk-admin-moderate-comment',
|
||||
styles.deleted
|
||||
)}
|
||||
id={`comment_${comment.id}`}
|
||||
ref={this.handleRef}
|
||||
>
|
||||
<CommentDeletedTombstone />
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
tabIndex={0}
|
||||
@@ -200,7 +222,7 @@ Comment.propTypes = {
|
||||
comment: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
body: PropTypes.string.isRequired,
|
||||
body: PropTypes.string,
|
||||
action_summaries: PropTypes.array,
|
||||
actions: PropTypes.array,
|
||||
created_at: PropTypes.string.isRequired,
|
||||
|
||||
@@ -432,6 +432,7 @@ const withModQueueQuery = withQuery(
|
||||
${Object.keys(queueConfig).map(
|
||||
queue => `
|
||||
${queue}: comments(query: {
|
||||
excludeDeleted: true,
|
||||
statuses: ${
|
||||
queueConfig[queue].statuses
|
||||
? `[${queueConfig[queue].statuses.join(', ')}],`
|
||||
@@ -458,6 +459,7 @@ const withModQueueQuery = withQuery(
|
||||
${Object.keys(queueConfig).map(
|
||||
queue => `
|
||||
${queue}Count: commentCount(query: {
|
||||
excludeDeleted: true,
|
||||
statuses: ${
|
||||
queueConfig[queue].statuses
|
||||
? `[${queueConfig[queue].statuses.join(', ')}],`
|
||||
|
||||
@@ -24,14 +24,20 @@ export default class Embed extends React.Component {
|
||||
>
|
||||
{t('embed_comments_tab')}
|
||||
</Tab>,
|
||||
<Tab
|
||||
key="profile"
|
||||
tabId="profile"
|
||||
className="talk-embed-stream-profile-tab"
|
||||
>
|
||||
{t('framework.my_profile')}
|
||||
</Tab>,
|
||||
];
|
||||
|
||||
if (this.props.currentUser) {
|
||||
tabs.push(
|
||||
<Tab
|
||||
key="profile"
|
||||
tabId="profile"
|
||||
className="talk-embed-stream-profile-tab"
|
||||
>
|
||||
{t('framework.my_profile')}
|
||||
</Tab>
|
||||
);
|
||||
}
|
||||
|
||||
if (can(this.props.currentUser, 'UPDATE_ASSET_CONFIG')) {
|
||||
tabs.push(
|
||||
<Tab
|
||||
@@ -43,6 +49,7 @@ export default class Embed extends React.Component {
|
||||
</Tab>
|
||||
);
|
||||
}
|
||||
|
||||
return tabs;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,11 @@ class ExtendableTabPanel extends React.Component {
|
||||
} = this.props;
|
||||
return (
|
||||
<div {...rest}>
|
||||
<TabBar activeTab={activeTab} onTabClick={setActiveTab} sub={sub}>
|
||||
{tabs}
|
||||
</TabBar>
|
||||
{tabs && (
|
||||
<TabBar activeTab={activeTab} onTabClick={setActiveTab} sub={sub}>
|
||||
{tabs}
|
||||
</TabBar>
|
||||
)}
|
||||
{loading ? (
|
||||
<div className={styles.spinnerContainer}>
|
||||
<Spinner />
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
.message {
|
||||
padding: 10px 0 20px;
|
||||
letter-spacing: 0.1px;
|
||||
font-size: 13px;
|
||||
line-height: 33px;
|
||||
}
|
||||
|
||||
.message a {
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
margin: 0px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
import styles from './NotLoggedIn.css';
|
||||
import cn from 'classnames';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export default ({ showSignInDialog }) => (
|
||||
<div className={cn(styles.message, 'talk-embed-stream-not-logged-in')}>
|
||||
<div>
|
||||
<a onClick={showSignInDialog}>{t('settings.sign_in')}</a>{' '}
|
||||
{t('settings.to_access')}
|
||||
</div>
|
||||
<div>{t('from_settings_page')}</div>
|
||||
</div>
|
||||
);
|
||||
@@ -4,13 +4,32 @@ import Slot from 'coral-framework/components/Slot';
|
||||
import styles from './Profile.css';
|
||||
import TabPanel from '../containers/TabPanel';
|
||||
|
||||
const Profile = ({ username, emailAddress, root, slotPassthrough }) => {
|
||||
const DefaultProfileHeader = ({ username, emailAddress }) => (
|
||||
<div className={styles.userInfo}>
|
||||
<h2 className={styles.username}>{username}</h2>
|
||||
{emailAddress ? <p className={styles.email}>{emailAddress}</p> : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
DefaultProfileHeader.propTypes = {
|
||||
username: PropTypes.string,
|
||||
emailAddress: PropTypes.string,
|
||||
};
|
||||
|
||||
const Profile = ({ id, username, emailAddress, root, slotPassthrough }) => {
|
||||
return (
|
||||
<div className="talk-my-profile talk-profile-container">
|
||||
<div className={styles.userInfo}>
|
||||
<h2 className={styles.username}>{username}</h2>
|
||||
{emailAddress ? <p className={styles.email}>{emailAddress}</p> : null}
|
||||
</div>
|
||||
<Slot
|
||||
fill="profileHeader"
|
||||
size={1}
|
||||
defaultComponent={DefaultProfileHeader}
|
||||
passthrough={{
|
||||
...slotPassthrough,
|
||||
id,
|
||||
username,
|
||||
emailAddress,
|
||||
}}
|
||||
/>
|
||||
<Slot fill="profileSections" passthrough={slotPassthrough} />
|
||||
<TabPanel root={root} slotPassthrough={slotPassthrough} />
|
||||
</div>
|
||||
@@ -18,6 +37,7 @@ const Profile = ({ username, emailAddress, root, slotPassthrough }) => {
|
||||
};
|
||||
|
||||
Profile.propTypes = {
|
||||
id: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
emailAddress: PropTypes.string,
|
||||
root: PropTypes.object,
|
||||
|
||||
@@ -2,27 +2,17 @@ import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { withQuery } from 'coral-framework/hocs';
|
||||
import NotLoggedIn from '../components/NotLoggedIn';
|
||||
import { Spinner } from 'coral-ui';
|
||||
import Profile from '../components/Profile';
|
||||
import TabPanel from './TabPanel';
|
||||
import { getDefinitionName } from 'coral-framework/utils';
|
||||
|
||||
import { showSignInDialog } from 'coral-embed-stream/src/actions/login';
|
||||
import { getSlotFragmentSpreads } from 'coral-framework/utils';
|
||||
|
||||
class ProfileContainer extends Component {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.props.currentUser && nextProps.currentUser) {
|
||||
// Refetch after login.
|
||||
this.props.data.refetch();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { currentUser, showSignInDialog, root } = this.props;
|
||||
const { currentUser, root } = this.props;
|
||||
const { me } = this.props.root;
|
||||
const loading = this.props.data.loading;
|
||||
|
||||
@@ -30,10 +20,6 @@ class ProfileContainer extends Component {
|
||||
return <div>{this.props.data.error.message}</div>;
|
||||
}
|
||||
|
||||
if (!currentUser) {
|
||||
return <NotLoggedIn showSignInDialog={showSignInDialog} />;
|
||||
}
|
||||
|
||||
if (loading || !me) {
|
||||
return <Spinner />;
|
||||
}
|
||||
@@ -44,6 +30,7 @@ class ProfileContainer extends Component {
|
||||
|
||||
return (
|
||||
<Profile
|
||||
id={me.id}
|
||||
username={me.username}
|
||||
emailAddress={emailAddress}
|
||||
root={root}
|
||||
@@ -57,10 +44,9 @@ ProfileContainer.propTypes = {
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
currentUser: PropTypes.object,
|
||||
showSignInDialog: PropTypes.func,
|
||||
};
|
||||
|
||||
const slots = ['profileSections'];
|
||||
const slots = ['profileSections', 'profileSettings', 'profileHeader'];
|
||||
|
||||
const withProfileQuery = withQuery(
|
||||
gql`
|
||||
@@ -68,6 +54,15 @@ const withProfileQuery = withQuery(
|
||||
me {
|
||||
id
|
||||
username
|
||||
state {
|
||||
status {
|
||||
username {
|
||||
history {
|
||||
created_at
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
...${getDefinitionName(TabPanel.fragments.root)}
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
@@ -85,10 +80,6 @@ const mapStateToProps = state => ({
|
||||
currentUser: state.auth.user,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators({ showSignInDialog }, dispatch);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withProfileQuery
|
||||
)(ProfileContainer);
|
||||
export default compose(connect(mapStateToProps), withProfileQuery)(
|
||||
ProfileContainer
|
||||
);
|
||||
|
||||
@@ -214,7 +214,7 @@ AllCommentsPane.propTypes = {
|
||||
asset: PropTypes.object,
|
||||
currentUser: PropTypes.object,
|
||||
postFlag: PropTypes.func,
|
||||
postDontAgree: PropTypes.func,
|
||||
postDontAgree: PropTypes.func.isRequired,
|
||||
loadNewReplies: PropTypes.func,
|
||||
deleteAction: PropTypes.func,
|
||||
showSignInDialog: PropTypes.func,
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
getActionSummary,
|
||||
iPerformedThisAction,
|
||||
isCommentActive,
|
||||
isCommentDeleted,
|
||||
getShallowChanges,
|
||||
} from 'coral-framework/utils';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
@@ -183,7 +184,7 @@ export default class Comment extends React.Component {
|
||||
maxCharCount: PropTypes.number,
|
||||
root: PropTypes.object,
|
||||
loadMore: PropTypes.func,
|
||||
postDontAgree: PropTypes.func,
|
||||
postDontAgree: PropTypes.func.isRequired,
|
||||
animateEnter: PropTypes.bool,
|
||||
commentClassNames: PropTypes.array,
|
||||
comment: PropTypes.object.isRequired,
|
||||
@@ -409,6 +410,7 @@ export default class Comment extends React.Component {
|
||||
charCountEnable,
|
||||
showSignInDialog,
|
||||
liveUpdates,
|
||||
postDontAgree,
|
||||
emit,
|
||||
} = this.props;
|
||||
return (
|
||||
@@ -439,6 +441,7 @@ export default class Comment extends React.Component {
|
||||
key={reply.id}
|
||||
comment={reply}
|
||||
emit={emit}
|
||||
postDontAgree={postDontAgree}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -574,7 +577,9 @@ export default class Comment extends React.Component {
|
||||
'talk-stream-comment-header-tags-container'
|
||||
)}
|
||||
>
|
||||
{isStaff(comment.tags) ? <TagLabel>Staff</TagLabel> : null}
|
||||
{isStaff(comment.tags) ? (
|
||||
<TagLabel>{t('community.staff')}</TagLabel>
|
||||
) : null}
|
||||
|
||||
<Slot
|
||||
className={cn(
|
||||
@@ -742,8 +747,14 @@ export default class Comment extends React.Component {
|
||||
|
||||
return (
|
||||
<div className={rootClassName} id={id}>
|
||||
{this.renderComment()}
|
||||
{activeReplyBox === comment.id && this.renderReplyBox()}
|
||||
{isCommentDeleted(comment) ? (
|
||||
<CommentTombstone action="deleted" />
|
||||
) : (
|
||||
<div>
|
||||
{this.renderComment()}
|
||||
{activeReplyBox === comment.id && this.renderReplyBox()}
|
||||
</div>
|
||||
)}
|
||||
{this.renderRepliesContainer()}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,6 +13,8 @@ class CommentTombstone extends React.Component {
|
||||
return t('framework.comment_is_ignored');
|
||||
case 'reject':
|
||||
return t('framework.comment_is_rejected');
|
||||
case 'deleted':
|
||||
return t('framework.comment_is_deleted');
|
||||
default:
|
||||
return t('framework.comment_is_hidden');
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ const InactiveCommentLabel = ({ status, className, ...rest }) => {
|
||||
label = t('modqueue.rejected');
|
||||
icon = 'close';
|
||||
break;
|
||||
case 'SYSTEM_WITHHELD':
|
||||
label = t('modqueue.system_withheld');
|
||||
icon = 'flag';
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown inactive status ${status}`);
|
||||
}
|
||||
|
||||
@@ -265,7 +265,7 @@ StreamContainer.propTypes = {
|
||||
commentClassNames: PropTypes.array,
|
||||
setActiveStreamTab: PropTypes.func,
|
||||
postFlag: PropTypes.func,
|
||||
postDontAgree: PropTypes.func,
|
||||
postDontAgree: PropTypes.func.isRequired,
|
||||
deleteAction: PropTypes.func,
|
||||
showSignInDialog: PropTypes.func,
|
||||
currentUser: PropTypes.object,
|
||||
@@ -372,6 +372,7 @@ const slots = [
|
||||
'streamTabsPrepend',
|
||||
'streamTabPanes',
|
||||
'streamFilter',
|
||||
'stream',
|
||||
];
|
||||
|
||||
const fragments = {
|
||||
|
||||
@@ -177,8 +177,8 @@ button.comment__action-button[disabled],
|
||||
}
|
||||
|
||||
.talk-plugin-flags-popup-header {
|
||||
font-weight: bolder;
|
||||
font-size: 1.33rem;
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ const ConfigureCard = ({
|
||||
);
|
||||
|
||||
ConfigureCard.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
onCheckbox: PropTypes.func,
|
||||
checked: PropTypes.bool,
|
||||
|
||||
@@ -25,6 +25,9 @@ export default {
|
||||
'UnsuspendUserResponse',
|
||||
'UpdateAssetSettingsResponse',
|
||||
'UpdateAssetStatusResponse',
|
||||
'UpdateSettingsResponse'
|
||||
'UpdateSettingsResponse',
|
||||
'ChangePasswordResponse',
|
||||
'UpdateEmailAddressResponse',
|
||||
'AttachLocalAuthResponse'
|
||||
),
|
||||
};
|
||||
|
||||
@@ -169,7 +169,7 @@ export const withSetCommentStatus = withMutation(
|
||||
},
|
||||
update: proxy => {
|
||||
const fragment = gql`
|
||||
fragment Talk_SetCommentStatus on Comment {
|
||||
fragment Talk_SetCommentStatus_Comment on Comment {
|
||||
status
|
||||
status_history {
|
||||
type
|
||||
@@ -182,9 +182,11 @@ export const withSetCommentStatus = withMutation(
|
||||
const data = proxy.readFragment({ fragment, id: fragmentId });
|
||||
|
||||
data.status = status;
|
||||
|
||||
data.status_history = data.status_history
|
||||
? data.status_history
|
||||
: [];
|
||||
|
||||
data.status_history.push({
|
||||
__typename: 'CommentStatusHistory',
|
||||
type: status,
|
||||
@@ -590,6 +592,27 @@ export const withUpdateSettings = withMutation(
|
||||
}
|
||||
);
|
||||
|
||||
export const withChangePassword = withMutation(
|
||||
gql`
|
||||
mutation ChangePassword($input: ChangePasswordInput!) {
|
||||
changePassword(input: $input) {
|
||||
...ChangePasswordResponse
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
props: ({ mutate }) => ({
|
||||
changePassword: input => {
|
||||
return mutate({
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
export const withUpdateAssetSettings = withMutation(
|
||||
gql`
|
||||
mutation UpdateAssetSettings($id: ID!, $input: AssetSettingsInput!) {
|
||||
|
||||
@@ -6,4 +6,5 @@ export default {
|
||||
username: t('error.username'),
|
||||
confirmPassword: t('error.confirm_password'),
|
||||
organizationName: t('error.organization_name'),
|
||||
organizationContactEmail: t('error.organization_contact_email'),
|
||||
};
|
||||
|
||||
@@ -4,4 +4,5 @@ export default {
|
||||
confirmPassword: () => true,
|
||||
username: username => /^[a-zA-Z0-9_]+$/.test(username),
|
||||
organizationName: org => /^[a-zA-Z0-9_ ]+$/.test(org),
|
||||
organizationContactEmail: email => /^.+@.+\..+$/.test(email),
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'moment/locale/da';
|
||||
import 'moment/locale/de';
|
||||
import 'moment/locale/es';
|
||||
import 'moment/locale/fr';
|
||||
import 'moment/locale/nl';
|
||||
import 'moment/locale/pt-br';
|
||||
|
||||
import { createStorage } from 'coral-framework/services/storage';
|
||||
@@ -18,10 +19,10 @@ import daTA from 'timeago.js/locales/da';
|
||||
import deTA from 'timeago.js/locales/de';
|
||||
import esTA from 'timeago.js/locales/es';
|
||||
import frTA from 'timeago.js/locales/fr';
|
||||
import nlTA from 'timeago.js/locales/nl';
|
||||
import pt_BRTA from 'timeago.js/locales/pt_BR';
|
||||
import zh_CNTA from 'timeago.js/locales/zh_CN';
|
||||
import zh_TWTA from 'timeago.js/locales/zh_TW';
|
||||
import nl from 'timeago.js/locales/nl';
|
||||
|
||||
import ar from '../../../locales/ar.yml';
|
||||
import en from '../../../locales/en.yml';
|
||||
@@ -29,10 +30,10 @@ import da from '../../../locales/da.yml';
|
||||
import de from '../../../locales/de.yml';
|
||||
import es from '../../../locales/es.yml';
|
||||
import fr from '../../../locales/fr.yml';
|
||||
import nl_NL from '../../../locales/nl_NL.yml';
|
||||
import pt_BR from '../../../locales/pt_BR.yml';
|
||||
import zh_CN from '../../../locales/zh_CN.yml';
|
||||
import zh_TW from '../../../locales/zh_TW.yml';
|
||||
import nl_NL from '../../../locales/nl_NL.yml';
|
||||
|
||||
const defaultLanguage = process.env.TALK_DEFAULT_LANG;
|
||||
const translations = {
|
||||
@@ -112,10 +113,10 @@ export function setupTranslations() {
|
||||
ta.register('da', daTA);
|
||||
ta.register('de', deTA);
|
||||
ta.register('fr', frTA);
|
||||
ta.register('nl_NL', nlTA);
|
||||
ta.register('pt_BR', pt_BRTA);
|
||||
ta.register('zh_CN', zh_CNTA);
|
||||
ta.register('zh_TW', zh_TWTA);
|
||||
ta.register('nl_NL', nl);
|
||||
|
||||
timeagoInstance = ta();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { gql } from 'react-apollo';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import union from 'lodash/union';
|
||||
import get from 'lodash/get';
|
||||
import { capitalize } from 'coral-framework/helpers/strings';
|
||||
import assignWith from 'lodash/assignWith';
|
||||
import mapValues from 'lodash/mapValues';
|
||||
@@ -221,6 +222,13 @@ export function isCommentActive(commentStatus) {
|
||||
return ['NONE', 'ACCEPTED'].indexOf(commentStatus) >= 0;
|
||||
}
|
||||
|
||||
export function isCommentDeleted(comment) {
|
||||
return (
|
||||
get(comment, 'body', null) === null ||
|
||||
get(comment, 'deleted_at', null) !== null
|
||||
);
|
||||
}
|
||||
|
||||
export function getShallowChanges(a, b) {
|
||||
return union(Object.keys(a), Object.keys(b)).filter(key => a[key] !== b[key]);
|
||||
}
|
||||
@@ -237,7 +245,11 @@ export function getTotalReactionsCount(actionSummaries) {
|
||||
|
||||
// Like lodash merge but does not recurse into arrays.
|
||||
export function mergeExcludingArrays(objValue, srcValue) {
|
||||
if (typeof srcValue === 'object' && !Array.isArray(srcValue)) {
|
||||
if (
|
||||
typeof srcValue === 'object' &&
|
||||
!Array.isArray(srcValue) &&
|
||||
srcValue !== null
|
||||
) {
|
||||
return assignWith({}, objValue, srcValue, mergeExcludingArrays);
|
||||
}
|
||||
return srcValue;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import get from 'lodash/get';
|
||||
import moment from 'moment';
|
||||
|
||||
/**
|
||||
* getReliability
|
||||
@@ -33,3 +34,18 @@ export const isSuspended = user => {
|
||||
export const isBanned = user => {
|
||||
return get(user, 'state.status.banned.status');
|
||||
};
|
||||
|
||||
/**
|
||||
* canUsernameBeUpdated
|
||||
* retrieves boolean whether a username can be updated or not
|
||||
*/
|
||||
|
||||
export const canUsernameBeUpdated = status => {
|
||||
const oldestEditTime = moment()
|
||||
.subtract(14, 'days')
|
||||
.toDate();
|
||||
|
||||
return !status.username.history.some(({ created_at }) =>
|
||||
moment(created_at).isAfter(oldestEditTime)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
.root {
|
||||
vertical-align: middle;
|
||||
vertical-align: sub;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
box-sizing: border-box;
|
||||
background: white;
|
||||
border-radius: 3px;
|
||||
padding: 20px 10px;
|
||||
padding: 10px 10px;
|
||||
z-index: 300;
|
||||
right: 1%;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
const { pluginsPath } = require('../plugins');
|
||||
|
||||
const buildTargets = ['coral-admin'];
|
||||
|
||||
const buildEmbeds = ['stream'];
|
||||
|
||||
const specPattern = 'client/**/__tests__/**/*.spec.js?(x)';
|
||||
|
||||
module.exports = {
|
||||
rootDir: '../',
|
||||
testMatch: [
|
||||
`<rootDir>/${specPattern}`,
|
||||
`<rootDir>/plugins/**/${specPattern}`,
|
||||
],
|
||||
setupTestFrameworkScriptFile: '<rootDir>/test/client/setupJest.js',
|
||||
modulePaths: [
|
||||
'<rootDir>/plugins',
|
||||
'<rootDir>/client',
|
||||
...buildTargets.map(target => `<rootDir>/client/${target}/src`),
|
||||
...buildEmbeds.map(embed => `<rootDir>/client/coral-embed-${embed}/src`),
|
||||
],
|
||||
moduleFileExtensions: ['js', 'jsx', 'json', 'yaml', 'yml'],
|
||||
moduleDirectories: ['node_modules'],
|
||||
transform: {
|
||||
'^.+\\.jsx?$': 'babel-jest',
|
||||
'\\.ya?ml$': '<rootDir>/test/client/yamlTransformer.js',
|
||||
},
|
||||
testResultsProcessor: process.env.JEST_REPORTER,
|
||||
moduleNameMapper: {
|
||||
'^plugin-api\\/(.*)$': '<rootDir>/plugin-api/$1',
|
||||
'^plugins\\/(.*)$': '<rootDir>/plugins/$1',
|
||||
'^pluginsConfig$': pluginsPath,
|
||||
'\\.(scss|css|less)$': 'identity-obj-proxy',
|
||||
'\\.(gif|ttf|eot|svg)$': '<rootDir>/test/client/fileMock.js',
|
||||
},
|
||||
};
|
||||
@@ -212,6 +212,13 @@ const CONFIG = {
|
||||
RECAPTCHA_PUBLIC: process.env.TALK_RECAPTCHA_PUBLIC,
|
||||
RECAPTCHA_SECRET: process.env.TALK_RECAPTCHA_SECRET,
|
||||
|
||||
// RECAPTCHA_WINDOW is the rate limit's time interval
|
||||
RECAPTCHA_WINDOW: process.env.TALK_RECAPTCHA_WINDOW || '10m',
|
||||
|
||||
// After RECAPTCHA_INCORRECT_TRIGGER incorrect attempts, recaptcha will be required.
|
||||
RECAPTCHA_INCORRECT_TRIGGER:
|
||||
process.env.TALK_RECAPTCHA_INCORRECT_TRIGGER || 5,
|
||||
|
||||
// WEBSOCKET_LIVE_URI is the absolute url to the live endpoint.
|
||||
WEBSOCKET_LIVE_URI: process.env.TALK_WEBSOCKET_LIVE_URI || null,
|
||||
|
||||
|
||||
+3
-1
@@ -41,7 +41,7 @@ relative_link: false
|
||||
future: true
|
||||
highlight:
|
||||
enable: false
|
||||
|
||||
|
||||
# Home page setting
|
||||
# path: Root path for your blogs index page. (default = '')
|
||||
# per_page: Posts displayed per page. (0 = disable pagination)
|
||||
@@ -114,6 +114,8 @@ sidebar:
|
||||
url: /integrating/styling-css/
|
||||
- title: Translations and i18n
|
||||
url: /integrating/translations-i18n/
|
||||
- title: GDPR Compliance
|
||||
url: /integrating/gdpr/
|
||||
- title: Product Guide
|
||||
children:
|
||||
- title: How Talk Works
|
||||
|
||||
@@ -316,6 +316,18 @@ default to providing only a time based lockout. Refer to
|
||||
[reCAPTCHA](https://www.google.com/recaptcha/intro/index.html) for information
|
||||
on getting an account setup.
|
||||
|
||||
## TALK_RECAPTCHA_WINDOW
|
||||
|
||||
The rate limit time interval that there can be [TALK_RECAPTCHA_INCORRECT_TRIGGER](#talk_recaptcha_incorrect_trigger) incorrect attempts until the reCAPTCHA is
|
||||
marked as required, parsed by
|
||||
[ms](https://www.npmjs.com/package/ms). (Default `10m`)
|
||||
|
||||
## TALK_RECAPTCHA_INCORRECT_TRIGGER
|
||||
|
||||
The number of times that an incorrect login can be entered before within a time
|
||||
perioud indicated by [TALK_RECAPTCHA_WINDOW](#talk_recaptcha_window) until the
|
||||
reCAPTCHA is marked as required. (Default `5`)
|
||||
|
||||
## TALK_REDIS_CLIENT_CONFIGURATION
|
||||
|
||||
Configuration overrides for the redis client configuration in a JSON encoded
|
||||
@@ -531,4 +543,4 @@ Sets the logging level for the context logger (from [Bunyan](https://github.com/
|
||||
A JSON string representing the configuration passed to the
|
||||
[fetch](https://www.npmjs.com/package/node-fetch) call for the scraper. It
|
||||
can be used to set an authorization header, or change the user agent. (Default
|
||||
`{}`)
|
||||
`{}`)
|
||||
|
||||
@@ -28,8 +28,7 @@ Plugins are additional functionality which are optional to use with Talk. You
|
||||
can turn these on or off, depending on your specific needs. Plugins are either
|
||||
part of our core plugins, which ship with Talk, or they are developed by 3rd
|
||||
parties and either used privately and internally, or are open sourced for use
|
||||
across the greater community. You can explore the plugins we offer by visiting our [Default Plugins](/talk/default-plugins/)
|
||||
and [Additional Plugins](/talk/additional-plugins/) pages.
|
||||
across the greater community. You can explore the plugins we offer by visiting our [Plugin Directory](/talk/plugins-directory/).
|
||||
|
||||
## Recipes
|
||||
|
||||
|
||||
@@ -170,8 +170,9 @@ moderators.
|
||||
|
||||
All your team and commenters show in the People sub-tab. From here, you can
|
||||
manage your team members’ roles (Admins, Moderators, Staff), as well as search
|
||||
for commenters and take action on them (e.g. Ban/Un-ban, Suspend, etc.). ###
|
||||
Configure
|
||||
for commenters and take action on them (e.g. Ban/Un-ban, Suspend, etc.).
|
||||
|
||||
### Configure
|
||||
|
||||
See [Configuring Talk](/talk/configuring-talk/).
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ trying to improve a broken part of the internet.
|
||||
|
||||
## How do I add the Toxic Comments plugin?
|
||||
To enable this behavior, visit the
|
||||
[talk-plugin-toxic-comments](/talk/additional-plugins/#talk-plugin-toxic-comments)
|
||||
[talk-plugin-toxic-comments](/talk/plugin/talk-plugin-toxic-comments)
|
||||
plugin documentation.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: GDPR Compliance
|
||||
permalink: /integrating/gdpr/
|
||||
---
|
||||
|
||||
In order to facilitate compliance with the
|
||||
[EU General Data Protection Regulation (GDPR)](https://www.eugdpr.org/), you
|
||||
can enable the following plugins:
|
||||
|
||||
- [talk-plugin-auth](/talk/plugin/talk-plugin-auth) - to facilitate username and password changes
|
||||
- [talk-plugin-local-auth](/talk/plugin/talk-plugin-local-auth) - to facilitate email changes and email association
|
||||
- [talk-plugin-profile-data](/talk/plugin/talk-plugin-profile-data) - to facilitate account download and deletion
|
||||
|
||||
Even if you don't reside in a location where GDPR will apply, it is recommended
|
||||
to enable these features as a best practice to provide your users with control over their
|
||||
own data.
|
||||
|
||||
## GPDR Feature Overview
|
||||
|
||||
Integrating our GDPR tools will give your users and organizations the following benefits:
|
||||
|
||||
- **Download my comment data**: Users can request a download of their comments. An email with a link is emailed to them to download a CSV with each comment they've made, what story it was made on, and the comment's ID and timestamp.
|
||||
- **Delete my account**: Users can request deletion of their account. Deleted account requests are pending for 24 hours to allow the user to download their comments, or to change their mind and reactivate their account before the expiry. Account deletions remove all of their comments from the site, all their comments and actions from the database, and their account info from our system.
|
||||
- **Add an email to an Oauth/external account**: Users are prompted to add an email to their non-Talk account (Facebook, Google, external, etc) so that they can take part in GDPR and other features requiring email communication.
|
||||
- **Change my username**: Users can update their username. This is capped at once every 2 weeks.
|
||||
- **Change my email**: Users can change their email.
|
||||
- **Change my password**: Users can change their password.
|
||||
|
||||
## Custom Authentication Solutions
|
||||
|
||||
As many of the newsrooms who have integrated Talk have followed our guides on
|
||||
[Integrating Authentication](/talk/integrating/authentication/), we have also
|
||||
provided tools for those newsrooms to integrate GDPR features into their
|
||||
existing workflows.
|
||||
|
||||
### Account Data
|
||||
|
||||
Through the [talk-plugin-profile-data](/talk/plugin/talk-plugin-profile-data)
|
||||
plugin we allow users to download and delete their account data easily. For
|
||||
custom integrations, this isn't always possible, so we instead provide some
|
||||
GraphQL mutations designed to allow you to integrate it into your existing user
|
||||
interfaces or exports.
|
||||
|
||||
- `downloadUser(id: ID!)` - lets you grab the direct link to download a users
|
||||
account in a zip format. From there, you can integrate it into your existing
|
||||
data export or simply proxy it to the user to allow them to download it
|
||||
elsewhere in your UI.
|
||||
- `delUser(id: ID!)` - lets you delete the specified user
|
||||
|
||||
**Note: These mutations require an administrative token**
|
||||
|
||||
If you would prefer to write your own user interfaces or integrate it into your
|
||||
own, you can disable the client plugin for [talk-plugin-profile-data](/talk/plugin/talk-plugin-profile-data)
|
||||
but keep the server side plugin active (See [Server and Client Plugins](/talk/plugins/#server-and-client-plugins) for more information).
|
||||
@@ -70,7 +70,7 @@ The most important predictors of the success of an online community are:
|
||||
|
||||
* Utilize [our Toxic Comments plugin](https://blog.coralproject.net/toxic-avenging/), developed with Google Jigsaw, to improve commenter behavior and use AI to help identify and prevent the most abusive comments from appearing on your site.
|
||||
|
||||
* Enable our [Akismet plugin](https://coralproject.github.io/talk/additional-plugins/#talk-plugin-akismet) to keep spam from appearing in your comments.
|
||||
* Enable our [Akismet plugin](/talk/plugin/talk-plugin-akismet) to keep spam from appearing in your comments.
|
||||
|
||||
* Use keyboard shortcuts in the moderation queue to moderate quickly (type '?' in the moderation view to see the list of shortcuts), and if there is a sudden deluge of comments, ask for someone in your newsroom to help you moderate. Talk will notify you in the moderation interface if someone else moderates a comment that's already on your screen.
|
||||
* Click on a community member's name in the moderation interface to review all their comments, and see if there is a clear pattern of abuse among their Rejected comments. You can also select all their recent comments and delete them in bulk.
|
||||
@@ -83,7 +83,7 @@ The most important predictors of the success of an online community are:
|
||||
|
||||

|
||||
|
||||
* If you find new commenters are causing a lot of trouble with their first comments, consider changing [the User Karma threshold](https://coralproject.github.io/talk/trust/) from negative one to zero, forcing every new commenter's first comment into pre-moderation.
|
||||
* If you find new commenters are causing a lot of trouble with their first comments, consider changing [the User Karma threshold](/talk/trust/) from negative one to zero, forcing every new commenter's first comment into pre-moderation.
|
||||
|
||||
#### Effectively
|
||||
|
||||
|
||||
@@ -3,10 +3,17 @@
|
||||
tags:
|
||||
- moderation
|
||||
- name: talk-plugin-auth
|
||||
description: Enables internal authentication from Coral.
|
||||
description: Enables internal sign-in authentication.
|
||||
tags:
|
||||
- default
|
||||
- auth
|
||||
- gdpr
|
||||
- name: talk-plugin-local-auth
|
||||
description: Enables email and password based authentication.
|
||||
tags:
|
||||
- default
|
||||
- auth
|
||||
- gdpr
|
||||
- name: talk-plugin-author-menu
|
||||
description: Enables the comment author name plugin area.
|
||||
tags:
|
||||
@@ -81,6 +88,11 @@
|
||||
description: Shows a Link button on comments for direct-linking to a comment.
|
||||
tags:
|
||||
- default
|
||||
- name: talk-plugin-profile-data
|
||||
description: Enables users to manage their own data within Talk.
|
||||
tags:
|
||||
- default
|
||||
- gdpr
|
||||
- name: talk-plugin-remember-sort
|
||||
description: Remembers the sort selection made by a user.
|
||||
- name: talk-plugin-respect
|
||||
|
||||
@@ -33,8 +33,7 @@ By default, Talk has various plugins provided by default. We can see this in `pl
|
||||
"talk-plugin-sort-most-respected",
|
||||
"talk-plugin-sort-newest",
|
||||
"talk-plugin-sort-oldest",
|
||||
"talk-plugin-viewing-options",
|
||||
"talk-plugin-profile-settings"
|
||||
"talk-plugin-viewing-options"
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -81,6 +80,7 @@ You won't have to use this to build plugins, but it's helpful to find where to e
|
||||
* `adminCommentMoreDetails`
|
||||
* `adminCommentLabels`
|
||||
* `adminModerationSettings`
|
||||
* `adminOrganizationSettings`
|
||||
* `adminStreamSettings`
|
||||
* `adminTechSettings`
|
||||
* `adminCommentInfoBar`
|
||||
|
||||
@@ -5,8 +5,8 @@ permalink: /contact/
|
||||
|
||||
## How can I get help integrating Talk into my newsroom?
|
||||
|
||||
We're here to help with newsrooms of all sizes. Email our Integration Engineer
|
||||
([jeff@mozillafoundation.org](mailto:jeff@mozillafoundation.org)) to set up a meeting.
|
||||
We're here to help with newsrooms of all sizes. Email our Support Team
|
||||
([support@coralproject.net](mailto:support@coralproject.net)) to set up a meeting.
|
||||
|
||||
## How do I request a feature or submit a bug?
|
||||
|
||||
@@ -14,4 +14,4 @@ The best way is to [submit a GitHub issue](https://github.com/coralproject/talk/
|
||||
|
||||
## How can our dev team contribute to Talk?
|
||||
|
||||
We are lucky to work with newsroom dev teams and individual contributors who span the world, and come from newsrooms of all sizes. You can read our [Contribution Guide](https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md) to get started, but feel free to reach out to us via GitHub, or get in touch directly with Jeff via jeff@mozillafoundation.org.
|
||||
We are lucky to work with newsroom dev teams and individual contributors who span the world, and come from newsrooms of all sizes. You can read our [Contribution Guide](https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md) to get started, but feel free to reach out to us via GitHub, or get in touch with us directly via support@coralproject.net.
|
||||
|
||||
@@ -74,7 +74,6 @@ example issuer and Talk must match:
|
||||
|------|----------------------|
|
||||
|`JWT_ISSUER`|`JWT_ISSUER`|
|
||||
|`JWT_AUDIENCE`|`JWT_AUDIENCE`|
|
||||
|`JWT_AUDIENCE`|`JWT_AUDIENCE`|
|
||||
|`SECRET`|`JWT_SECRET`*|
|
||||
|
||||
\* Note that secrets is a pretty complex topic, refer to the
|
||||
@@ -83,4 +82,4 @@ reference, the basic takeaway is that the secret used to sign the tokens issued
|
||||
by the issuer must be able to be verified by Talk.
|
||||
|
||||
For an example of implementing the plugin, refer to [`tokenUserNotFound`](/talk/reference/server/#tokenUserNotFound)
|
||||
reference.
|
||||
reference.
|
||||
|
||||
@@ -65,10 +65,6 @@ First, you'll enable `talk-plugin-author-menu`, as this houses the Ignore button
|
||||
|
||||
And then we will enable the Ignore User plugin: `talk-plugin-ignore-user`.
|
||||
|
||||
And finally, we will need to enable Profile Settings; this is the tab on My Profile > Settings where commenters can manage their Ignored Users list.
|
||||
|
||||
`talk-plugin-profile-settings`
|
||||
|
||||
### Featured Comments
|
||||
|
||||
To enable the featuring of comments, you'll need to activate `talk-plugin-featured-comments`. If you would like the Featured Comments tab to be the default tab you land on for the stream, you will need to set the default tab ENV variable:
|
||||
|
||||
@@ -36,10 +36,6 @@ Adding the `talk-plugin-notifications` plugin will also enable the `notification
|
||||
|
||||
See https://github.com/coralproject/talk/blob/8b669a31c551a042f0f079d8cfc16825673eb8f0/plugins/talk-plugin-notifications-reply/index.js for an example.
|
||||
|
||||
### Commenter Notification Settings
|
||||
|
||||
Note that notifications REQUIRE the `talk-plugin-profile-settings` plugin; this is where on My Profile commenters will enable and manage their notification settings.
|
||||
|
||||
### Notification Categories
|
||||
|
||||
Talk currently supports the following Notifications options out of the box:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
../../../plugins/talk-plugin-local-auth/README.md
|
||||
@@ -0,0 +1 @@
|
||||
../../../plugins/talk-plugin-profile-data/README.md
|
||||
@@ -9,11 +9,16 @@ functionality. We provide methods to inject behavior into the server side and
|
||||
the client side application to affect different parts of the application
|
||||
life cycle.
|
||||
|
||||
## Recipes
|
||||
## Server and Client Plugins
|
||||
|
||||
Recipes are plugin templates provided by the Coral Core team. Developers can use
|
||||
these recipes to build their own plugins. You can find all the Talk recipes
|
||||
here: [github.com/coralproject/talk-recipes](https://github.com/coralproject/talk-recipes/).
|
||||
When you're adding a plugin to Talk, you can specify it in the `client` and/or
|
||||
the `server` section. If you only want to enable the server side component of a
|
||||
plugin, you simply only specify the plugin in the `server` section. If you only
|
||||
want the client side plugin, the `client` section.
|
||||
|
||||
Plugins listed in the [Plugins Directory](/talk/plugins-directory/) will
|
||||
indicate if they have/support a client/server plugin, and should be activated
|
||||
accordingly.
|
||||
|
||||
## Plugin Registration
|
||||
|
||||
@@ -117,3 +122,9 @@ assets inside the image as well.
|
||||
|
||||
For more information on the onbuild image, refer to the
|
||||
[Installation from Docker](/talk/installation-from-docker/) documentation.
|
||||
|
||||
## Recipes
|
||||
|
||||
Recipes are plugin templates provided by the Coral Core team. Developers can use
|
||||
these recipes to build their own plugins. You can find all the Talk recipes
|
||||
here: [github.com/coralproject/talk-recipes](https://github.com/coralproject/talk-recipes/).
|
||||
|
||||
@@ -3,8 +3,9 @@ title: Plugins Directory
|
||||
permalink: /plugins-directory/
|
||||
layout: plugins
|
||||
data: plugins
|
||||
class: plugins
|
||||
---
|
||||
|
||||
Talk provides a growing ecosystem of plugins that interact with our extensive
|
||||
Server and Client API's. Below you can search for a plugin to use with Talk and
|
||||
discover what their requirements and configuration are.
|
||||
discover what their requirements and configuration are.
|
||||
|
||||
+5
-4
@@ -4,9 +4,9 @@
|
||||
<h1>{{ page.title }}</h1>
|
||||
<hr/>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{{ page.content }}
|
||||
|
||||
|
||||
<div class="form-group mt-5">
|
||||
<div class="clearfix">
|
||||
<label for="exampleInputEmail1">Plugin Search</label>
|
||||
@@ -15,6 +15,7 @@
|
||||
<input type="text" class="form-control" id="plugin-search-input" aria-describedby="pluginSearchHelp">
|
||||
<small id="pluginSearchHelp" class="form-text text-muted">Enter a few keywords about the plugin to filter the list</small>
|
||||
</div>
|
||||
|
||||
<div class="row plugins">
|
||||
{% for plugin in _.sortBy(site.data[page.data], 'name') %}
|
||||
<div class="col-sm-6 plugin d-block">
|
||||
@@ -25,7 +26,7 @@
|
||||
{% if plugin.tags %}
|
||||
<p class="card-text">
|
||||
{% for tag in plugin.tags %}
|
||||
<span class="badge badge-{% if tag == "default" %}success{% else %}light{% endif %}">{{ tag }}</span>
|
||||
<a class="plain-link" title="Search for plugins with the {{ tag }} tag" href="?q={{ tag }}"><span class="badge badge-light badge-tag badge-tag-{{ _.kebabCase(tag) }}">{{ tag }}</span></a>
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
@@ -35,4 +36,4 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</article>
|
||||
<script>window.SEARCH_INDEX = {{ lunr_index(site.data[page.data]) }}</script>
|
||||
<script>window.SEARCH_INDEX = {{ lunr_index(site.data[page.data]) }}</script>
|
||||
|
||||
+14
@@ -448,3 +448,17 @@ a.brand {
|
||||
.plugin {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.badge-tag {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.badge-tag-default {
|
||||
background: #28a745;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.badge-tag-gdpr {
|
||||
background: rgb(0, 102, 176);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
+17
@@ -1,6 +1,17 @@
|
||||
/* global lunr */
|
||||
/* eslint-env browser */
|
||||
|
||||
// Sourced from https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
|
||||
function getParameterByName(name, url) {
|
||||
if (!url) url = window.location.href;
|
||||
name = name.replace(/[\[\]]/g, '\\$&');
|
||||
var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
|
||||
results = regex.exec(url);
|
||||
if (!results) return null;
|
||||
if (!results[2]) return '';
|
||||
return decodeURIComponent(results[2].replace(/\+/g, ' '));
|
||||
}
|
||||
|
||||
// Sourced from https://github.com/hexojs/site/blob/8e8ed4901769abbf76263125f82832df76ced58b/themes/navy/source/js/plugins.js.
|
||||
(function() {
|
||||
'use strict';
|
||||
@@ -60,6 +71,12 @@
|
||||
updateCount(elements.length);
|
||||
}
|
||||
|
||||
var searchParam = getParameterByName('q');
|
||||
if (searchParam && searchParam.length > 0) {
|
||||
$input.value = searchParam;
|
||||
search(searchParam);
|
||||
}
|
||||
|
||||
$input.addEventListener('input', function() {
|
||||
var value = this.value;
|
||||
|
||||
|
||||
@@ -371,6 +371,14 @@ class ErrParentDoesNotVisible extends TalkError {
|
||||
}
|
||||
}
|
||||
|
||||
class ErrPasswordIncorrect extends TalkError {
|
||||
constructor() {
|
||||
super('Your current password was entered incorrectly', {
|
||||
translation_key: 'PASSWORD_INCORRECT',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TalkError,
|
||||
ErrAlreadyExists,
|
||||
@@ -395,6 +403,7 @@ module.exports = {
|
||||
ErrNotFound,
|
||||
ErrNotVerified,
|
||||
ErrParentDoesNotVisible,
|
||||
ErrPasswordIncorrect,
|
||||
ErrPasswordResetToken,
|
||||
ErrPasswordTooShort,
|
||||
ErrPermissionUpdateUsername,
|
||||
|
||||
@@ -10,6 +10,9 @@ const secrets = require('../secrets');
|
||||
// Errors.
|
||||
const errors = require('../errors');
|
||||
|
||||
// URLs.
|
||||
const url = require('../url');
|
||||
|
||||
// Graph.
|
||||
const { getBroker } = require('./subscriptions/broker');
|
||||
const { getPubsub } = require('./subscriptions/pubsub');
|
||||
@@ -58,6 +61,7 @@ const defaultConnectors = {
|
||||
errors,
|
||||
config,
|
||||
secrets,
|
||||
url,
|
||||
models: {
|
||||
Action,
|
||||
Asset,
|
||||
|
||||
@@ -137,6 +137,13 @@ class Context {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* masqueradeAs will allow a given context to be copied to a new user.
|
||||
*/
|
||||
masqueradeAs(user) {
|
||||
return new Context(merge({}, this, { user }));
|
||||
}
|
||||
|
||||
/**
|
||||
* forSystem returns a system context object that can be used for internal
|
||||
* operations.
|
||||
|
||||
@@ -71,7 +71,7 @@ const findOrCreateAssetByURL = async (ctx, url) => {
|
||||
// Check for whitelisting + get the settings at the same time.
|
||||
const [whitelisted, settings] = await Promise.all([
|
||||
DomainList.urlCheck(url),
|
||||
Settings.load('autoCloseStream closedTimeout'),
|
||||
Settings.select('autoCloseStream', 'closedTimeout'),
|
||||
]);
|
||||
|
||||
// If the domain wasn't whitelisted, then we shouldn't create this asset!
|
||||
|
||||
+24
-10
@@ -94,6 +94,7 @@ const getCommentCountByQuery = (ctx, options) => {
|
||||
author_id,
|
||||
tags,
|
||||
action_type,
|
||||
excludeDeleted,
|
||||
} = options;
|
||||
|
||||
// If user queries for statuses other than NONE and/or ACCEPTED statuses, it needs
|
||||
@@ -120,6 +121,12 @@ const getCommentCountByQuery = (ctx, options) => {
|
||||
query.merge({ author_id });
|
||||
}
|
||||
|
||||
if (excludeDeleted) {
|
||||
// The null query matches documents that either contain the `deleted_at`
|
||||
// field whose value is null or that do not contain the `deleted_at` field.
|
||||
query.merge({ deleted_at: null });
|
||||
}
|
||||
|
||||
if (ctx.user != null && ctx.user.can(SEARCH_OTHERS_COMMENTS) && action_type) {
|
||||
query.merge({
|
||||
[`action_counts.${sc(action_type.toLowerCase())}`]: {
|
||||
@@ -328,11 +335,12 @@ const getCommentsByQuery = async (
|
||||
sortOrder,
|
||||
sortBy,
|
||||
excludeIgnored,
|
||||
excludeDeleted,
|
||||
tags,
|
||||
action_type,
|
||||
}
|
||||
) => {
|
||||
let comments = CommentModel.find();
|
||||
const query = CommentModel.find();
|
||||
|
||||
// Enforce that the limit must be gte 0 if this option is not true.
|
||||
if (!ALLOW_NO_LIMIT_QUERIES && limit < 0) {
|
||||
@@ -350,11 +358,17 @@ const getCommentsByQuery = async (
|
||||
}
|
||||
|
||||
if (statuses) {
|
||||
comments = comments.where({ status: { $in: statuses } });
|
||||
query.merge({ status: { $in: statuses } });
|
||||
}
|
||||
|
||||
if (excludeDeleted) {
|
||||
// The null query matches documents that either contain the `deleted_at`
|
||||
// field whose value is null or that do not contain the `deleted_at` field.
|
||||
query.merge({ deleted_at: null });
|
||||
}
|
||||
|
||||
if (ctx.user != null && ctx.user.can(SEARCH_OTHERS_COMMENTS) && action_type) {
|
||||
comments = comments.where({
|
||||
query.merge({
|
||||
[`action_counts.${sc(action_type.toLowerCase())}`]: {
|
||||
$gt: 0,
|
||||
},
|
||||
@@ -362,7 +376,7 @@ const getCommentsByQuery = async (
|
||||
}
|
||||
|
||||
if (ids) {
|
||||
comments = comments.find({
|
||||
query.merge({
|
||||
id: {
|
||||
$in: ids,
|
||||
},
|
||||
@@ -370,7 +384,7 @@ const getCommentsByQuery = async (
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
comments = comments.find({
|
||||
query.merge({
|
||||
'tags.tag.name': {
|
||||
$in: tags,
|
||||
},
|
||||
@@ -383,17 +397,17 @@ const getCommentsByQuery = async (
|
||||
(ctx.user.can(SEARCH_OTHERS_COMMENTS) || ctx.user.id === author_id) &&
|
||||
author_id != null
|
||||
) {
|
||||
comments = comments.where({ author_id });
|
||||
query.merge({ author_id });
|
||||
}
|
||||
|
||||
if (asset_id) {
|
||||
comments = comments.where({ asset_id });
|
||||
query.merge({ asset_id });
|
||||
}
|
||||
|
||||
// We perform the undefined check because, null, is a valid state for the
|
||||
// search to be with, which indicates that it is at depth 0.
|
||||
if (parent_id !== undefined) {
|
||||
comments = comments.where({ parent_id });
|
||||
query.merge({ parent_id });
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -402,12 +416,12 @@ const getCommentsByQuery = async (
|
||||
ctx.user.ignoresUsers &&
|
||||
ctx.user.ignoresUsers.length > 0
|
||||
) {
|
||||
comments = comments.where({
|
||||
query.merge({
|
||||
author_id: { $nin: ctx.user.ignoresUsers },
|
||||
});
|
||||
}
|
||||
|
||||
return executeWithSort(ctx, comments, { cursor, sortOrder, sortBy, limit });
|
||||
return executeWithSort(ctx, query, { cursor, sortOrder, sortBy, limit });
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+55
-18
@@ -1,23 +1,60 @@
|
||||
const SettingsService = require('../../services/settings');
|
||||
const Settings = require('../../services/settings');
|
||||
const DataLoader = require('dataloader');
|
||||
const { zipObject } = require('lodash');
|
||||
|
||||
/**
|
||||
* Creates a set of loaders based on a GraphQL context.
|
||||
* @param {Object} context the context of the GraphQL request
|
||||
* @return {Object} object of loaders
|
||||
* SettingsLoader manages loading specific fields only of the Settings object.
|
||||
*/
|
||||
module.exports = () => {
|
||||
const loader = new DataLoader(selections =>
|
||||
Promise.all(
|
||||
selections.map(fields => {
|
||||
return SettingsService.retrieve(fields);
|
||||
})
|
||||
)
|
||||
);
|
||||
class SettingsLoader {
|
||||
constructor() {
|
||||
this._loader = new DataLoader(this._batchLoadFn.bind(this));
|
||||
this._cache = null;
|
||||
}
|
||||
|
||||
return {
|
||||
Settings: {
|
||||
load: (fields = false) => loader.load(fields),
|
||||
},
|
||||
};
|
||||
};
|
||||
async _batchLoadFn(fields) {
|
||||
// Load a settings object with all the requested fields, unless we have the
|
||||
// entire object cached, in which case we'll return the whole cache.
|
||||
const obj = this._cache
|
||||
? await this._cache
|
||||
: await Settings.select(...fields);
|
||||
|
||||
// Return the specific fields for each of the fields that were loaded.
|
||||
return fields.map(field => obj[field]);
|
||||
}
|
||||
|
||||
/**
|
||||
* load will return the entire Settings object with all fields.
|
||||
*/
|
||||
load() {
|
||||
if (this._cache) {
|
||||
// Return the cached settings promise.
|
||||
return this._cache;
|
||||
}
|
||||
|
||||
// Create a promise that will return the settings object.
|
||||
const promise = Settings.retrieve();
|
||||
|
||||
// Set this as the cached value.
|
||||
this._cache = promise;
|
||||
|
||||
// Return the promised settings.
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* select will return a promise which resolves to the Settings object that
|
||||
* contains the requested fields only.
|
||||
*
|
||||
* @param {Array<String>} fields the fields from Settings we want to load.
|
||||
*/
|
||||
async select(...fields) {
|
||||
// Load all the values for the specific fields.
|
||||
const values = await this._loader.loadMany(fields);
|
||||
|
||||
// Zip up the fields and values to create an object to return and return the
|
||||
// assembled Settings object.
|
||||
return zipObject(fields, values);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = () => ({ Settings: new SettingsLoader() });
|
||||
|
||||
@@ -14,8 +14,15 @@ const getActionItem = async (ctx, { item_id, item_type }) => {
|
||||
const { loaders: { Comments, Users } } = ctx;
|
||||
|
||||
switch (item_type) {
|
||||
case 'COMMENTS':
|
||||
return Comments.get.load(item_id);
|
||||
case 'COMMENTS': {
|
||||
// Get a comment by ID, unless the comment is deleted, then return null.
|
||||
const comment = await Comments.get.load(item_id);
|
||||
if (comment.deleted_at) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return comment;
|
||||
}
|
||||
case 'USERS':
|
||||
return Users.getByID.load(item_id);
|
||||
default:
|
||||
|
||||
+110
-51
@@ -1,5 +1,9 @@
|
||||
const { ErrNotFound, ErrNotAuthorized } = require('../../errors');
|
||||
const UsersService = require('../../services/users');
|
||||
const {
|
||||
ErrNotFound,
|
||||
ErrNotAuthorized,
|
||||
ErrPasswordIncorrect,
|
||||
} = require('../../errors');
|
||||
const Users = require('../../services/users');
|
||||
const migrationHelpers = require('../../services/migration/helpers');
|
||||
const {
|
||||
CHANGE_USERNAME,
|
||||
@@ -8,11 +12,12 @@ const {
|
||||
SET_USER_BAN_STATUS,
|
||||
SET_USER_SUSPENSION_STATUS,
|
||||
UPDATE_USER_ROLES,
|
||||
DELETE_USER,
|
||||
DELETE_OTHER_USER,
|
||||
CHANGE_PASSWORD,
|
||||
} = require('../../perms/constants');
|
||||
|
||||
const setUserUsernameStatus = async (ctx, id, status) => {
|
||||
const user = await UsersService.setUsernameStatus(id, status, ctx.user.id);
|
||||
const user = await Users.setUsernameStatus(id, status, ctx.user.id);
|
||||
if (status === 'REJECTED') {
|
||||
ctx.pubsub.publish('usernameRejected', user);
|
||||
} else if (status === 'APPROVED') {
|
||||
@@ -21,12 +26,7 @@ const setUserUsernameStatus = async (ctx, id, status) => {
|
||||
};
|
||||
|
||||
const setUserBanStatus = async (ctx, id, status = false, message = null) => {
|
||||
const user = await UsersService.setBanStatus(
|
||||
id,
|
||||
status,
|
||||
ctx.user.id,
|
||||
message
|
||||
);
|
||||
const user = await Users.setBanStatus(id, status, ctx.user.id, message);
|
||||
if (user.banned) {
|
||||
ctx.pubsub.publish('userBanned', user);
|
||||
}
|
||||
@@ -38,52 +38,57 @@ const setUserSuspensionStatus = async (
|
||||
until = null,
|
||||
message = null
|
||||
) => {
|
||||
const user = await UsersService.setSuspensionStatus(
|
||||
id,
|
||||
until,
|
||||
ctx.user.id,
|
||||
message
|
||||
);
|
||||
const user = await Users.setSuspensionStatus(id, until, ctx.user.id, message);
|
||||
if (user.suspended) {
|
||||
ctx.pubsub.publish('userSuspended', user);
|
||||
}
|
||||
};
|
||||
|
||||
const ignoreUser = ({ user }, userToIgnore) => {
|
||||
return UsersService.ignoreUsers(user.id, [userToIgnore.id]);
|
||||
return Users.ignoreUsers(user.id, [userToIgnore.id]);
|
||||
};
|
||||
|
||||
const stopIgnoringUser = ({ user }, userToStopIgnoring) => {
|
||||
return UsersService.stopIgnoringUsers(user.id, [userToStopIgnoring.id]);
|
||||
return Users.stopIgnoringUsers(user.id, [userToStopIgnoring.id]);
|
||||
};
|
||||
|
||||
const changeUsername = async (ctx, id, username) => {
|
||||
const user = await UsersService.changeUsername(id, username, ctx.user.id);
|
||||
const user = await Users.changeUsername(id, username, ctx.user.id);
|
||||
const previousUsername = ctx.user.username;
|
||||
ctx.pubsub.publish('usernameChanged', { previousUsername, user });
|
||||
return user;
|
||||
};
|
||||
|
||||
const setUsername = async (ctx, id, username) => {
|
||||
return UsersService.setUsername(id, username, ctx.user.id);
|
||||
return Users.setUsername(id, username, ctx.user.id);
|
||||
};
|
||||
|
||||
const setRole = (ctx, id, role) => {
|
||||
return UsersService.setRole(id, role);
|
||||
return Users.setRole(id, role);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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: {
|
||||
const actionDecrTransformer = ({ item_id, action_type, group_id }) => {
|
||||
const update = {
|
||||
$inc: {
|
||||
[`action_counts.${action_type.toLowerCase()}`]: -1,
|
||||
[`action_counts.${action_type.toLowerCase()}_${group_id.toLowerCase()}`]: -1,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (group_id) {
|
||||
// If the action had a groupID, also decrement that key.
|
||||
update.$inc[
|
||||
`action_counts.${action_type.toLowerCase()}_${group_id.toLowerCase()}`
|
||||
] = -1;
|
||||
}
|
||||
|
||||
return {
|
||||
query: { id: item_id },
|
||||
update,
|
||||
};
|
||||
};
|
||||
|
||||
// delUser will delete a given user with the specified id.
|
||||
const delUser = async (ctx, id) => {
|
||||
@@ -102,7 +107,7 @@ const delUser = async (ctx, id) => {
|
||||
updateBatchSize: 10000,
|
||||
});
|
||||
|
||||
// Remove all actions against comments.
|
||||
// Remove all actions against this users comments.
|
||||
await transformSingleWithCursor(
|
||||
Action.collection.find({ user_id: user.id, item_type: 'COMMENTS' }),
|
||||
actionDecrTransformer,
|
||||
@@ -121,38 +126,86 @@ const delUser = async (ctx, id) => {
|
||||
.setOptions({ multi: true })
|
||||
.remove();
|
||||
|
||||
// Removes all the user's reply counts on each of the comments that they
|
||||
// have commented on.
|
||||
// Remove the user from all other user's ignore lists.
|
||||
await User.update(
|
||||
{ ignoresUsers: user.id },
|
||||
{
|
||||
$pull: { ignoresUsers: user.id },
|
||||
},
|
||||
{ multi: true }
|
||||
);
|
||||
|
||||
// For each comment that the user has authored, purge the comment data from it
|
||||
// and unset their id from those comments.
|
||||
await transformSingleWithCursor(
|
||||
Comment.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,
|
||||
},
|
||||
Comment.collection.find({ author_id: user.id }),
|
||||
({
|
||||
id,
|
||||
asset_id,
|
||||
status,
|
||||
parent_id,
|
||||
reply_count,
|
||||
created_at,
|
||||
updated_at,
|
||||
}) => ({
|
||||
query: { id },
|
||||
replace: {
|
||||
id,
|
||||
body: null,
|
||||
body_history: [],
|
||||
asset_id,
|
||||
author_id: null,
|
||||
status_history: [],
|
||||
status,
|
||||
parent_id,
|
||||
reply_count,
|
||||
action_counts: {},
|
||||
tags: [],
|
||||
metadata: {},
|
||||
deleted_at: new Date(),
|
||||
created_at,
|
||||
updated_at,
|
||||
},
|
||||
}),
|
||||
Comment
|
||||
);
|
||||
|
||||
// Remove all the user's comments.
|
||||
await Comment.where({ author_id: user.id })
|
||||
.setOptions({ multi: true })
|
||||
.remove();
|
||||
|
||||
// Remove the user.
|
||||
await user.remove();
|
||||
};
|
||||
|
||||
const changeUserPassword = async (ctx, oldPassword, newPassword) => {
|
||||
const {
|
||||
user,
|
||||
loaders: { Settings },
|
||||
connectors: { services: { I18n } },
|
||||
} = ctx;
|
||||
|
||||
// Verify the old password.
|
||||
const validPassword = await user.verifyPassword(oldPassword);
|
||||
if (!validPassword) {
|
||||
throw new ErrPasswordIncorrect();
|
||||
}
|
||||
|
||||
// Change the users password now.
|
||||
await Users.changePassword(user.id, newPassword);
|
||||
|
||||
// Get some context for the email to be sent.
|
||||
const { organizationName, organizationContactEmail } = await Settings.select(
|
||||
'organizationName',
|
||||
'organizationContactEmail'
|
||||
);
|
||||
|
||||
// Send the password change email.
|
||||
await Users.sendEmail(user, {
|
||||
template: 'plain',
|
||||
locals: {
|
||||
body: I18n.t('email.password_change.body', organizationContactEmail),
|
||||
},
|
||||
subject: I18n.t('email.password_change.subject', organizationName),
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = ctx => {
|
||||
let mutators = {
|
||||
User: {
|
||||
@@ -165,6 +218,7 @@ module.exports = ctx => {
|
||||
setUsername: () => Promise.reject(new ErrNotAuthorized()),
|
||||
stopIgnoringUser: () => Promise.reject(new ErrNotAuthorized()),
|
||||
del: () => Promise.reject(new ErrNotAuthorized()),
|
||||
changePassword: () => Promise.reject(new ErrNotAuthorized()),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -201,9 +255,14 @@ module.exports = ctx => {
|
||||
setUserSuspensionStatus(ctx, id, until, message);
|
||||
}
|
||||
|
||||
if (ctx.user.can(DELETE_USER)) {
|
||||
if (ctx.user.can(DELETE_OTHER_USER)) {
|
||||
mutators.User.del = id => delUser(ctx, id);
|
||||
}
|
||||
|
||||
if (ctx.user.can(CHANGE_PASSWORD)) {
|
||||
mutators.User.changePassword = ({ oldPassword, newPassword }) =>
|
||||
changeUserPassword(ctx, oldPassword, newPassword);
|
||||
}
|
||||
}
|
||||
|
||||
return mutators;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { decorateWithTags } = require('./util');
|
||||
const { decorateWithTags, getRequestedFields } = require('./util');
|
||||
|
||||
const Asset = {
|
||||
async comment({ id }, { id: commentId }, { loaders: { Comments } }) {
|
||||
@@ -64,14 +64,17 @@ const Asset = {
|
||||
|
||||
return Comments.countByAssetID.load(id);
|
||||
},
|
||||
async settings({ settings = null }, _, { loaders: { Settings } }) {
|
||||
async settings({ settings = null }, _, { loaders: { Settings } }, info) {
|
||||
// Get the fields we want from the settings.
|
||||
const fields = getRequestedFields(info);
|
||||
|
||||
// Load the global settings, and merge them into the asset specific settings
|
||||
// if we have some.
|
||||
let globalSettings = await Settings.load();
|
||||
let globalSettings = await Settings.select(...fields);
|
||||
if (settings !== null) {
|
||||
settings = Object.assign({}, globalSettings.toObject(), settings);
|
||||
settings = Object.assign({}, globalSettings, settings);
|
||||
} else {
|
||||
settings = globalSettings.toObject();
|
||||
settings = globalSettings;
|
||||
}
|
||||
|
||||
return settings;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
const { URL } = require('url');
|
||||
const { property } = require('lodash');
|
||||
const {
|
||||
SEARCH_ACTIONS,
|
||||
SEARCH_COMMENT_STATUS_HISTORY,
|
||||
VIEW_BODY_HISTORY,
|
||||
VIEW_COMMENT_DELETED_AT,
|
||||
} = require('../../perms/constants');
|
||||
const {
|
||||
decorateWithTags,
|
||||
@@ -22,7 +24,9 @@ const Comment = {
|
||||
return Comments.get.load(parent_id);
|
||||
},
|
||||
user({ author_id }, _, { loaders: { Users } }) {
|
||||
return Users.getByID.load(author_id);
|
||||
if (author_id) {
|
||||
return Users.getByID.load(author_id);
|
||||
}
|
||||
},
|
||||
replies({ id, asset_id, reply_count }, { query }, { loaders: { Comments } }) {
|
||||
// Don't bother looking up replies if there aren't any there!
|
||||
@@ -54,15 +58,29 @@ const Comment = {
|
||||
return Assets.getByID.load(asset_id);
|
||||
},
|
||||
async editing(comment, _, { loaders: { Settings } }) {
|
||||
const settings = await Settings.load();
|
||||
const editableUntil = new Date(
|
||||
Number(new Date(comment.created_at)) + settings.editCommentWindowLength
|
||||
const { editCommentWindowLength } = await Settings.select(
|
||||
'editCommentWindowLength'
|
||||
);
|
||||
|
||||
const editableUntil = new Date(
|
||||
Number(new Date(comment.created_at)) + editCommentWindowLength
|
||||
);
|
||||
|
||||
return {
|
||||
edited: comment.edited,
|
||||
editableUntil: editableUntil,
|
||||
};
|
||||
},
|
||||
async url(comment, args, { loaders: { Assets } }) {
|
||||
const asset = await Assets.getByID.load(comment.asset_id);
|
||||
if (!asset) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const assetURL = new URL(asset.url);
|
||||
assetURL.searchParams.set('commentId', comment.id);
|
||||
return assetURL.href;
|
||||
},
|
||||
};
|
||||
|
||||
// Decorate the Comment type resolver with a tags field.
|
||||
@@ -72,6 +90,7 @@ decorateWithTags(Comment);
|
||||
decorateWithPermissionCheck(Comment, {
|
||||
actions: [SEARCH_ACTIONS],
|
||||
status_history: [SEARCH_COMMENT_STATUS_HISTORY],
|
||||
deleted_at: [VIEW_COMMENT_DELETED_AT],
|
||||
});
|
||||
|
||||
// Protect privileged fields.
|
||||
|
||||
@@ -139,6 +139,9 @@ const RootMutation = {
|
||||
delUser: async (_, { id }, { mutators: { User } }) => {
|
||||
await User.del(id);
|
||||
},
|
||||
changePassword: async (_, { input }, { mutators: { User } }) => {
|
||||
await User.changePassword(input);
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = RootMutation;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { decorateWithPermissionCheck } = require('./util');
|
||||
const { decorateWithPermissionCheck, getRequestedFields } = require('./util');
|
||||
const {
|
||||
SEARCH_ASSETS,
|
||||
SEARCH_OTHERS_COMMENTS,
|
||||
@@ -16,8 +16,12 @@ const RootQuery = {
|
||||
|
||||
return Assets.getByURL(query.url);
|
||||
},
|
||||
settings(_, args, { loaders: { Settings } }) {
|
||||
return Settings.load();
|
||||
settings(_, args, { loaders: { Settings } }, info) {
|
||||
// Get the fields we want from the settings.
|
||||
const fields = getRequestedFields(info);
|
||||
|
||||
// Load only the requested fields.
|
||||
return Settings.select(...fields);
|
||||
},
|
||||
|
||||
// This endpoint is used for loading moderation queues, so hide it in the
|
||||
|
||||
@@ -2,7 +2,8 @@ const {
|
||||
ADD_COMMENT_TAG,
|
||||
SEARCH_OTHER_USERS,
|
||||
} = require('../../perms/constants');
|
||||
const { property, isBoolean } = require('lodash');
|
||||
const { property, isBoolean, pull } = require('lodash');
|
||||
const graphqlFields = require('graphql-fields');
|
||||
|
||||
/**
|
||||
* getResolver will get the resolver from the typeResolver or apply the default
|
||||
@@ -207,7 +208,11 @@ const decorateWithTags = (
|
||||
};
|
||||
};
|
||||
|
||||
const getRequestedFields = info =>
|
||||
pull(Object.keys(graphqlFields(info)), '__typename');
|
||||
|
||||
module.exports = {
|
||||
getRequestedFields,
|
||||
decorateUserField,
|
||||
decorateWithTags,
|
||||
decorateWithPermissionCheck,
|
||||
|
||||
+42
-2
@@ -418,6 +418,9 @@ input CommentsQuery {
|
||||
|
||||
# Exclude comments ignored by the requesting user
|
||||
excludeIgnored: Boolean
|
||||
|
||||
# excludeDeleted when true will exclude deleted comments from the response.
|
||||
excludeDeleted: Boolean = false
|
||||
}
|
||||
|
||||
input RepliesQuery {
|
||||
@@ -434,6 +437,9 @@ input RepliesQuery {
|
||||
|
||||
# Exclude comments ignored by the requesting user
|
||||
excludeIgnored: Boolean
|
||||
|
||||
# excludeDeleted when true will exclude deleted comments from the response.
|
||||
excludeDeleted: Boolean = false
|
||||
}
|
||||
|
||||
# CommentCountQuery allows the ability to query comment counts by specific
|
||||
@@ -463,6 +469,9 @@ input CommentCountQuery {
|
||||
|
||||
# Filter by a specific tag name.
|
||||
tags: [String!]
|
||||
|
||||
# excludeDeleted when true will exclude deleted comments from the count.
|
||||
excludeDeleted: Boolean = false
|
||||
}
|
||||
|
||||
# UserCountQuery allows the ability to query user counts by specific
|
||||
@@ -503,7 +512,7 @@ type Comment {
|
||||
id: ID!
|
||||
|
||||
# The actual comment data.
|
||||
body: String!
|
||||
body: String
|
||||
|
||||
# The body history of the comment. Requires the `ADMIN` or `MODERATOR` role or
|
||||
# the author.
|
||||
@@ -519,7 +528,7 @@ type Comment {
|
||||
replies(query: RepliesQuery = {}): CommentConnection!
|
||||
|
||||
# replyCount is the number of replies with a depth of 1. Only direct replies
|
||||
# to this comment are counted.
|
||||
# to this comment are counted. Deleted comments are included in this count.
|
||||
replyCount: Int
|
||||
|
||||
# Actions completed on the parent. Requires the `ADMIN` role.
|
||||
@@ -537,6 +546,9 @@ type Comment {
|
||||
# The status history of the comment. Requires the `ADMIN` or `MODERATOR` role.
|
||||
status_history: [CommentStatusHistory!]
|
||||
|
||||
# The date that the comment was deleted at if it was.
|
||||
deleted_at: Date
|
||||
|
||||
# The time when the comment was created
|
||||
created_at: Date!
|
||||
|
||||
@@ -548,6 +560,9 @@ type Comment {
|
||||
|
||||
# Indicates if it has a parent
|
||||
hasParent: Boolean
|
||||
|
||||
# url is the permalink to this particular Comment on the Asset.
|
||||
url: String
|
||||
}
|
||||
|
||||
# CommentConnection represents a paginable subset of a comment list.
|
||||
@@ -835,6 +850,9 @@ type Settings {
|
||||
# organizationName is the name of the organization.
|
||||
organizationName: String
|
||||
|
||||
# organizationContactEmail is the email of the organization.
|
||||
organizationContactEmail: String
|
||||
|
||||
# wordlist will return a given list of words.
|
||||
wordlist: Wordlist
|
||||
|
||||
@@ -1291,6 +1309,9 @@ input UpdateSettingsInput {
|
||||
# organizationName is the name of the organization.
|
||||
organizationName: String
|
||||
|
||||
# organizationContactEmail is the email of the organization.
|
||||
organizationContactEmail: String
|
||||
|
||||
# editCommentWindowLength is the length of time (in milliseconds) after a
|
||||
# comment is posted that it can still be edited by the author.
|
||||
editCommentWindowLength: Int
|
||||
@@ -1433,6 +1454,21 @@ type DelUserResponse implements Response {
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
input ChangePasswordInput {
|
||||
# oldPassword is the previous password set on the account. An incorrect
|
||||
# password here will result in an unauthorized error being thrown.
|
||||
oldPassword: String!
|
||||
|
||||
# newPassword is the password we're changing it to.
|
||||
newPassword: String!
|
||||
}
|
||||
|
||||
type ChangePasswordResponse implements Response {
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
# All mutations for the application are defined on this object.
|
||||
type RootMutation {
|
||||
|
||||
@@ -1533,6 +1569,10 @@ type RootMutation {
|
||||
|
||||
# delUser will delete the user with the specified id.
|
||||
delUser(id: ID!): DelUserResponse
|
||||
|
||||
# changePassword allows the current user to change their password that have an
|
||||
# associated local user account.
|
||||
changePassword(input: ChangePasswordInput!): ChangePasswordResponse
|
||||
}
|
||||
|
||||
type UsernameChangedPayload {
|
||||
|
||||
+5
-37
@@ -1,40 +1,8 @@
|
||||
const path = require('path');
|
||||
const { pluginsPath } = require('./plugins');
|
||||
|
||||
const buildTargets = ['coral-admin'];
|
||||
|
||||
const buildEmbeds = ['stream'];
|
||||
|
||||
// jest.config.js
|
||||
module.exports = {
|
||||
testMatch: ['**/client/**/__tests__/**/*.js?(x)'],
|
||||
setupTestFrameworkScriptFile: '<rootDir>/test/client/setupJest.js',
|
||||
modulePaths: [
|
||||
'<rootDir>/plugins',
|
||||
'<rootDir>/client',
|
||||
...buildTargets.map(target =>
|
||||
path.join('<rootDir>', 'client', target, 'src')
|
||||
),
|
||||
...buildEmbeds.map(embed =>
|
||||
path.join('<rootDir>', 'client', `coral-embed-${embed}`, 'src')
|
||||
),
|
||||
],
|
||||
moduleFileExtensions: ['js', 'jsx', 'json', 'yaml', 'yml'],
|
||||
moduleDirectories: ['node_modules'],
|
||||
|
||||
transform: {
|
||||
'^.+\\.jsx?$': 'babel-jest',
|
||||
'\\.ya?ml$': '<rootDir>/test/client/yamlTransformer.js',
|
||||
},
|
||||
|
||||
projects: ['<rootDir>', '<rootDir>/client'],
|
||||
testPathIgnorePatterns: ['client'],
|
||||
setupTestFrameworkScriptFile: '<rootDir>/test/setupJest.js',
|
||||
testResultsProcessor: process.env.JEST_REPORTER,
|
||||
|
||||
moduleNameMapper: {
|
||||
'^plugin-api\\/(.*)$': '<rootDir>/plugin-api/$1',
|
||||
'^plugins\\/(.*)$': '<rootDir>/plugins/$1',
|
||||
'^pluginsConfig$': pluginsPath,
|
||||
|
||||
'\\.(scss|css|less)$': 'identity-obj-proxy',
|
||||
'\\.(gif|ttf|eot|svg)$': '<rootDir>/test/client/fileMock.js',
|
||||
},
|
||||
testEnvironment: 'node',
|
||||
modulePaths: ['<rootDir>'],
|
||||
};
|
||||
|
||||
+3
-2
@@ -438,8 +438,8 @@ ar:
|
||||
reports: "Reports"
|
||||
all: "All"
|
||||
rejected: "Rejected"
|
||||
account_history: "Account History"
|
||||
account_history:
|
||||
user_history: "User History"
|
||||
user_history:
|
||||
user_banned: "User banned"
|
||||
ban_removed: "Ban removed"
|
||||
username_status: "Username {0}"
|
||||
@@ -466,6 +466,7 @@ ar:
|
||||
username: "Username"
|
||||
password: "Password"
|
||||
confirm_password: "Confirm Password"
|
||||
organization_contact_email: "Organization Contact Email"
|
||||
save: "Save"
|
||||
permitted_domains:
|
||||
title: "Permitted domains"
|
||||
|
||||
+3
-2
@@ -431,8 +431,8 @@ da:
|
||||
reports: "Rapporter"
|
||||
all: "Alle"
|
||||
rejected: "Afvist"
|
||||
account_history: "Konto historik"
|
||||
account_history:
|
||||
user_history: "Konto historik"
|
||||
user_history:
|
||||
user_banned: "Bruger bannet"
|
||||
ban_removed: "Ban fjernet"
|
||||
username_status: "Brugernavn {0}"
|
||||
@@ -459,6 +459,7 @@ da:
|
||||
username: "Brugernavn"
|
||||
password: "Kodeord"
|
||||
confirm_password: "Bekræft kodeord"
|
||||
organization_contact_email: "Organization Contact Email"
|
||||
save: "Gem"
|
||||
permitted_domains:
|
||||
title: "Tilladte domæner"
|
||||
|
||||
+3
-2
@@ -430,8 +430,8 @@ de:
|
||||
reports: "Meldungen"
|
||||
all: "Alle"
|
||||
rejected: "Abgelehnte"
|
||||
account_history: "Konto-Verlauf"
|
||||
account_history:
|
||||
user_history: "Konto-Verlauf"
|
||||
user_history:
|
||||
user_banned: "User banned"
|
||||
ban_removed: "Ban removed"
|
||||
username_status: "Username {0}"
|
||||
@@ -458,6 +458,7 @@ de:
|
||||
username: "Nutzername"
|
||||
password: "Passwort"
|
||||
confirm_password: "Passwort bestätigen"
|
||||
organization_contact_email: "Organization Contact Email"
|
||||
save: "Speichern"
|
||||
permitted_domains:
|
||||
title: "Zugelassene Domains"
|
||||
|
||||
+30
-9
@@ -20,11 +20,13 @@ en:
|
||||
bio_offensive: "This bio is offensive"
|
||||
cancel: "Cancel"
|
||||
confirm_email:
|
||||
click_to_confirm: "Click below to confirm your email address"
|
||||
email_confirmation: "Email Confirmation"
|
||||
click_to_confirm: "Click below to confirm your email address."
|
||||
confirm: "Confirm"
|
||||
password_reset:
|
||||
mail_sent: 'If you have a registered account, a password reset link was sent to that email'
|
||||
set_new_password: "Change Your Password"
|
||||
change_password_help: "Please enter a new password to use to login. Make it secure!"
|
||||
new_password: "New Password"
|
||||
new_password_help: "Password must be at least 8 characters"
|
||||
confirm_new_password: "Confirm New Password"
|
||||
@@ -121,9 +123,10 @@ en:
|
||||
custom_css_url: "Custom CSS URL"
|
||||
custom_css_url_desc: "URL of a CSS stylesheet that will override default Embed Stream styles. Can be internal or external."
|
||||
days: Days
|
||||
description: "As an admin, you can customize the settings for the comment stream for this story:"
|
||||
description: "Change the comment settings on this story."
|
||||
domain_list_text: "Enter the domains you would like to permit for Talk e.g. your local staging and production environments (ex. localhost:3000 staging.domain.com domain.com)."
|
||||
domain_list_title: "Permitted Domains"
|
||||
edit_info: "Edit Info"
|
||||
edit_comment_timeframe_heading: "Edit Comment Timeframe"
|
||||
edit_comment_timeframe_text_pre: "Commenters will have"
|
||||
edit_comment_timeframe_text_post: "seconds to edit their comments."
|
||||
@@ -149,6 +152,7 @@ en:
|
||||
open_stream_configuration: "This comment stream is currently open. By closing this comment stream no new comments may be submitted and all previous comments will still be displayed."
|
||||
require_email_verification: "Require Email Verification"
|
||||
require_email_verification_text: "New Users must verify their email before commenting"
|
||||
save: Save
|
||||
save_changes: "Save Changes"
|
||||
shortcuts: Shortcuts
|
||||
sign_out: "Sign Out"
|
||||
@@ -158,6 +162,12 @@ en:
|
||||
suspect_word_title: "Suspect words list"
|
||||
suspect_word_text: "Comments which contain these words or phrases (not case-sensitive) will be highlighted in the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list."
|
||||
tech_settings: "Tech Settings"
|
||||
organization_information: "Organization information"
|
||||
organization_info_copy: "We use this information in email notifications generated by Talk. This connects the messages to your organization, and provides a way for users to contact you if they have an issue with their account."
|
||||
organization_info_copy_2: "We recommend creating a generic email account (eg. community@yournewsroom.com) for this purpose. This means it can remain consistent over time, and doesn't expose a name that users could target if their account were blocked."
|
||||
organization_details: "Organization Details"
|
||||
organization_name: "Organization Name"
|
||||
organization_contact_email: "Organization Contact Email"
|
||||
title: "Configure Comment Stream"
|
||||
weeks: Weeks
|
||||
wordlist: "Banned Words"
|
||||
@@ -208,9 +218,14 @@ en:
|
||||
we_received_a_request: "We received a request to reset your password. If you did not request this change, you can ignore this email."
|
||||
if_you_did: "If you did,"
|
||||
please_click: "please click here to reset password"
|
||||
subject: "Password reset"
|
||||
password_change:
|
||||
subject: "{0} password change"
|
||||
body: "The password on your account has been changed.\n\nIf you did not request this change, please contact us at {0}."
|
||||
embedlink:
|
||||
copy: "Copy to Clipboard"
|
||||
error:
|
||||
PASSWORD_INCORRECT: "Your current password was entered incorrectly"
|
||||
COMMENT_PARENT_NOT_VISIBLE: "The comment that you're replying to has been removed or doesn't exist."
|
||||
EMAIL_VERIFICATION_TOKEN_INVALID: "Email verification token is invalid."
|
||||
EMAIL_ALREADY_VERIFIED: "Email address already verified."
|
||||
@@ -223,7 +238,7 @@ en:
|
||||
RATE_LIMIT_EXCEEDED: "Rate limit exceeded"
|
||||
USERNAME_IN_USE: "Username already in use"
|
||||
USERNAME_REQUIRED: "Must input a username"
|
||||
EMAIL_NOT_VERIFIED: "E-mail address not verified"
|
||||
EMAIL_NOT_VERIFIED: "Email address not verified"
|
||||
EDIT_WINDOW_ENDED: "You can no longer edit this comment. The time window to do so has expired."
|
||||
EDIT_USERNAME_NOT_AUTHORIZED: "You do not have permission to update your username."
|
||||
SAME_USERNAME_PROVIDED: "You must submit a different username."
|
||||
@@ -236,12 +251,15 @@ en:
|
||||
ALREADY_EXISTS: "Resource already exists"
|
||||
INVALID_ASSET_URL: "Assert URL is invalid"
|
||||
CANNOT_IGNORE_STAFF: "Cannot ignore Staff members."
|
||||
email: "Not a valid E-Mail"
|
||||
INCORRECT_PASSWORD: "Incorrect Password"
|
||||
email: "Please enter a valid email."
|
||||
DELETION_NOT_SCHEDULED: "Deletion was not scheduled"
|
||||
confirm_password: "Passwords don't match. Please check again"
|
||||
network_error: "Failed to connect to server. Check your internet connection and try again."
|
||||
email_not_verified: "E-mail address {0} not verified."
|
||||
email_password: "E-mail and/or password combination incorrect."
|
||||
email_not_verified: "Email address {0} not verified."
|
||||
email_password: "Email and/or password combination incorrect."
|
||||
organization_name: "Organization name must only contain letters or numbers."
|
||||
organization_contact_email: "Organization email is not valid."
|
||||
password: "Password must be at least 8 characters"
|
||||
username: "Usernames can contain letters numbers and _ only"
|
||||
unexpected: "Unexpected error occurred. Sorry!"
|
||||
@@ -256,6 +274,7 @@ en:
|
||||
comment: comment
|
||||
comment_is_ignored: "This comment is hidden because you ignored this user."
|
||||
comment_is_rejected: "You have rejected this comment."
|
||||
comment_is_deleted: "This comment was deleted."
|
||||
comment_is_hidden: "This comment is not available."
|
||||
comments: comments
|
||||
configure_stream: "Configure"
|
||||
@@ -344,6 +363,7 @@ en:
|
||||
sort: "Sort"
|
||||
show_shortcuts: "Show Shortcuts"
|
||||
singleview: "Zen mode"
|
||||
system_withheld: "System Withheld"
|
||||
thismenu: "Open this menu"
|
||||
jump_to_queue: "Jump to specific queue"
|
||||
thousand: k
|
||||
@@ -426,7 +446,7 @@ en:
|
||||
title_reject: "We noticed you rejected a username"
|
||||
suspend_user: "Suspend User"
|
||||
yes_suspend: "Yes suspend"
|
||||
email_message_reject: "Another member of the community recently flagged your username for review. Because of its content your user was rejected. This means you can no longer comment, like, or flag content until you rewrite your username. Please e-mail us if you have any questions or concerns."
|
||||
email_message_reject: "Another member of the community recently flagged your username for review. Because of its content your user was rejected. This means you can no longer comment, like, or flag content until you rewrite your username. Please email us if you have any questions or concerns."
|
||||
write_message: "Write a message"
|
||||
send: Send
|
||||
thank_you: "We value your safety and feedback. A moderator will review your report."
|
||||
@@ -446,8 +466,8 @@ en:
|
||||
reports: "Reports"
|
||||
all: "All"
|
||||
rejected: "Rejected"
|
||||
account_history: "Account History"
|
||||
account_history:
|
||||
user_history: "User History"
|
||||
user_history:
|
||||
user_banned: "User banned"
|
||||
ban_removed: "Ban removed"
|
||||
username_status: "Username {0}"
|
||||
@@ -474,6 +494,7 @@ en:
|
||||
username: "Username"
|
||||
password: "Password"
|
||||
confirm_password: "Confirm Password"
|
||||
organization_contact_email: "Organization Contact Email"
|
||||
save: "Save"
|
||||
permitted_domains:
|
||||
title: "Permitted domains"
|
||||
|
||||
+76
-63
@@ -16,26 +16,27 @@ es:
|
||||
send: "Enviar"
|
||||
notify_ban_headline: "Notificar al usuario de la suspensión"
|
||||
notify_ban_description: "Esto notificará al usuario por email que fueron suspendidos de la comunidad"
|
||||
email_message_ban: "Dear {0},\n\nSomeone with access to your account has violated our community guidelines. As a result, your account has been banned. You will no longer be able to comment, like or report comments. if you think this has been done in error, please contact our community team."
|
||||
email_message_ban: "Estimado/a {0},\n\nUsted o alguien con acceso a su cuenta a violado los lineamientos de nuestra comunidad. Como consecuencia de esto, su cuenta fue bloqueada. No podrá realizar ni reportar más comentarios. Si usted piensa que esto ha sido un error, por favor contáctese con nosotros."
|
||||
bio_offensive: "Esta biografia es ofensiva"
|
||||
cancel: Cancelar
|
||||
confirm_email:
|
||||
click_to_confirm: "Click below to confirm your email address"
|
||||
confirm: "Confirm"
|
||||
click_to_confirm: "Haga click debajo para confirmar su dirección de email"
|
||||
confirm: "Confirmar"
|
||||
password_reset:
|
||||
set_new_password: "Change Your Password"
|
||||
new_password: "New Password"
|
||||
new_password_help: "Password must be at least 8 characters"
|
||||
confirm_new_password: "Confirm New Password"
|
||||
change_password: "Change Password"
|
||||
characters_remaining: "carácteres restantes"
|
||||
mail_sent: 'Si tiene una cuenta registrada, un enlace para resetear su clave ha sido enviado a esa dirección de correo.'
|
||||
set_new_password: "Cambie su contraseña"
|
||||
new_password: "Nueva contraseña"
|
||||
new_password_help: "La contraseña debe tener al menos 8 caracteres"
|
||||
confirm_new_password: "Confirme su nueva contraseña"
|
||||
change_password: "Cambio de contraseña"
|
||||
characters_remaining: "caracteres restantes"
|
||||
comment:
|
||||
anon: Anónimo
|
||||
undo_reject: "Undo"
|
||||
undo_reject: "Deshacer"
|
||||
ban_user: "Usuario Suspendido"
|
||||
comment: "Publicar un comentario"
|
||||
edited: Editado
|
||||
flagged: reportado
|
||||
flagged: Reportado
|
||||
view_context: "Ver contexto"
|
||||
comment_box:
|
||||
post: "Publicar"
|
||||
@@ -62,16 +63,16 @@ es:
|
||||
reactions: 'reacciones'
|
||||
story: 'Artículo'
|
||||
flagged_usernames:
|
||||
notify_approved: '{0} approved username {1}'
|
||||
notify_rejected: '{0} rejected username {1}'
|
||||
notify_flagged: '{0} reported username {1}'
|
||||
notify_changed: 'user {0} changed their username to {1}'
|
||||
notify_approved: '{0} ha aprobado el nombre de usuario {1}'
|
||||
notify_rejected: '{0} ha rechazado el nombre de usuario {1}'
|
||||
notify_flagged: '{0} ha reportado el nombre de usuario {1}'
|
||||
notify_changed: 'El usuario {0} ha modificado su nombre de usuario por {1}'
|
||||
community:
|
||||
account_creation_date: "Fecha de creación de la cuenta"
|
||||
active: Activa
|
||||
admin: Administrator
|
||||
ads_marketing: "This looks like an ad/marketing"
|
||||
are_you_sure: "¿Estas segura que quieres suspender a {0}?"
|
||||
ads_marketing: "Esto parece ser un ad/marketing"
|
||||
are_you_sure: "¿Estás segura que quieres suspender a {0}?"
|
||||
ban_user: "¿Quieres suspender al Usuario?"
|
||||
banned: Suspendido
|
||||
banned_user: "Usuario Suspendido"
|
||||
@@ -120,9 +121,10 @@ es:
|
||||
custom_css_url: "URL CSS a medida"
|
||||
custom_css_url_desc: "URL de una hoja de estilo que va a sobrescribir los estilos por defecto del hilo de comentarios. Puede ser interna o externa."
|
||||
days: Días
|
||||
description: "Como Administrador/a puedes modificar la configuración de los comentarios en este artículo"
|
||||
description: "Modificar la configuración de los comentarios en este artículo."
|
||||
domain_list_text: "Agrega dominios permitidos a Talk, por ejemplo tu localhost, staging y ambientes de producción (ej. localhost:3000, staging.domain.com, domain.com)."
|
||||
domain_list_title: "Dominios Permitidos"
|
||||
edit_info: "Editar Información"
|
||||
edit_comment_timeframe_heading: "Periodo de Tiempo para Edición del Comentario"
|
||||
edit_comment_timeframe_text_pre: "Los comentaristas tendrán"
|
||||
edit_comment_timeframe_text_post: "segundos para editar sus comentarios."
|
||||
@@ -148,6 +150,7 @@ es:
|
||||
open_stream_configuration: "Este hilo de comentarios está abierto. Al cerrarlo no se podrán publicar nuevos comentarios pero todos los comentarios anteriores aún serán mostrados."
|
||||
require_email_verification: "Necesita confirmación su correo"
|
||||
require_email_verification_text: "Nuevos usuarios deben confirmar sus correos antes de comentar"
|
||||
save: Guardar
|
||||
save_changes: "Guardar Cambios"
|
||||
shortcuts: Atajos
|
||||
sign_out: "Desconectar"
|
||||
@@ -157,6 +160,12 @@ es:
|
||||
suspect_word_title: "Lista de palabras sospechosas"
|
||||
suspect_word_text: "Comentarios que contengan estas palabras o frases, considerando mayusculas y minúsculas, serán automáticamente destacadas en los comentarios publicados. Escribir una palabra y apretar Enter o Tabulador para agregarla. O pegar una lista de palabras separadas por coma."
|
||||
tech_settings: "Configuración Técnica"
|
||||
organization_information: "Información de la Organización"
|
||||
organization_details: "Detalles de la Organización"
|
||||
organization_info_copy: "Nosotros usamos esta información en las notificaciones de email generadas por Talk. Esto conecta los mensajes de tu organización, y provee una forma para que los usuarios se comuniquen si tienen un inconveniente con su cuenta."
|
||||
organization_info_copy_2: "Recomendamos crear un email genérico (ej: community@yournewsroom.com) for this purpose. Esto significa que puede permanecer consistente con el tiempo y no expone un nombre que los usuarios puedan atacar si su cuenta fue bloqueada."
|
||||
organization_name: "Nombre de la Organización"
|
||||
organization_contact_email: "Email de la Organización"
|
||||
title: "Configurar los comentarios"
|
||||
weeks: Semanas
|
||||
wordlist: "Palabras Suspendidas"
|
||||
@@ -193,10 +202,10 @@ es:
|
||||
minutes_plural: "minutos"
|
||||
email:
|
||||
suspended:
|
||||
subject: "Your account has been suspended"
|
||||
subject: "Su cuenta ha sido suspendida"
|
||||
banned:
|
||||
subject: "Your account has been banned"
|
||||
body: "In accordance with The Coral Project’s community guidelines, your account has been banned. You are now longer allowed to comment, flag or engage with our community."
|
||||
subject: "Su cuenta ha sido bloqueada"
|
||||
body: "De acuerdo con las guías de comunidad de The Coral Project, su cuenta a sido bloqueada. No podrá hacer comentarios, reportar o entrar en contacto con nuestra comunidad."
|
||||
confirm:
|
||||
has_been_requested: "Un correo de confirmación ha sido pedido para la siguiente cuenta:"
|
||||
to_confirm: "Para confirmar la cuenta, por favor visitar el siguiente enlace:"
|
||||
@@ -207,12 +216,13 @@ es:
|
||||
we_received_a_request: "Recibimos un pedido para resetear su contraseña. Si no ha pedido ese cambio, por favor ignorar el correo. "
|
||||
if_you_did: "Si lo hizo,"
|
||||
please_click: "por favor cliquea aqui para resetear la contraseña."
|
||||
subject: "Recuperar contraseña"
|
||||
embedlink:
|
||||
copy: "Copiar al portapapeles"
|
||||
error:
|
||||
COMMENT_PARENT_NOT_VISIBLE: "El comentario a la que estás contestando ha sido eliminado o no existe."
|
||||
EMAIL_VERIFICATION_TOKEN_INVALID: "Email verification token is invalid."
|
||||
PASSWORD_RESET_TOKEN_INVALID: "Your password reset link is invalid."
|
||||
EMAIL_VERIFICATION_TOKEN_INVALID: "El token de verificación de su cuenta de correo es inválido."
|
||||
PASSWORD_RESET_TOKEN_INVALID: "El enlace para resetear su clave es inválido."
|
||||
COMMENT_TOO_SHORT: "Tu comentario debe tener algo escrito"
|
||||
NOT_AUTHORIZED: "Acción no autorizada."
|
||||
NO_SPECIAL_CHARACTERS: "Los nombres pueden contener letras números y _"
|
||||
@@ -221,30 +231,32 @@ es:
|
||||
RATE_LIMIT_EXCEEDED: "Rate limit exceeded"
|
||||
USERNAME_IN_USE: "Este nombre ya está siendo usado."
|
||||
USERNAME_REQUIRED: "Debe ingresar un nombre"
|
||||
EMAIL_NOT_VERIFIED: "E-mail address not verified"
|
||||
EDIT_WINDOW_ENDED:: "No puedes editar este comentario. El tiempo de edición ha expirado."
|
||||
EMAIL_NOT_VERIFIED: "La cuenta de email no ha sido verificada."
|
||||
EDIT_WINDOW_ENDED: "No puedes editar este comentario. El tiempo de edición ha expirado."
|
||||
EDIT_USERNAME_NOT_AUTHORIZED: "No tiene permiso para editar el nombre de usuario."
|
||||
SAME_USERNAME_PROVIDED: "You must submit a different username."
|
||||
SAME_USERNAME_PROVIDED: "Debe proporcinar un nombre de usuario diferente."
|
||||
EMAIL_IN_USE: "Este correo se encuentra en uso"
|
||||
EMAIL_REQUIRED: "Se requiere un correo"
|
||||
LOGIN_MAXIMUM_EXCEEDED: "Ha realizado demasiados intentos fallidos de usar la contraseña. Por favor espere."
|
||||
PASSWORD_REQUIRED: "Debe ingresar la contraseña"
|
||||
COMMENTING_CLOSED: "Los comentarios ya estan cerrados"
|
||||
NOT_FOUND: "Recurso no encontrado"
|
||||
ALREADY_EXISTS: "Resource already exists"
|
||||
ALREADY_EXISTS: "El recurso ya existe"
|
||||
INVALID_ASSET_URL: "La URL del articulo no es valida"
|
||||
CANNOT_IGNORE_STAFF: "Cannot ignore Staff members."
|
||||
CANNOT_IGNORE_STAFF: "No puede ignorar a miembros del Staff."
|
||||
INCORRECT_PASSWORD: "Contraseña Incorrecta"
|
||||
email: "No es un correo válido"
|
||||
confirm_password: "Las contraseñas no coinciden. Inténtelo nuevamente"
|
||||
network_error: "Error al conectar con el servidor. Compruebe su conexión a Internet y vuelva a intentarlo."
|
||||
email_not_verified: "Correo {0} no confirmado."
|
||||
email_password: "Correo y/o contraseña incorrecta."
|
||||
organization_name: "El nombre de la organización debe contener letras y/o números."
|
||||
organization_contact_email: "El email de la organización no es válido."
|
||||
password: "La contraseña debe tener por lo menos 8 caracteres"
|
||||
username: "Los nombres pueden contener letras números y _"
|
||||
required_field: "Este campo es requerido"
|
||||
unexpected: "Lo siento. Ha habido un error no previsto."
|
||||
temporarily_suspended: "Your account is currently suspended. It will be reactivated {0}. Please contact us if you have any questions."
|
||||
unexpected: "Lo siento. Ha ocurrido un error no previsto."
|
||||
temporarily_suspended: "Su cuenta fue suspendida. Será reactivada {0}. Si tiene preguntas, por favor contáctese con nosotros."
|
||||
flag_comment: "Reportar este comentario"
|
||||
flag_reason: "Razón por la que hacer este reporte (Opcional)"
|
||||
flag_username: "Reportar el nombre de usuario"
|
||||
@@ -264,7 +276,7 @@ es:
|
||||
label: "Nuevo Nombre"
|
||||
msg: "Tu cuenta está suspendida porque tu nombre de usuario ha sido considerado no apropiado para el espacio. Para recuperar la cuenta, por favor ingresar un nuevo nombre de usuario. Contáctanos si tienes alguna pregunta."
|
||||
changed_name:
|
||||
msg: "Your username change is under review by our moderation team."
|
||||
msg: "El cambio en su nombre de usuario está siendo revisado por nuestro equipo de moderación."
|
||||
my_comments: "Mis Comentarios"
|
||||
my_profile: "Mi perfil"
|
||||
new_count: "Ver {0} más {1} "
|
||||
@@ -295,9 +307,9 @@ es:
|
||||
comment_spam: "Contiene spam"
|
||||
comment_noagree: "No está de acuerdo"
|
||||
comment_other: "Otra razón"
|
||||
suspect_word: "Suspect Word"
|
||||
banned_word: "Banned Word"
|
||||
body_count: "Body exceeds max length"
|
||||
suspect_word: "Palabra sospechosa"
|
||||
banned_word: "Palabra prohibida"
|
||||
body_count: "El texto exede el límite permitido"
|
||||
trust: "Trust"
|
||||
links: "Link"
|
||||
modqueue:
|
||||
@@ -305,14 +317,14 @@ es:
|
||||
actions: Acciones
|
||||
all: todos
|
||||
all_streams: "Todos los Hilos"
|
||||
notify_edited: '{0} edited comment "{1}"'
|
||||
notify_accepted: '{0} accepted comment "{1}"'
|
||||
notify_rejected: '{0} rejected comment "{1}"'
|
||||
notify_flagged: '{0} flagged comment "{1}"'
|
||||
notify_reset: '{0} reset status of comment "{1}"'
|
||||
notify_edited: '{0} ha editado el comentario "{1}"'
|
||||
notify_accepted: '{0} ha aceptado el comentario "{1}"'
|
||||
notify_rejected: '{0} ha rechazado el comentario "{1}"'
|
||||
notify_flagged: '{0} ha reportado el comentario "{1}"'
|
||||
notify_reset: '{0} ha reseteado el status del comentario "{1}"'
|
||||
approve: "Aprobar"
|
||||
approved: "Aprobado"
|
||||
ban_user: "Ban"
|
||||
ban_user: "Bloquear"
|
||||
billion: B
|
||||
close: Cerrar
|
||||
empty_queue: "¡No hay más comentarios para moderar! Tiempo para un ☕️"
|
||||
@@ -352,7 +364,7 @@ es:
|
||||
no_agree_comment: "No estoy de acuerdo con este comentario"
|
||||
no_like_bio: "No me gusta esta biografia"
|
||||
no_like_username: "No me gusta este nombre de usuario"
|
||||
already_flagged_username: "You have already flagged this username."
|
||||
already_flagged_username: "Usted ya reportó a este usuario."
|
||||
other: Otro
|
||||
permalink: Compartir
|
||||
personal_info: "Este comentario muestra información personal"
|
||||
@@ -428,28 +440,28 @@ es:
|
||||
user_bio: "Bio de Usuario"
|
||||
username_flags: "reportes para este nombre de usuario"
|
||||
user_detail:
|
||||
remove_suspension: "Remove Suspension"
|
||||
suspend: "Suspend User"
|
||||
remove_ban: "Remove Ban"
|
||||
ban: "Ban User"
|
||||
member_since: "Member Since"
|
||||
remove_suspension: "Cancelar suspensión"
|
||||
suspend: "Suspender usuario"
|
||||
remove_ban: "Cancelar bloqueo"
|
||||
ban: "Bloquear usuario"
|
||||
member_since: "Miembro desde"
|
||||
email: "Email"
|
||||
total_comments: "Total Comments"
|
||||
total_comments: "Comentarios totales"
|
||||
reject_rate: "Reject Rate"
|
||||
reports: "Reports"
|
||||
all: "All"
|
||||
rejected: "Rejected"
|
||||
account_history: "Account History"
|
||||
account_history:
|
||||
user_banned: "User banned"
|
||||
ban_removed: "Ban removed"
|
||||
username_status: "Username {0}"
|
||||
suspended: "Suspended, {0}"
|
||||
suspension_removed: "Suspension removed"
|
||||
system: "System"
|
||||
date: "Date"
|
||||
action: "Action"
|
||||
taken_by: "Taken By"
|
||||
reports: "Reportes"
|
||||
all: "Todo"
|
||||
rejected: "Rechazar"
|
||||
user_history: "Historial del usuario"
|
||||
user_history:
|
||||
user_banned: "Usuario bloqueado"
|
||||
ban_removed: "Bloqueo cancelado"
|
||||
username_status: "Nombre de usuario {0}"
|
||||
suspended: "Suspendido, {0}"
|
||||
suspension_removed: "Suspensión cancelada"
|
||||
system: "Sistema"
|
||||
date: "Fecha"
|
||||
action: "Acción"
|
||||
taken_by: "Está en uso"
|
||||
user_impersonating: "Este usuario suplanta a alguien"
|
||||
user_no_comment: "No has dejado aún ningún comentario. ¡Únete a la conversación!"
|
||||
username_offensive: "Este nombre de usuario es ofensivo"
|
||||
@@ -467,6 +479,7 @@ es:
|
||||
username: "Nombre de Usuario"
|
||||
password: "Contraseña"
|
||||
confirm_password: "Confirmar Contraseña"
|
||||
organization_contact_email: "Organización: Email de contacto"
|
||||
save: "Guardar"
|
||||
permitted_domains:
|
||||
title: "Dominios permitidos"
|
||||
@@ -477,5 +490,5 @@ es:
|
||||
launch: "Iniciar Talk"
|
||||
close: "Cerrar este instalador"
|
||||
admin_sidebar:
|
||||
view_options: "View Options"
|
||||
sort_comments: "Sort Comments"
|
||||
view_options: "Ver opciones"
|
||||
sort_comments: "Ordenar comentarios"
|
||||
|
||||
+2
-1
@@ -121,7 +121,7 @@ fr:
|
||||
custom_css_url: "URL CSS personnalisée"
|
||||
custom_css_url_desc: "URL d'une feuille de style CSS qui remplacera les styles par défaut d'intégration des commentaires. Peut être interne ou externe."
|
||||
days: Journées
|
||||
description: "En tant qu'administrateur, vous pouvez personnaliser les paramètres du fil de commentaires pour cet élément."
|
||||
description: "Personnaliser les paramètres du fil de commentaires pour cet élément."
|
||||
domain_list_text: "Entrez les domaines que vous souhaitez autoriser pour Talk, par exemple vos environnements locaux de production et de production (ex : localhost: 3000 staging.domain.com domain.com)."
|
||||
domain_list_title: "Domaines autorisés"
|
||||
edit_comment_timeframe_heading: "Modifier l'horodatage des commentaires"
|
||||
@@ -474,6 +474,7 @@ fr:
|
||||
username: "Nom d'utilisateur"
|
||||
password: "Mot de passe"
|
||||
confirm_password: "Confirmez Le mot de passe"
|
||||
organization_contact_email: "Organization Contact Email"
|
||||
save: "Sauvegarder"
|
||||
permitted_domains:
|
||||
title: "Domaines autorisés"
|
||||
|
||||
+2
-2
@@ -431,8 +431,8 @@ nl_NL:
|
||||
reports: "Rapportages"
|
||||
all: "Alle"
|
||||
rejected: "Afgewezen"
|
||||
account_history: "Accountgeschiedenis"
|
||||
account_history:
|
||||
user_history: "Accountgeschiedenis"
|
||||
user_history:
|
||||
user_banned: "User banned"
|
||||
ban_removed: "Ban removed"
|
||||
username_status: "Username {0}"
|
||||
|
||||
+4
-3
@@ -120,7 +120,7 @@ pt_BR:
|
||||
custom_css_url: "URL para CSS customizado"
|
||||
custom_css_url_desc: "URL de uma folha de estilo CSS para substituir os estilos padrão dos comentários embutidos. Pode ser interno ou externo."
|
||||
days: Dias
|
||||
description: "Como administrador, você pode personalizar as configurações da lista de comentários para esta história:"
|
||||
description: "Personalizar as configurações da lista de comentários para esta história."
|
||||
domain_list_text: "Insira os domínios que você gostaria de permitir para o Talk, como seus ambientes de desenvolvimento, teste ou produção (ej. localhost:3000 staging.domain.com domain.com)."
|
||||
domain_list_title: "Domínios permitidos"
|
||||
edit_comment_timeframe_heading: "Período de tempo para editar comentários"
|
||||
@@ -430,8 +430,8 @@ pt_BR:
|
||||
reports: "Reports"
|
||||
all: "All"
|
||||
rejected: "Rejected"
|
||||
account_history: "Account History"
|
||||
account_history:
|
||||
user_history: "User History"
|
||||
user_history:
|
||||
user_banned: "User banned"
|
||||
ban_removed: "Ban removed"
|
||||
username_status: "Username {0}"
|
||||
@@ -458,6 +458,7 @@ pt_BR:
|
||||
username: "Nome de usuário"
|
||||
password: "Senha"
|
||||
confirm_password: "Confirme a senha"
|
||||
organization_contact_email: "Organization Contact Email"
|
||||
save: "Salvar"
|
||||
permitted_domains:
|
||||
title: "Domínios permitidos"
|
||||
|
||||
+2
-2
@@ -432,8 +432,8 @@ zh_CN:
|
||||
reports: "Reports"
|
||||
all: "All"
|
||||
rejected: "Rejected"
|
||||
account_history: "Account History"
|
||||
account_history:
|
||||
user_history: "User History"
|
||||
user_history:
|
||||
user_banned: "User banned"
|
||||
ban_removed: "Ban removed"
|
||||
username_status: "Username {0}"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user