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 | ||
|
|
87ffa45050 | ||
|
|
4a4357bc4c | ||
|
|
bb11de562f | ||
|
|
5125bb5d2d | ||
|
|
6553096bc7 | ||
|
|
1c87181b17 | ||
|
|
142f463211 | ||
|
|
96733ade4d | ||
|
|
9598e6d426 | ||
|
|
91c4197fd7 | ||
|
|
8042083f6c | ||
|
|
8232ae3e76 | ||
|
|
5178529083 | ||
|
|
10d11c1615 | ||
|
|
4e63cff57f | ||
|
|
0206804bf4 | ||
|
|
68473e5a29 | ||
|
|
d481dc8651 | ||
|
|
322740d1a9 | ||
|
|
86385e0d86 | ||
|
|
270c070075 | ||
|
|
dc0c3b4a67 | ||
|
|
b51bb7486e | ||
|
|
7f9503d6aa | ||
|
|
f81e973315 | ||
|
|
412fd34cc1 | ||
|
|
1dc3b855aa | ||
|
|
aef5076ac1 | ||
|
|
2294286483 | ||
|
|
6b7580acaf | ||
|
|
b2dc178fdb | ||
|
|
dd916a3ef9 | ||
|
|
4eff55a0d1 | ||
|
|
e77243536c | ||
|
|
97a8a6dadd | ||
|
|
05c0998541 | ||
|
|
18570a97ea | ||
|
|
28e264868a | ||
|
|
4dfc90cac4 | ||
|
|
7b4829bfac | ||
|
|
e4cc49cda5 | ||
|
|
2e74f79b79 | ||
|
|
f1b8d71cba | ||
|
|
edfe60b8e0 | ||
|
|
46078117c8 | ||
|
|
879820d340 | ||
|
|
88d9cd193b | ||
|
|
624d768f38 | ||
|
|
254033a294 | ||
|
|
637630b8dd | ||
|
|
9e07618a26 | ||
|
|
46ef214bd3 | ||
|
|
5c2d8c4b62 | ||
|
|
1ceb1b4014 | ||
|
|
80987a1e46 | ||
|
|
e9e70f7e75 | ||
|
|
ffde113a74 | ||
|
|
afa592aa78 | ||
|
|
8a7d4c4fcc | ||
|
|
adb7e97a91 | ||
|
|
43e3cb8f10 | ||
|
|
333ef98378 | ||
|
|
5941bfac7c | ||
|
|
6d4623f8bf | ||
|
|
e358dce594 | ||
|
|
91aa95d3f2 | ||
|
|
3fcad35492 | ||
|
|
4694f986c0 | ||
|
|
841738c34c | ||
|
|
aef096fa4c | ||
|
|
f27c41b83c | ||
|
|
a6518b183f | ||
|
|
93ccec6614 | ||
|
|
691e0f5256 | ||
|
|
071942faca | ||
|
|
9bbcb6a073 | ||
|
|
0e1ace115e | ||
|
|
ae361d5110 | ||
|
|
48bfc2699b | ||
|
|
ad214a8e76 | ||
|
|
cfe5b12abd | ||
|
|
1e2c224204 | ||
|
|
fbe2ec30aa | ||
|
|
d0652d0583 | ||
|
|
2f7f6f4f53 | ||
|
|
f77493afb1 | ||
|
|
793b473681 | ||
|
|
ad56d3b99a | ||
|
|
e6b8cd3220 | ||
|
|
98725fcb19 | ||
|
|
ee163af833 | ||
|
|
0c6ee2fe99 | ||
|
|
c29b12688c | ||
|
|
8ec933e844 | ||
|
|
13f81ad819 | ||
|
|
8d185fe309 | ||
|
|
ccfa7aff5f | ||
|
|
0b6b85b2a5 | ||
|
|
a6a0b3e931 | ||
|
|
06f8926964 | ||
|
|
edbe49618a | ||
|
|
2ec7b53cea | ||
|
|
ef10ab8088 | ||
|
|
1a39727e38 | ||
|
|
c448d8d2cd | ||
|
|
bb7f1a57f0 | ||
|
|
fb3233cf01 | ||
|
|
30c38fa696 | ||
|
|
c904d0f0c0 | ||
|
|
64d8d53987 | ||
|
|
e4358ac3a1 | ||
|
|
de6f325204 | ||
|
|
13667deaed | ||
|
|
610bf36947 | ||
|
|
126d55e6d8 | ||
|
|
6f0af18619 | ||
|
|
3da201c22d | ||
|
|
5ae9d719f3 | ||
|
|
91bce6c74a | ||
|
|
232ed4f4ef | ||
|
|
7720f1d178 | ||
|
|
533cd3217d | ||
|
|
d16ce17e36 | ||
|
|
e8026afb41 | ||
|
|
81509b660e | ||
|
|
4ed04d1c39 | ||
|
|
17e90b0313 | ||
|
|
a509405f43 | ||
|
|
3d61713f16 | ||
|
|
56987bdcd6 | ||
|
|
24c1abaa06 | ||
|
|
fe75d9543c | ||
|
|
681f9ecf5a | ||
|
|
394ebb8f2d | ||
|
|
6982509233 | ||
|
|
06b64ad250 | ||
|
|
9cc089b3b2 | ||
|
|
ae674a4e62 | ||
|
|
ec117a67aa | ||
|
|
f32c9fa6b1 | ||
|
|
761e372016 | ||
|
|
8d147c9e7b | ||
|
|
4a4c502fdd | ||
|
|
bfea8b00e1 | ||
|
|
94dbfd6802 | ||
|
|
c5dc8454a2 | ||
|
|
ff9a212cfb | ||
|
|
0b3b252fc3 | ||
|
|
133984ab36 | ||
|
|
bcbaf25365 | ||
|
|
6c815c1132 | ||
|
|
6f616a1e78 | ||
|
|
3a436bd530 | ||
|
|
b32d1ef717 | ||
|
|
94d12dfbd4 | ||
|
|
cb7b4133f9 | ||
|
|
6a22cd54ec | ||
|
|
217c2f1bfa | ||
|
|
480428b4a2 | ||
|
|
5650b7cf3a | ||
|
|
bad72e0ef4 | ||
|
|
a808cdc195 | ||
|
|
ee5316e6ef | ||
|
|
cf0551d3e7 | ||
|
|
93d6ad9e55 | ||
|
|
f8fbedd727 | ||
|
|
2cf047a393 | ||
|
|
f9ca66d165 | ||
|
|
45b570e163 | ||
|
|
ab96417d3d | ||
|
|
2fe17d29e3 | ||
|
|
a53194b4a0 |
+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"
|
||||
}
|
||||
|
||||
+4
-2
@@ -6,6 +6,7 @@ npm-debug.log*
|
||||
dump.rdb
|
||||
|
||||
client/coral-framework/graphql/introspection.json
|
||||
docs/source/_data/introspection.json
|
||||
|
||||
.env
|
||||
*.cfg
|
||||
@@ -37,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
|
||||
@@ -49,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
|
||||
@@ -65,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,5 +1,6 @@
|
||||
const express = require('express');
|
||||
const morgan = require('morgan');
|
||||
const trace = require('./middleware/trace');
|
||||
const logging = require('./middleware/logging');
|
||||
const path = require('path');
|
||||
const merge = require('lodash/merge');
|
||||
const helmet = require('helmet');
|
||||
@@ -12,6 +13,10 @@ const { ENABLE_TRACING, APOLLO_ENGINE_KEY, PORT } = require('./config');
|
||||
|
||||
const app = express();
|
||||
|
||||
// Add the trace middleware first, it will create a request ID for each request
|
||||
// downstream.
|
||||
app.use(trace);
|
||||
|
||||
//==============================================================================
|
||||
// PLUGIN PRE APPLICATION MIDDLEWARE
|
||||
//==============================================================================
|
||||
@@ -30,7 +35,7 @@ plugins.get('server', 'app').forEach(({ plugin, app: callback }) => {
|
||||
|
||||
// Add the logging middleware only if we aren't testing.
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
app.use(morgan('dev'));
|
||||
app.use(logging.log);
|
||||
}
|
||||
|
||||
if (ENABLE_TRACING && APOLLO_ENGINE_KEY) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+19
-13
@@ -13,6 +13,7 @@ const CommentModel = require('../models/comment');
|
||||
const AssetsService = require('../services/assets');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const scraper = require('../services/scraper');
|
||||
const Context = require('../graph/context');
|
||||
const inquirer = require('inquirer');
|
||||
const { URL } = require('url');
|
||||
|
||||
@@ -52,22 +53,27 @@ async function refreshAssets(ageString) {
|
||||
const ageMs = parseDuration(ageString);
|
||||
const age = new Date(now - ageMs);
|
||||
|
||||
let assets = await AssetModel.find({
|
||||
$or: [
|
||||
{
|
||||
scraped: {
|
||||
$lte: age,
|
||||
let assets = await AssetModel.find(
|
||||
{
|
||||
$or: [
|
||||
{
|
||||
scraped: {
|
||||
$lte: age,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
scraped: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
{
|
||||
scraped: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ id: 1 }
|
||||
);
|
||||
|
||||
// Create a graph context.
|
||||
const ctx = Context.forSystem();
|
||||
|
||||
// Queue all the assets for scraping.
|
||||
await Promise.all(assets.map(scraper.create));
|
||||
|
||||
await Promise.all(assets.map(({ id }) => scraper.create(ctx, id)));
|
||||
console.log('Assets were queued to be scraped');
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
||||
+7
-5
@@ -14,7 +14,7 @@ const SettingsService = require('../services/settings');
|
||||
const SetupService = require('../services/setup');
|
||||
const UsersService = require('../services/users');
|
||||
const MigrationService = require('../services/migration');
|
||||
const errors = require('../errors');
|
||||
const { ErrSettingsInit, ErrSettingsNotInit } = require('../errors');
|
||||
const Context = require('../graph/context');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
@@ -41,13 +41,15 @@ const performSetup = async () => {
|
||||
|
||||
// We should NOT have gotten a settings object, this means that the
|
||||
// application is already setup. Error out here.
|
||||
throw errors.ErrSettingsInit;
|
||||
} catch (e) {
|
||||
throw new ErrSettingsInit();
|
||||
} catch (err) {
|
||||
// If the error is `not init`, then we're good, otherwise, it's something
|
||||
// else.
|
||||
if (e !== errors.ErrSettingsNotInit) {
|
||||
throw e;
|
||||
if (err instanceof ErrSettingsNotInit) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (program.defaults) {
|
||||
|
||||
+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;
|
||||
|
||||
@@ -2,10 +2,16 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Router, Route, IndexRedirect, IndexRoute } from 'react-router';
|
||||
|
||||
import Configure from 'routes/Configure';
|
||||
import Install from 'routes/Install';
|
||||
import Stories from 'routes/Stories';
|
||||
import Community from 'routes/Community';
|
||||
|
||||
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';
|
||||
|
||||
import Layout from 'containers/Layout';
|
||||
@@ -15,7 +21,15 @@ const routes = (
|
||||
<Route exact path="/admin/install" component={Install} />
|
||||
<Route path="/admin" component={Layout}>
|
||||
<IndexRedirect to="/admin/moderate" />
|
||||
<Route path="configure" component={Configure} />
|
||||
|
||||
<Route path="configure" component={Configure}>
|
||||
<Route path="stream" component={StreamSettings} />
|
||||
<Route path="moderation" component={ModerationSettings} />
|
||||
<Route path="tech" component={TechSettings} />
|
||||
<Route path="organization" component={OrganizationSettings} />
|
||||
<IndexRedirect to="stream" />
|
||||
</Route>
|
||||
|
||||
<Route path="stories" component={Stories} />
|
||||
|
||||
{/* Community Routes */}
|
||||
|
||||
@@ -8,6 +8,10 @@ export const clearPending = () => {
|
||||
return { type: actions.CLEAR_PENDING };
|
||||
};
|
||||
|
||||
export const setActiveSection = section => {
|
||||
return { type: actions.SET_ACTIVE_SECTION, section };
|
||||
export const showSaveDialog = () => {
|
||||
return { type: actions.SHOW_SAVE_DIALOG };
|
||||
};
|
||||
|
||||
export const hideSaveDialog = () => {
|
||||
return { type: actions.HIDE_SAVE_DIALOG };
|
||||
};
|
||||
|
||||
@@ -5,14 +5,7 @@ export const singleView = () => ({ type: actions.SINGLE_VIEW });
|
||||
|
||||
// hide shortcuts note
|
||||
export const hideShortcutsNote = () => (dispatch, _, { localStorage }) => {
|
||||
try {
|
||||
if (localStorage) {
|
||||
localStorage.setItem('coral:shortcutsNote', 'hide');
|
||||
}
|
||||
} catch (e) {
|
||||
// above will fail in Safari private mode
|
||||
}
|
||||
|
||||
localStorage.setItem('coral:shortcutsNote', 'hide');
|
||||
dispatch({ type: actions.HIDE_SHORTCUTS_NOTE });
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -2,4 +2,6 @@ const prefix = 'TALK_ADMIN_CONFIGURE';
|
||||
|
||||
export const UPDATE_PENDING = `${prefix}_UPDATE_PENDING`;
|
||||
export const CLEAR_PENDING = `${prefix}_CLEAR_PENDING`;
|
||||
export const SET_ACTIVE_SECTION = `${prefix}_SET_ACTIVE_SECTION`;
|
||||
|
||||
export const SHOW_SAVE_DIALOG = `${prefix}_SHOW_SAVE_DIALOG`;
|
||||
export const HIDE_SAVE_DIALOG = `${prefix}_HIDE_SAVE_DIALOG`;
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,7 +15,8 @@ import { hideShortcutsNote } from './actions/moderation';
|
||||
smoothscroll.polyfill();
|
||||
|
||||
function init({ store, localStorage }) {
|
||||
if (localStorage && localStorage.getItem('coral:shortcutsNote') === 'hide') {
|
||||
const shouldHide = localStorage.getItem('coral:shortcutsNote') === 'hide';
|
||||
if (shouldHide) {
|
||||
store.dispatch(hideShortcutsNote());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,23 @@ const initialState = {
|
||||
canSave: false,
|
||||
pending: {},
|
||||
errors: {},
|
||||
activeSection: 'stream',
|
||||
saveDialog: false,
|
||||
};
|
||||
|
||||
export default function configure(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.SHOW_SAVE_DIALOG: {
|
||||
return {
|
||||
...state,
|
||||
saveDialog: true,
|
||||
};
|
||||
}
|
||||
case actions.HIDE_SAVE_DIALOG: {
|
||||
return {
|
||||
...state,
|
||||
saveDialog: false,
|
||||
};
|
||||
}
|
||||
case actions.UPDATE_PENDING: {
|
||||
let next = state;
|
||||
if (action.updater) {
|
||||
@@ -40,11 +52,8 @@ export default function configure(state = initialState, action) {
|
||||
pending: {},
|
||||
canSave: false,
|
||||
};
|
||||
case actions.SET_ACTIVE_SECTION:
|
||||
return {
|
||||
...state,
|
||||
activeSection: action.section,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -1,50 +1,47 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { Button, List, Item } from 'coral-ui';
|
||||
import styles from './Configure.css';
|
||||
import StreamSettings from '../containers/StreamSettings';
|
||||
import ModerationSettings from '../containers/ModerationSettings';
|
||||
import TechSettings from '../containers/TechSettings';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { Button, List, Item } from 'coral-ui';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import styles from './Configure.css';
|
||||
import SaveChangesDialog from './SaveChangesDialog';
|
||||
|
||||
export default class Configure extends Component {
|
||||
getSectionComponent(section) {
|
||||
switch (section) {
|
||||
case 'stream':
|
||||
return StreamSettings;
|
||||
case 'moderation':
|
||||
return ModerationSettings;
|
||||
case 'tech':
|
||||
return TechSettings;
|
||||
}
|
||||
throw new Error(`Unknown section ${section}`);
|
||||
}
|
||||
|
||||
class Configure extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
currentUser,
|
||||
canSave,
|
||||
currentUser,
|
||||
root,
|
||||
savePending,
|
||||
setActiveSection,
|
||||
activeSection,
|
||||
settings,
|
||||
clearPending,
|
||||
} = this.props;
|
||||
const SectionComponent = this.getSectionComponent(activeSection);
|
||||
|
||||
if (!can(currentUser, 'UPDATE_CONFIG')) {
|
||||
return (
|
||||
<p>
|
||||
You must be an administrator to access config settings. Please find
|
||||
the nearest Admin and ask them to level you up!
|
||||
</p>
|
||||
);
|
||||
return <p>{t('configure.access_message')}</p>;
|
||||
}
|
||||
|
||||
const passProps = {
|
||||
root,
|
||||
settings,
|
||||
savePending,
|
||||
clearPending,
|
||||
canSave,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<SaveChangesDialog
|
||||
saveDialog={this.props.saveDialog}
|
||||
hideSaveDialog={this.props.hideSaveDialog}
|
||||
saveChanges={this.props.saveChanges}
|
||||
discardChanges={this.props.discardChanges}
|
||||
/>
|
||||
<div className={styles.leftColumn}>
|
||||
<List onChange={setActiveSection} activeItem={activeSection}>
|
||||
<List
|
||||
onChange={this.props.handleSectionChange}
|
||||
activeItem={this.props.activeSection}
|
||||
>
|
||||
<Item itemId="stream" icon="speaker_notes">
|
||||
{t('configure.stream_settings')}
|
||||
</Item>
|
||||
@@ -54,6 +51,9 @@ export default class Configure extends 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 ? (
|
||||
@@ -74,10 +74,7 @@ export default class Configure extends Component {
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.mainContent}>
|
||||
<SectionComponent
|
||||
root={this.props.root}
|
||||
settings={this.props.settings}
|
||||
/>
|
||||
{React.cloneElement(this.props.children, passProps)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -86,10 +83,18 @@ export default class Configure extends Component {
|
||||
|
||||
Configure.propTypes = {
|
||||
savePending: PropTypes.func.isRequired,
|
||||
saveChanges: PropTypes.func.isRequired,
|
||||
discardChanges: PropTypes.func.isRequired,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
canSave: PropTypes.bool.isRequired,
|
||||
setActiveSection: PropTypes.func.isRequired,
|
||||
handleSectionChange: PropTypes.func.isRequired,
|
||||
activeSection: PropTypes.string.isRequired,
|
||||
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;
|
||||
@@ -0,0 +1,40 @@
|
||||
.buttonActions {
|
||||
padding-top: 15px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
padding: 25px;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.close {
|
||||
font-size: 20px;
|
||||
line-height: 14px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
position: absolute;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #363636;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
color: #363636;
|
||||
margin-right: 15px;
|
||||
display: inline-block;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import { Button, Dialog } from 'coral-ui';
|
||||
import styles from './SaveChangesDialog.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const SaveChangesDialog = ({
|
||||
saveDialog,
|
||||
hideSaveDialog,
|
||||
saveChanges,
|
||||
discardChanges,
|
||||
}) => (
|
||||
<Dialog
|
||||
className={cn(styles.dialog, 'talk-admin-configure-save-dialog')}
|
||||
id="saveDialog"
|
||||
open={saveDialog}
|
||||
onCancel={hideSaveDialog}
|
||||
>
|
||||
<span className={styles.close} onClick={hideSaveDialog}>
|
||||
×
|
||||
</span>
|
||||
<div className={styles.title}>
|
||||
{t('configure.save_changes_dialog.unsaved_changes')}
|
||||
</div>
|
||||
{t('configure.save_changes_dialog.copy')}
|
||||
<div
|
||||
className={cn(
|
||||
styles.buttonActions,
|
||||
'talk-admin-configure-save-dialog-button-actions'
|
||||
)}
|
||||
>
|
||||
<a className={styles.cancel} onClick={hideSaveDialog}>
|
||||
Cancel
|
||||
</a>
|
||||
<Button onClick={discardChanges} className={styles.button}>
|
||||
{t('configure.save_changes_dialog.discard')}
|
||||
</Button>
|
||||
<Button onClick={saveChanges} cStyle="green" className={styles.button}>
|
||||
{t('configure.save_changes_dialog.save_settings')}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
SaveChangesDialog.propTypes = {
|
||||
saveDialog: PropTypes.bool.isRequired,
|
||||
hideSaveDialog: PropTypes.func.isRequired,
|
||||
saveChanges: PropTypes.func.isRequired,
|
||||
discardChanges: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default SaveChangesDialog;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
@@ -10,15 +10,87 @@ import { getDefinitionName } from 'coral-framework/utils';
|
||||
import StreamSettings from './StreamSettings';
|
||||
import TechSettings from './TechSettings';
|
||||
import ModerationSettings from './ModerationSettings';
|
||||
import { clearPending, setActiveSection } from '../../../actions/configure';
|
||||
import {
|
||||
clearPending,
|
||||
showSaveDialog,
|
||||
hideSaveDialog,
|
||||
} from '../../../actions/configure';
|
||||
import Configure from '../components/Configure';
|
||||
import OrganizationSettings from './OrganizationSettings';
|
||||
import { withRouter } from 'react-router';
|
||||
|
||||
class ConfigureContainer extends React.Component {
|
||||
nextRoute = '';
|
||||
unregisterLeaveHook = null;
|
||||
|
||||
class ConfigureContainer extends Component {
|
||||
savePending = async () => {
|
||||
await this.props.updateSettings(this.props.pending);
|
||||
this.props.clearPending();
|
||||
};
|
||||
|
||||
saveChanges = async () => {
|
||||
await this.savePending();
|
||||
this.props.hideSaveDialog();
|
||||
this.gotoNextRoute();
|
||||
};
|
||||
|
||||
discardChanges = async () => {
|
||||
await this.props.clearPending();
|
||||
this.props.hideSaveDialog();
|
||||
this.gotoNextRoute();
|
||||
};
|
||||
|
||||
gotoNextRoute = () => {
|
||||
if (this.nextRoute) {
|
||||
this.props.router.push(this.nextRoute);
|
||||
this.nextRoute = '';
|
||||
}
|
||||
};
|
||||
|
||||
handleSectionChange = async section => {
|
||||
const nextRoute = `/admin/configure/${section}`;
|
||||
if (this.hasPendingData()) {
|
||||
this.nextRoute = nextRoute;
|
||||
this.props.showSaveDialog();
|
||||
} else {
|
||||
// Just go to the section
|
||||
this.props.router.push(nextRoute);
|
||||
}
|
||||
};
|
||||
|
||||
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.hasPendingData()) {
|
||||
this.nextRoute = pathname;
|
||||
this.props.showSaveDialog();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
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() {
|
||||
if (this.props.data.error) {
|
||||
return <div>{this.props.data.error.message}</div>;
|
||||
@@ -28,16 +100,25 @@ class ConfigureContainer extends Component {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const activeSection = this.props.routes[3].path;
|
||||
|
||||
return (
|
||||
<Configure
|
||||
saveChanges={this.saveChanges}
|
||||
discardChanges={this.discardChanges}
|
||||
saveDialog={this.props.saveDialog}
|
||||
activeSection={activeSection}
|
||||
hideSaveDialog={this.props.hideSaveDialog}
|
||||
canSave={this.props.canSave}
|
||||
currentUser={this.props.currentUser}
|
||||
root={this.props.root}
|
||||
settings={this.props.mergedSettings}
|
||||
canSave={this.props.canSave}
|
||||
handleSectionChange={this.handleSectionChange}
|
||||
clearPending={this.props.clearPending}
|
||||
savePending={this.savePending}
|
||||
setActiveSection={this.props.setActiveSection}
|
||||
activeSection={this.props.activeSection}
|
||||
/>
|
||||
>
|
||||
{this.props.children}
|
||||
</Configure>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -49,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}
|
||||
@@ -60,6 +143,8 @@ const withConfigureQuery = withQuery(
|
||||
${TechSettings.fragments.settings}
|
||||
${ModerationSettings.fragments.root}
|
||||
${ModerationSettings.fragments.settings}
|
||||
${OrganizationSettings.fragments.root}
|
||||
${OrganizationSettings.fragments.settings}
|
||||
`,
|
||||
{
|
||||
options: () => ({
|
||||
@@ -74,18 +159,21 @@ const mapStateToProps = state => ({
|
||||
pending: state.configure.pending,
|
||||
canSave: state.configure.canSave,
|
||||
activeSection: state.configure.activeSection,
|
||||
saveDialog: state.configure.saveDialog,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
clearPending,
|
||||
setActiveSection,
|
||||
showSaveDialog,
|
||||
hideSaveDialog,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withUpdateSettings,
|
||||
withConfigureQuery,
|
||||
@@ -93,14 +181,20 @@ export default compose(
|
||||
)(ConfigureContainer);
|
||||
|
||||
ConfigureContainer.propTypes = {
|
||||
activeSection: PropTypes.string,
|
||||
updateSettings: PropTypes.func.isRequired,
|
||||
clearPending: PropTypes.func.isRequired,
|
||||
setActiveSection: PropTypes.func.isRequired,
|
||||
showSaveDialog: PropTypes.func.isRequired,
|
||||
hideSaveDialog: PropTypes.func.isRequired,
|
||||
saveDialog: PropTypes.bool.isRequired,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
canSave: PropTypes.bool.isRequired,
|
||||
pending: PropTypes.object.isRequired,
|
||||
mergedSettings: PropTypes.object.isRequired,
|
||||
activeSection: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
router: PropTypes.object,
|
||||
route: PropTypes.object,
|
||||
routes: PropTypes.array,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -10,12 +10,14 @@ const InitialStep = () => {
|
||||
return (
|
||||
<div className={cn(styles.step, styles.finalStep, 'talk-install-step-5')}>
|
||||
<p>{t('install.final.description')}</p>
|
||||
<Button raised>
|
||||
<Link to="/admin">{t('install.final.launch')}</Link>
|
||||
</Button>
|
||||
<Button cStyle="black" raised>
|
||||
<a href="http://coralproject.net">{t('install.final.close')}</a>
|
||||
</Button>
|
||||
<Link to="/admin">
|
||||
<Button raised>{t('install.final.launch')}</Button>
|
||||
</Link>
|
||||
<a href="http://coralproject.net">
|
||||
<Button cStyle="black" raised>
|
||||
{t('install.final.close')}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+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(', ')}],`
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { GraphQLDocs } from 'graphql-docs';
|
||||
|
||||
import fetcher from './services/fetcher';
|
||||
|
||||
// Render the application into the DOM
|
||||
render(<GraphQLDocs fetcher={fetcher} />, document.querySelector('#root'));
|
||||
@@ -1,10 +0,0 @@
|
||||
export default function fetcher(query) {
|
||||
return fetch(`${window.location.origin}/api/v1/graph/ql`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ query }),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
@@ -35,12 +35,9 @@ class ExtendableTabPanelContainer extends React.Component {
|
||||
|
||||
createPluginTabFactory = (props = this.props) => el => {
|
||||
return (
|
||||
<ExtendableTab
|
||||
tabId={el.type.talkPluginName}
|
||||
key={el.type.talkPluginName}
|
||||
>
|
||||
<ExtendableTab tabId={el.key} key={el.key}>
|
||||
{React.cloneElement(el, {
|
||||
active: props.activeTab === el.type.talkPluginName,
|
||||
active: props.activeTab === el.key,
|
||||
})}
|
||||
</ExtendableTab>
|
||||
);
|
||||
@@ -59,7 +56,7 @@ class ExtendableTabPanelContainer extends React.Component {
|
||||
|
||||
createPluginTabPane(el) {
|
||||
return (
|
||||
<TabPane tabId={el.type.talkPluginName} key={el.type.talkPluginName}>
|
||||
<TabPane tabId={el.key} key={el.key}>
|
||||
{el}
|
||||
</TabPane>
|
||||
);
|
||||
|
||||
@@ -34,6 +34,7 @@ class Comment extends React.Component {
|
||||
defaultComponent={CommentContent}
|
||||
className={cn(styles.commentBody, 'my-comment-body')}
|
||||
passthrough={slotPassthrough}
|
||||
size={1}
|
||||
/>
|
||||
<div className={cn(styles.commentSummary, 'comment-summary')}>
|
||||
<span
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,7 @@ export const checkLogin = () => (
|
||||
rest('/auth')
|
||||
.then(result => {
|
||||
if (!result.user) {
|
||||
if (localStorage) {
|
||||
cleanAuthData(localStorage);
|
||||
}
|
||||
cleanAuthData(localStorage);
|
||||
dispatch(checkLoginSuccess(null));
|
||||
return;
|
||||
}
|
||||
@@ -52,10 +50,12 @@ const checkLoginSuccess = user => ({
|
||||
});
|
||||
|
||||
export const setAuthToken = token => (dispatch, _, { localStorage }) => {
|
||||
if (localStorage) {
|
||||
localStorage.setItem('exp', jwtDecode(token).exp);
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
localStorage.setItem('exp', jwtDecode(token).exp);
|
||||
localStorage.setItem('token', token);
|
||||
|
||||
// Dispatch the set auth token action. For some browsers and situations, we
|
||||
// may not be able to persist the auth token any other way. Keep it in redux!
|
||||
dispatch({ type: actions.SET_AUTH_TOKEN, token });
|
||||
|
||||
dispatch(checkLogin());
|
||||
};
|
||||
@@ -66,11 +66,8 @@ export const handleSuccessfulLogin = (user, token) => (
|
||||
{ client, localStorage, postMessage }
|
||||
) => {
|
||||
const { exp } = jwtDecode(token);
|
||||
|
||||
if (localStorage) {
|
||||
localStorage.setItem('exp', exp);
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
localStorage.setItem('exp', exp);
|
||||
localStorage.setItem('token', token);
|
||||
|
||||
// Send the message via the messages service to the window.opener if it
|
||||
// exists.
|
||||
@@ -87,6 +84,7 @@ export const handleSuccessfulLogin = (user, token) => (
|
||||
dispatch({
|
||||
type: actions.HANDLE_SUCCESSFUL_LOGIN,
|
||||
user,
|
||||
token,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -100,9 +98,8 @@ export const logout = () => async (
|
||||
) => {
|
||||
await rest('/auth', { method: 'DELETE' });
|
||||
|
||||
if (localStorage) {
|
||||
cleanAuthData(localStorage);
|
||||
}
|
||||
// Clear the auth data persisted to localStorage.
|
||||
cleanAuthData(localStorage);
|
||||
|
||||
// Reset the websocket.
|
||||
client.resetWebsocket();
|
||||
|
||||
@@ -43,7 +43,7 @@ const ConfigureCard = ({
|
||||
);
|
||||
|
||||
ConfigureCard.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
onCheckbox: PropTypes.func,
|
||||
checked: PropTypes.bool,
|
||||
|
||||
@@ -7,7 +7,7 @@ class IfSlotIsNotEmpty extends React.Component {
|
||||
isSlotEmpty(props = this.props) {
|
||||
const { slotElements } = props;
|
||||
return slotElements.length === 0
|
||||
? false
|
||||
? true
|
||||
: slotElements.every(elements => elements.length === 0);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ class IfSlotIsNotEmpty extends React.Component {
|
||||
|
||||
IfSlotIsNotEmpty.propTypes = {
|
||||
slot: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
|
||||
slotElements: PropTypes.array.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
passthrough: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const prefix = `TALK_FRAMEWORK`;
|
||||
|
||||
export const SET_AUTH_TOKEN = `${prefix}_SET_AUTH_TOKEN`;
|
||||
|
||||
export const CHECK_LOGIN_REQUEST = `${prefix}_CHECK_LOGIN_REQUEST`;
|
||||
export const CHECK_LOGIN_SUCCESS = `${prefix}_CHECK_LOGIN_SUCCESS`;
|
||||
export const CHECK_LOGIN_FAILURE = `${prefix}_CHECK_LOGIN_FAILURE`;
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ const initialState = {
|
||||
checkedInitialLogin: false,
|
||||
initialLoginError: null,
|
||||
user: null,
|
||||
token: null,
|
||||
};
|
||||
|
||||
const purge = user => {
|
||||
@@ -14,12 +15,18 @@ const purge = user => {
|
||||
|
||||
export default function auth(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.SET_AUTH_TOKEN:
|
||||
return {
|
||||
...state,
|
||||
token: action.token || null,
|
||||
};
|
||||
case actions.CHECK_LOGIN_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
initialLoginError: action.error,
|
||||
checkedInitialLogin: true,
|
||||
user: null,
|
||||
token: null,
|
||||
};
|
||||
case actions.CHECK_LOGIN_SUCCESS:
|
||||
return {
|
||||
@@ -31,11 +38,13 @@ export default function auth(state = initialState, action) {
|
||||
return {
|
||||
...state,
|
||||
user: action.user ? purge(action.user) : null,
|
||||
token: action.token || null,
|
||||
};
|
||||
case actions.LOGOUT:
|
||||
return {
|
||||
...state,
|
||||
user: null,
|
||||
token: null,
|
||||
};
|
||||
case actions.UPDATE_STATUS: {
|
||||
return {
|
||||
|
||||
+7
-1
@@ -47,6 +47,12 @@ const getAuthToken = (store, storage) => {
|
||||
} else if (!bowser.safari && !bowser.ios && storage) {
|
||||
// Use local storage auth tokens where there's a stable api.
|
||||
return storage.getItem('token');
|
||||
} else if (state.auth && state.auth.token) {
|
||||
// Use the redux token state if the remaining methods fall out. If the embed
|
||||
// is called with `embed.login(token)`, and the browser is not capable of
|
||||
// storing the token in localStorage, then we would have persisted it to the
|
||||
// redux state.
|
||||
return state.auth.token;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -123,7 +129,7 @@ export async function createContext({
|
||||
// Try to get the token from localStorage. If it isn't here, it may
|
||||
// be passed as a cookie.
|
||||
|
||||
// NOTE: THIS IS ONLY EVER EVALUATED ONCE, IN ORDER TO SEND A DIFFERNT
|
||||
// NOTE: THIS IS ONLY EVER EVALUATED ONCE, IN ORDER TO SEND A DIFFERENT
|
||||
// TOKEN YOU MUST DISCONNECT AND RECONNECT THE WEBSOCKET CLIENT.
|
||||
return getAuthToken(store, localStorage);
|
||||
};
|
||||
|
||||
@@ -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 = {
|
||||
@@ -52,26 +53,41 @@ let lang;
|
||||
let timeagoInstance;
|
||||
|
||||
function setLocale(storage, locale) {
|
||||
try {
|
||||
if (storage) {
|
||||
storage.setItem('locale', locale);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
storage.setItem('locale', locale);
|
||||
}
|
||||
|
||||
function getLocale(storage) {
|
||||
// detectLanguage will try to get the locale from storage if available,
|
||||
// otherwise will try to get it from the navigator, otherwise, it will fallback
|
||||
// to the default language.
|
||||
function detectLanguage(storage) {
|
||||
try {
|
||||
return (
|
||||
(storage && storage.getItem('locale')) ||
|
||||
navigator.language ||
|
||||
defaultLanguage
|
||||
).split('-')[0];
|
||||
const lang = storage.getItem('locale') || navigator.language;
|
||||
if (lang) {
|
||||
return lang;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return null;
|
||||
console.warn(
|
||||
'Error while trying to detect language, will fallback to',
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
console.warn('Could not detect language, will fallback to', defaultLanguage);
|
||||
return defaultLanguage;
|
||||
}
|
||||
|
||||
// getLocale will get the users locale from the local detector and parse it to a
|
||||
// format we can work with.
|
||||
function getLocale(storage) {
|
||||
// Get the language from the local detector.
|
||||
const lang = detectLanguage(storage);
|
||||
|
||||
// Some language strings come with additional subtags as defined in:
|
||||
//
|
||||
// https://www.ietf.org/rfc/bcp/bcp47.txt
|
||||
//
|
||||
// So we should strip that off if we find it.
|
||||
return lang.split('-')[0];
|
||||
}
|
||||
|
||||
export function setupTranslations() {
|
||||
@@ -97,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();
|
||||
}
|
||||
@@ -136,6 +152,7 @@ export function t(key, ...replacements) {
|
||||
replacements.forEach((str, i) => {
|
||||
translation = translation.replace(new RegExp(`\\{${i}\\}`, 'g'), str);
|
||||
});
|
||||
|
||||
return translation;
|
||||
} else {
|
||||
console.warn(`${lang}.${key} and en.${key} language key not set`);
|
||||
|
||||
@@ -173,11 +173,30 @@ class PluginsService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This adds a consistent keying for the slot elements.
|
||||
* It uses the plugin name as the key. If the same plugin inserts
|
||||
* multiple elements it will append `.${noOfOccurence}` to the
|
||||
* key starting with the second element.
|
||||
*/
|
||||
const getKey = (() => {
|
||||
const map = {};
|
||||
return component => {
|
||||
if (map[component.talkPluginName] === undefined) {
|
||||
map[component.talkPluginName] = 0;
|
||||
} else {
|
||||
map[component.talkPluginName]++;
|
||||
}
|
||||
const i = map[component.talkPluginName];
|
||||
return `${component.talkPluginName}${i > 0 ? `.${i}` : ''}`;
|
||||
};
|
||||
})();
|
||||
|
||||
return (size > 0 ? slots.slice(0, size) : slots)
|
||||
.map((component, i) => ({
|
||||
.map(component => ({
|
||||
component,
|
||||
disabled: isDisabled(component),
|
||||
key: i,
|
||||
key: getKey(component),
|
||||
}))
|
||||
.filter(o => !o.disabled)
|
||||
.map(({ component, key }) =>
|
||||
|
||||
@@ -1,40 +1,116 @@
|
||||
import uuid from 'uuid/v4';
|
||||
|
||||
function getStorage(type) {
|
||||
let storage;
|
||||
try {
|
||||
storage = window[type];
|
||||
const x = '__storage_test__';
|
||||
storage.setItem(x, x);
|
||||
storage.removeItem(x);
|
||||
} catch (e) {
|
||||
const ignore =
|
||||
e instanceof DOMException &&
|
||||
// everything except Firefox
|
||||
(e.code === 22 ||
|
||||
// SecurityError related to having 3rd party cookies disabled.
|
||||
e.code === 18 ||
|
||||
// Firefox
|
||||
function testStorageAccess(storage) {
|
||||
const key = '__storage_test__';
|
||||
|
||||
e.code === 1014 ||
|
||||
// test name field too, because code might not be present
|
||||
// Create a unique test value.
|
||||
const expectedValue = String(Date.now());
|
||||
|
||||
// everything except Firefox
|
||||
e.name === 'QuotaExceededError' ||
|
||||
// Firefox
|
||||
e.name === 'NS_ERROR_DOM_QUOTA_REACHED');
|
||||
if (!ignore) {
|
||||
console.warn(e);
|
||||
// Try to set, get, and remove that item.
|
||||
storage.setItem(key, expectedValue);
|
||||
const canSetGet = expectedValue === storage.getItem(key);
|
||||
storage.removeItem(key);
|
||||
|
||||
if (!canSetGet) {
|
||||
// We can't access the desired storage!
|
||||
throw new Error('Storage access test failed');
|
||||
}
|
||||
}
|
||||
|
||||
// InMemoryStorage is a dumb implementation of the Storage interface that will
|
||||
// not persist the data at all. It implements the Storage interface found:
|
||||
//
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Storage
|
||||
//
|
||||
class InMemoryStorage {
|
||||
constructor() {
|
||||
this.storage = {};
|
||||
}
|
||||
|
||||
get length() {
|
||||
return Object.keys(this.storage).length;
|
||||
}
|
||||
|
||||
key(n) {
|
||||
if (this.length <= n) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// When third party cookies are disabled, session storage is readable/
|
||||
// writable, but localStorage is not. Try to get the sessionStorage to use.
|
||||
if (type !== 'sessionStorage') {
|
||||
return getStorage('sessionStorage');
|
||||
return this.storage[Object.keys(this.storage)[n]];
|
||||
}
|
||||
|
||||
getItem(key) {
|
||||
return this.storage[key];
|
||||
}
|
||||
|
||||
setItem(key, value) {
|
||||
this.storage[key] = value;
|
||||
|
||||
try {
|
||||
// Test sessionStorage. We could have been given access recently.
|
||||
testStorageAccess(sessionStorage);
|
||||
|
||||
// Test passed! Set the item in sessionStorage.
|
||||
sessionStorage.setItem(key, value);
|
||||
console.log(
|
||||
'Attempt to persist InMemoryStorage value to sessionStorage succeeded'
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
'Attempt to persist InMemoryStorage value to sessionStorage failed',
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return storage;
|
||||
removeItem(key) {
|
||||
delete this.storage[key];
|
||||
|
||||
try {
|
||||
// Test sessionStorage. We could have been given access recently.
|
||||
testStorageAccess(sessionStorage);
|
||||
|
||||
// Test passed! Remove the item from sessionStorage.
|
||||
sessionStorage.removeItem(key);
|
||||
console.log(
|
||||
'Attempt to persist InMemoryStorage delete to sessionStorage succeeded'
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
'Attempt to persist InMemoryStorage delete to sessionStorage failed',
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getStorage will test to see if the requested storage type is available, if it
|
||||
// is not, it will try sessionStorage, and if that is also not available, it
|
||||
// will fallback to InMemoryStorage.
|
||||
function getStorage(type) {
|
||||
try {
|
||||
// Get the desired storage from the window and test it out.
|
||||
const storage = window[type];
|
||||
testStorageAccess(storage);
|
||||
|
||||
// Storage test was successful! Return it.
|
||||
return storage;
|
||||
} catch (err) {
|
||||
// When third party cookies are disabled, session storage is readable/
|
||||
// writable, but localStorage is not. Try to get the sessionStorage to use.
|
||||
if (type !== 'sessionStorage') {
|
||||
console.warn('Could not access', type, 'trying sessionStorage', err);
|
||||
return getStorage('sessionStorage');
|
||||
}
|
||||
|
||||
console.warn(
|
||||
'Could not access sessionStorage falling back to InMemoryStorage',
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
// No acceptable storage could be found, returning the InMemoryStorage.
|
||||
return new InMemoryStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -27,11 +27,14 @@ const TextField = ({
|
||||
);
|
||||
|
||||
TextField.propTypes = {
|
||||
id: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
errorMsg: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
showErrors: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default TextField;
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
@@ -59,6 +59,10 @@ const CONFIG = {
|
||||
// it was built at.
|
||||
REVISION_HASH: process.env.REVISION_HASH,
|
||||
|
||||
// SCRAPER_HEADERS is a JSON string that will be used to override the headers
|
||||
// on the scraper when it makes requests.
|
||||
SCRAPER_HEADERS: process.env.TALK_SCRAPER_HEADERS || '{}',
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// JWT based configuration
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -208,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,
|
||||
|
||||
|
||||
+7
-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
|
||||
@@ -122,6 +124,8 @@ sidebar:
|
||||
url: /commenter-features/
|
||||
- title: Moderator Features
|
||||
url: /moderator-features/
|
||||
- title: User Roles in Talk
|
||||
url: /roles/
|
||||
- title: Trust
|
||||
url: /trust/
|
||||
- title: Toxic Comments
|
||||
@@ -154,6 +158,8 @@ sidebar:
|
||||
url: /building-basic-plugin/
|
||||
- title: Customizing Plugins with Coral UI
|
||||
url: /customizing-plugins-coral-ui/
|
||||
- title: When You've Installed Talk
|
||||
url: /when-youve-installed-talk/
|
||||
- title: Migrating
|
||||
children:
|
||||
- title: Migrating to v4.0.0
|
||||
|
||||
@@ -167,14 +167,10 @@ TALK_REDIS_URL=redis://127.0.0.1:6379
|
||||
TALK_ROOT_URL=http://127.0.0.1:3000
|
||||
TALK_PORT=3000
|
||||
TALK_JWT_SECRET=password
|
||||
TALK_FACEBOOK_APP_ID=A-Facebook-App-ID
|
||||
TALK_FACEBOOK_APP_SECRET=A-Facebook-App-Secret
|
||||
```
|
||||
|
||||
This is only the bare minimum needed to run the demo, for more configuration
|
||||
variables, check out the [Configuration](/talk/configuration/) section. Facebook login above
|
||||
will definitely not work unless you change those values as well.
|
||||
|
||||
variables, check out the [Configuration](/talk/configuration/) section.
|
||||
|
||||
You can now start the application by running:
|
||||
|
||||
|
||||
@@ -57,14 +57,11 @@ TALK_REDIS_URL=redis://127.0.0.1:6379
|
||||
TALK_ROOT_URL=http://127.0.0.1:3000
|
||||
TALK_PORT=3000
|
||||
TALK_JWT_SECRET=password
|
||||
TALK_FACEBOOK_APP_ID=A-Facebook-App-ID
|
||||
TALK_FACEBOOK_APP_SECRET=A-Facebook-App-Secret
|
||||
```
|
||||
|
||||
This is the bare minimum needed to start Talk, for more configuration
|
||||
variables, check out the [Configuration](/talk/configuration/)
|
||||
section. Facebook login above will definitely not work unless you change those
|
||||
values as well.
|
||||
section.
|
||||
|
||||
|
||||
You can now start the application by running:
|
||||
|
||||
@@ -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
|
||||
@@ -524,4 +536,11 @@ Sets the logging level for the context logger (from [Bunyan](https://github.com/
|
||||
- `warn`
|
||||
- `info`
|
||||
- `debug`
|
||||
- `trace`
|
||||
- `trace`
|
||||
|
||||
## TALK_SCRAPER_HEADERS
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ All levels of comments and replies are able to be linked to via permalink. Perma
|
||||
```text
|
||||
https://<your asset url>?commentId=<the comment id>
|
||||
```
|
||||
{:.no-copy}
|
||||
|
||||
|
||||
### Threading
|
||||
|
||||
@@ -44,7 +44,7 @@ talk-stream-comment-level-${depth}
|
||||
talk-stream-highlighted-comment
|
||||
talk-stream-pending-comment
|
||||
```
|
||||
{:.no-copy}
|
||||
|
||||
|
||||
### Automatic Updates
|
||||
|
||||
|
||||
@@ -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/).
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: User Roles in Talk
|
||||
permalink: /roles/
|
||||
---
|
||||
|
||||
We have four preset roles in Talk:
|
||||
|
||||
**Commenter**
|
||||
* A standard community member
|
||||
* Could receive a badge (eg. 'Subscriber') via [a custom newsroom Plugin Recipe](https://docs.coralproject.net/talk/plugin-recipes/#recipe-subscriber)
|
||||
* No moderation abilities
|
||||
* No configuration abilities
|
||||
|
||||
**Staff**
|
||||
* A standard community member
|
||||
* Receives a Staff badge when they comment
|
||||
* Comments are automatically approved
|
||||
* No moderation abilities
|
||||
* No configuration abilities
|
||||
|
||||
**Moderator**
|
||||
* A standard community member
|
||||
* Receives a Staff badge when they comment
|
||||
* Comments are automatically approved
|
||||
* Has full moderation privileges
|
||||
* Can configure individual articles via the Configure tab on the article page
|
||||
* No site-wide configuration abilities
|
||||
|
||||
**Administrator**
|
||||
* A standard community member
|
||||
* Receives a Staff badge when they comment
|
||||
* Comments are automatically approved
|
||||
* Has full moderation privileges
|
||||
* Can configure individual articles via the Configure tab on the article page
|
||||
* Can configure site settings via the Configure tab in the moderation interface
|
||||
@@ -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/).
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user