mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
375ef09d78 | ||
|
|
135f0e300f | ||
|
|
9d752685cd | ||
|
|
a638dafd85 | ||
|
|
93436a2eb5 | ||
|
|
a16233484c | ||
|
|
c98830aa63 | ||
|
|
c24251e8f2 | ||
|
|
9ed991936e | ||
|
|
ede273a3e0 | ||
|
|
c6deabf919 | ||
|
|
2e40cd53cb | ||
|
|
b9e3a01cdd | ||
|
|
5c7345313e | ||
|
|
8933b43e1a | ||
|
|
6c1fb2b34d | ||
|
|
0db3b354ca | ||
|
|
a732793cb1 | ||
|
|
03a7ecff0b | ||
|
|
606f463331 | ||
|
|
576820f1f8 | ||
|
|
3a84da9495 | ||
|
|
b15a306c43 | ||
|
|
4299c373ba | ||
|
|
6f0694d341 | ||
|
|
da820ad6db | ||
|
|
ed679096c8 | ||
|
|
6577365930 | ||
|
|
1fb206daa4 | ||
|
|
2bd4e1182b | ||
|
|
9fe2175da3 | ||
|
|
c1692dfb70 | ||
|
|
cb83514365 | ||
|
|
ba3af0aeb3 | ||
|
|
51cb917d6b | ||
|
|
b02fd67742 | ||
|
|
90e4682cf6 | ||
|
|
3ff8b8519e | ||
|
|
105cff6990 | ||
|
|
99854d8644 | ||
|
|
cedabf04e3 | ||
|
|
2c6843129a | ||
|
|
47761cf8a0 | ||
|
|
4442ffc784 | ||
|
|
016b6d6007 | ||
|
|
e94d13e594 | ||
|
|
68c7477f3a | ||
|
|
700cee4e56 | ||
|
|
d0740cb323 | ||
|
|
c021c3adc8 | ||
|
|
130fb0d4e9 | ||
|
|
79a0f0321f | ||
|
|
5090f9c6b2 | ||
|
|
5e8c950dc5 | ||
|
|
c7b2641ea7 | ||
|
|
ba2abab358 | ||
|
|
5535dde053 | ||
|
|
c1eab3f2c6 | ||
|
|
540df09b5b | ||
|
|
ed2842717d | ||
|
|
d9621e6801 | ||
|
|
5f6b1235aa | ||
|
|
d923857eda | ||
|
|
d96702eef5 | ||
|
|
0e29a85d72 | ||
|
|
7d861236b8 | ||
|
|
3a79cc684b | ||
|
|
e1d77daf47 | ||
|
|
4ec5ccd1a3 | ||
|
|
5e6586d228 | ||
|
|
d414867bb0 | ||
|
|
cfb162c490 | ||
|
|
b7d2621cb8 | ||
|
|
3141133e34 | ||
|
|
325e763fd0 | ||
|
|
ab6a193dcb | ||
|
|
56483f3888 | ||
|
|
4800e32758 | ||
|
|
16573b127e | ||
|
|
abafa45be7 | ||
|
|
ce708bff42 | ||
|
|
64c16a1805 | ||
|
|
f8cc658c54 | ||
|
|
e39bcb11ba | ||
|
|
a046162f9c | ||
|
|
8f51b07388 | ||
|
|
45113f14c7 | ||
|
|
0c35badd95 | ||
|
|
1c0bfdb89e | ||
|
|
8f4846404a | ||
|
|
c642cc811c | ||
|
|
945ab9a1b6 | ||
|
|
e1298e4fd9 | ||
|
|
0c1e4873f8 | ||
|
|
f3d89e63d2 | ||
|
|
0835f3f321 | ||
|
|
9a73c6db59 | ||
|
|
97416d421b | ||
|
|
f36b6e2cb6 | ||
|
|
975448ec69 | ||
|
|
92420ae827 | ||
|
|
a6ec021bd1 | ||
|
|
921b85fba3 | ||
|
|
9c7ed62cba | ||
|
|
b7a053c011 | ||
|
|
6548df971c | ||
|
|
27818d966d | ||
|
|
f7f6ed635c | ||
|
|
61c09f5818 | ||
|
|
def20a3b37 | ||
|
|
4ae3e90422 | ||
|
|
9539073eb8 | ||
|
|
422260607e | ||
|
|
a16c584880 | ||
|
|
3fcc85139d | ||
|
|
17f76e85b0 | ||
|
|
a95d55b6da | ||
|
|
879865a4c1 | ||
|
|
8405bdf9bc | ||
|
|
8e24d4f5c6 | ||
|
|
9265d29ac0 | ||
|
|
849fe37b9b | ||
|
|
1a52ad5d97 | ||
|
|
b984af3e2e | ||
|
|
10a786f84d | ||
|
|
4f5f424c77 | ||
|
|
9b4455f7bd | ||
|
|
3a12e776c3 | ||
|
|
6667bbe01f | ||
|
|
583cb6566b | ||
|
|
f90e17c3e0 | ||
|
|
71b1259db6 | ||
|
|
a39b967164 | ||
|
|
b411f3e059 | ||
|
|
1aed51468f | ||
|
|
fd271d237c | ||
|
|
04ae6fbb43 | ||
|
|
4767354379 | ||
|
|
55998a67da | ||
|
|
eee1e858bf | ||
|
|
2c9d30bfd2 | ||
|
|
763833783d | ||
|
|
1f5e3d9f3f | ||
|
|
30f03f5fb8 | ||
|
|
43c7b6f23f | ||
|
|
ced9d2ea4b | ||
|
|
5aba578a98 | ||
|
|
dd0ad17038 | ||
|
|
868343a29b | ||
|
|
5403872bb7 | ||
|
|
f58e3df439 | ||
|
|
000d9ae56a | ||
|
|
e2ba08e708 | ||
|
|
20860bb773 | ||
|
|
b15766bf80 | ||
|
|
531cb0793f | ||
|
|
8265c13149 | ||
|
|
b816053191 | ||
|
|
ffa6b99d09 | ||
|
|
0e8065cdf9 | ||
|
|
9484d4c6f5 | ||
|
|
5ebbc24264 | ||
|
|
43e0f95aa6 | ||
|
|
1f40d6b7bb | ||
|
|
8ee90387f6 | ||
|
|
43170121f3 | ||
|
|
4f001bbdf2 | ||
|
|
f5362f445a | ||
|
|
1bcf1a23fc | ||
|
|
b2306066ce | ||
|
|
ebcff4d95e | ||
|
|
2484066922 | ||
|
|
2822a2dddd | ||
|
|
5f410276c6 | ||
|
|
c84a9716a1 | ||
|
|
d83c2fd566 | ||
|
|
cd6aacbc76 | ||
|
|
95072be97e | ||
|
|
f0589fba26 | ||
|
|
417befd52a | ||
|
|
8d8cd57d79 | ||
|
|
086c43a003 | ||
|
|
887f7e17f4 | ||
|
|
6444e8c2ab | ||
|
|
0339e97b37 | ||
|
|
ae88ae3f98 | ||
|
|
5241206b03 | ||
|
|
663caa1001 | ||
|
|
d07c54918d | ||
|
|
2eac8f48d6 | ||
|
|
3a383ff8ba | ||
|
|
34760653a2 | ||
|
|
d176578aed | ||
|
|
e3323ce5ec | ||
|
|
7fc5c5a6be | ||
|
|
2524004e2a | ||
|
|
91060537bb | ||
|
|
f0b87501c0 | ||
|
|
ea6757cce5 | ||
|
|
cb9945053f | ||
|
|
fca6603b38 | ||
|
|
56ee58efdf | ||
|
|
0b407d516b | ||
|
|
4cca800af0 | ||
|
|
ee0790dbe8 | ||
|
|
6890d24e47 | ||
|
|
2392e1ca78 | ||
|
|
40a431b374 | ||
|
|
5214705f5b | ||
|
|
79df9b487f | ||
|
|
ef5e5ddbcf | ||
|
|
2e773a4438 | ||
|
|
2a2eabfae0 | ||
|
|
bb9be71a7a | ||
|
|
50acf9ca50 | ||
|
|
3ae0136f75 | ||
|
|
d3ad486f81 | ||
|
|
32510db1fa | ||
|
|
8d44525c16 | ||
|
|
83fddfdc40 | ||
|
|
ed32a69b68 | ||
|
|
02b1e03c21 | ||
|
|
08d523a4d6 | ||
|
|
b5adea4b2a | ||
|
|
beef4e8b3b | ||
|
|
5a190839d1 | ||
|
|
ab7e5bc858 | ||
|
|
12e848aace | ||
|
|
67c3fe3c5b | ||
|
|
1f7c4e58a1 | ||
|
|
8e392ef32d | ||
|
|
36063b459e | ||
|
|
400890237c | ||
|
|
bde1dff33b | ||
|
|
f846ef0c06 | ||
|
|
fa31541fb7 | ||
|
|
4fcd5dd4f7 | ||
|
|
7d8d5a6115 | ||
|
|
f0dcd844a7 | ||
|
|
9a1b9fb864 | ||
|
|
6cf4203c9b | ||
|
|
85e21f8090 | ||
|
|
64583fabbe | ||
|
|
bdaf5c633e | ||
|
|
945e2670f5 | ||
|
|
4166e36e1e | ||
|
|
f018ccf33e | ||
|
|
e61f325e86 | ||
|
|
ef72b7b1d5 | ||
|
|
a571cf830d | ||
|
|
5dea051427 | ||
|
|
730ef1a5c4 | ||
|
|
622b79b491 | ||
|
|
faa0ee8ceb | ||
|
|
d67c25a791 | ||
|
|
32d615527a | ||
|
|
3a9051bf1b | ||
|
|
e8cce02bef | ||
|
|
44c2d7847a | ||
|
|
93359d8250 | ||
|
|
ba2985e891 | ||
|
|
42d4abf842 | ||
|
|
29ffe287cd | ||
|
|
05c13126ad | ||
|
|
787dd8ec8a | ||
|
|
47c7125190 | ||
|
|
fc5621a060 | ||
|
|
f57f8397fe | ||
|
|
1236e7a504 | ||
|
|
bae20c38ec | ||
|
|
d731357001 | ||
|
|
f28a3f2f4e | ||
|
|
48fc3ab5db | ||
|
|
fab15daacd | ||
|
|
499030028c | ||
|
|
bbd3050b95 | ||
|
|
b73620bcb0 | ||
|
|
2d1b675234 | ||
|
|
ac7bea209e | ||
|
|
dd1b51e09b | ||
|
|
33be00a540 | ||
|
|
631206f360 | ||
|
|
4d6ef95416 | ||
|
|
462f5e8206 | ||
|
|
524aa3e863 | ||
|
|
e0cada5bde | ||
|
|
dd0381d22a | ||
|
|
2e37f78705 | ||
|
|
0203a65656 | ||
|
|
4413e5616c | ||
|
|
b9c5024e68 | ||
|
|
38f9c2f458 | ||
|
|
fc14c5ab1c | ||
|
|
75f9a697cb | ||
|
|
89f87dc9ad | ||
|
|
85f45f390c | ||
|
|
10317671ee | ||
|
|
b16f15e47d | ||
|
|
2554b26110 | ||
|
|
a092a197cf | ||
|
|
e069d823a4 | ||
|
|
ca3d56def8 | ||
|
|
9af3772087 | ||
|
|
51525de679 | ||
|
|
969206f905 | ||
|
|
302cab8ca0 | ||
|
|
55a9c1e6b4 | ||
|
|
4426be5f39 | ||
|
|
0ded5c72e9 | ||
|
|
1517611c9e | ||
|
|
8be4300ff4 | ||
|
|
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 | ||
|
|
9e07618a26 | ||
|
|
46ef214bd3 | ||
|
|
5c2d8c4b62 | ||
|
|
1ceb1b4014 | ||
|
|
afa592aa78 | ||
|
|
8a7d4c4fcc | ||
|
|
adb7e97a91 | ||
|
|
43e3cb8f10 | ||
|
|
333ef98378 | ||
|
|
5941bfac7c | ||
|
|
6d4623f8bf | ||
|
|
e358dce594 | ||
|
|
91aa95d3f2 | ||
|
|
3fcad35492 | ||
|
|
4694f986c0 | ||
|
|
841738c34c | ||
|
|
f27c41b83c | ||
|
|
071942faca | ||
|
|
9bbcb6a073 | ||
|
|
48bfc2699b | ||
|
|
ad214a8e76 | ||
|
|
d0652d0583 | ||
|
|
0c6ee2fe99 | ||
|
|
c29b12688c | ||
|
|
ccfa7aff5f | ||
|
|
0b6b85b2a5 | ||
|
|
a6a0b3e931 | ||
|
|
06f8926964 | ||
|
|
ef10ab8088 | ||
|
|
1a39727e38 | ||
|
|
bb7f1a57f0 | ||
|
|
c904d0f0c0 | ||
|
|
64d8d53987 | ||
|
|
133984ab36 | ||
|
|
bcbaf25365 | ||
|
|
6c815c1132 | ||
|
|
b32d1ef717 | ||
|
|
94d12dfbd4 | ||
|
|
6a22cd54ec | ||
|
|
480428b4a2 | ||
|
|
5650b7cf3a | ||
|
|
a808cdc195 | ||
|
|
ee5316e6ef |
+18
-1
@@ -1,12 +1,27 @@
|
||||
|
||||
# job_environment will setup the environment for any job being executed.
|
||||
job_environment: &job_environment
|
||||
NODE_ENV: test
|
||||
DISABLE_CREATE_MONGO_INDEXES: TRUE
|
||||
|
||||
# job_defaults applies all the defaults for each job.
|
||||
job_defaults: &job_defaults
|
||||
working_directory: ~/coralproject/talk
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
environment:
|
||||
<<: *job_environment
|
||||
|
||||
# create_indexes will create the mongo indexes and wait until they have been
|
||||
# built.
|
||||
create_indexes: &create_indexes
|
||||
run:
|
||||
name: Create the database indexes and wait until they are built
|
||||
command: ./bin/cli db createIndexes
|
||||
|
||||
# integration_environment is the environment that configures the tests.
|
||||
integration_environment: &integration_environment
|
||||
NODE_ENV: test
|
||||
<<: *job_environment
|
||||
CIRCLE_TEST_REPORTS: /tmp/circleci-test-results
|
||||
E2E_MAX_RETRIES: 3
|
||||
|
||||
@@ -25,6 +40,7 @@ integration_job: &integration_job
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/coralproject/talk
|
||||
- <<: *create_indexes
|
||||
- run:
|
||||
name: Setup the database with defaults
|
||||
command: ./bin/cli setup --defaults
|
||||
@@ -117,6 +133,7 @@ jobs:
|
||||
environment:
|
||||
JEST_JUNIT_OUTPUT: /tmp/circleci-test-results/jest/test-results.xml
|
||||
JEST_REPORTER: jest-junit
|
||||
- <<: *create_indexes
|
||||
- run:
|
||||
name: Run the server unit tests
|
||||
command: yarn test:server
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
{
|
||||
"env": {
|
||||
"jest": true
|
||||
},
|
||||
"extends": "@coralproject/eslint-config-talk"
|
||||
}
|
||||
|
||||
+3
-2
@@ -38,6 +38,7 @@ plugins/*
|
||||
!plugins/talk-plugin-google-auth
|
||||
!plugins/talk-plugin-ignore-user
|
||||
!plugins/talk-plugin-like
|
||||
!plugins/talk-plugin-local-auth
|
||||
!plugins/talk-plugin-love
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-mod
|
||||
@@ -50,9 +51,10 @@ plugins/*
|
||||
!plugins/talk-plugin-notifications-digest-hourly
|
||||
!plugins/talk-plugin-offtopic
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-profile-settings
|
||||
!plugins/talk-plugin-profile-data
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-respect
|
||||
!plugins/talk-plugin-rich-text
|
||||
!plugins/talk-plugin-slack-notifications
|
||||
!plugins/talk-plugin-sort-most-downvoted
|
||||
!plugins/talk-plugin-sort-most-liked
|
||||
@@ -66,7 +68,6 @@ plugins/*
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-upvote
|
||||
!plugins/talk-plugin-viewing-options
|
||||
!plugins/talk-plugin-rich-text
|
||||
|
||||
**/node_modules/*
|
||||
yarn-error.log
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
"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",
|
||||
"https://nodesecurity.io/advisories/654"
|
||||
]
|
||||
}
|
||||
|
||||
+2
-1
@@ -7,6 +7,7 @@ ONBUILD ARG TALK_REPLY_COMMENTS_LOAD_DEPTH=3
|
||||
ONBUILD ARG TALK_THREADING_LEVEL=3
|
||||
ONBUILD ARG TALK_DEFAULT_STREAM_TAB=all
|
||||
ONBUILD ARG TALK_DEFAULT_LANG=en
|
||||
ONBUILD ARG TALK_WHITELISTED_LANGUAGES
|
||||
ONBUILD ARG TALK_PLUGINS_JSON
|
||||
ONBUILD ARG TALK_WEBPACK_SOURCE_MAP
|
||||
|
||||
@@ -20,4 +21,4 @@ ONBUILD COPY . /usr/src/app
|
||||
ONBUILD RUN cli plugins reconcile && \
|
||||
yarn && \
|
||||
yarn build && \
|
||||
yarn cache clean
|
||||
yarn cache clean
|
||||
|
||||
@@ -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,8 @@
|
||||
const express = require('express');
|
||||
const morgan = require('morgan');
|
||||
const nunjucks = require('nunjucks');
|
||||
const cons = require('consolidate');
|
||||
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 +15,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 +37,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) {
|
||||
@@ -67,7 +74,26 @@ app.use(
|
||||
// VIEW CONFIGURATION
|
||||
//==============================================================================
|
||||
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
// configure the default views directory.
|
||||
const views = path.join(__dirname, 'views');
|
||||
app.set('views', views);
|
||||
|
||||
// reconfigure nunjucks.
|
||||
cons.requires.nunjucks = nunjucks.configure(views, {
|
||||
autoescape: true,
|
||||
trimBlocks: true,
|
||||
lstripBlocks: true,
|
||||
watch: process.env.NODE_ENV === 'development',
|
||||
});
|
||||
|
||||
// assign the nunjucks engine to .njk files.
|
||||
app.engine('njk', cons.nunjucks);
|
||||
|
||||
// assign the ejs engine to .ejs and .html files.
|
||||
app.engine('ejs', cons.ejs);
|
||||
app.engine('html', cons.ejs);
|
||||
|
||||
// set .ejs as the default extension.
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
//==============================================================================
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+53
-24
@@ -7,7 +7,7 @@
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const parseDuration = require('ms');
|
||||
const Table = require('cli-table');
|
||||
const Table = require('cli-table2');
|
||||
const AssetModel = require('../models/asset');
|
||||
const CommentModel = require('../models/comment');
|
||||
const AssetsService = require('../services/assets');
|
||||
@@ -23,23 +23,33 @@ util.onshutdown([() => mongoose.disconnect()]);
|
||||
/**
|
||||
* Lists all the assets registered in the database.
|
||||
*/
|
||||
async function listAssets() {
|
||||
async function listAssets(opts) {
|
||||
try {
|
||||
let assets = await AssetModel.find({}).sort({ created_at: 1 });
|
||||
|
||||
let table = new Table({
|
||||
head: ['ID', 'Title', 'URL'],
|
||||
});
|
||||
switch (opts.format) {
|
||||
case 'json': {
|
||||
console.log(JSON.stringify(assets, null, 2));
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
let table = new Table({
|
||||
head: ['ID', 'Title', 'URL'],
|
||||
});
|
||||
|
||||
assets.forEach(asset => {
|
||||
table.push([
|
||||
asset.id,
|
||||
asset.title ? asset.title : '',
|
||||
asset.url ? asset.url : '',
|
||||
]);
|
||||
});
|
||||
assets.forEach(asset => {
|
||||
table.push([
|
||||
asset.id,
|
||||
asset.title ? asset.title : '',
|
||||
asset.url ? asset.url : '',
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(table.toString());
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -49,12 +59,13 @@ async function listAssets() {
|
||||
|
||||
async function refreshAssets(ageString) {
|
||||
try {
|
||||
const now = new Date().getTime();
|
||||
const ageMs = parseDuration(ageString);
|
||||
const age = new Date(now - ageMs);
|
||||
const query = AssetModel.find({}, { id: 1 });
|
||||
if (ageString) {
|
||||
// An age was specified, so filter only those assets.
|
||||
const ageMs = parseDuration(ageString);
|
||||
const age = new Date(Date.now() - ageMs);
|
||||
|
||||
let assets = await AssetModel.find(
|
||||
{
|
||||
query.merge({
|
||||
$or: [
|
||||
{
|
||||
scraped: {
|
||||
@@ -65,16 +76,28 @@ async function refreshAssets(ageString) {
|
||||
scraped: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ id: 1 }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Create a graph context.
|
||||
const ctx = Context.forSystem();
|
||||
|
||||
// Load the assets.
|
||||
const cursor = query.cursor();
|
||||
|
||||
// Queue all the assets for scraping.
|
||||
await Promise.all(assets.map(({ id }) => scraper.create(ctx, id)));
|
||||
console.log('Assets were queued to be scraped');
|
||||
const promises = [];
|
||||
|
||||
let asset = await cursor.next();
|
||||
while (asset) {
|
||||
promises.push(scraper.create(ctx, asset.id));
|
||||
asset = await cursor.next();
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
console.log(`${promises.length} Assets were queued to be scraped.`);
|
||||
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -202,11 +225,17 @@ async function rewrite(search, replace, options) {
|
||||
|
||||
program
|
||||
.command('list')
|
||||
.option(
|
||||
'--format <type>',
|
||||
'Specify the output format [table]',
|
||||
/^(table|json)$/i,
|
||||
'table'
|
||||
)
|
||||
.description('list all the assets in the database')
|
||||
.action(listAssets);
|
||||
|
||||
program
|
||||
.command('refresh <age>')
|
||||
.command('refresh [age]')
|
||||
.description('queues the assets that exceed the age requested')
|
||||
.action(refreshAssets);
|
||||
|
||||
|
||||
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) {
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ const util = require('./util');
|
||||
const program = require('commander');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const TokensService = require('../services/tokens');
|
||||
const Table = require('cli-table');
|
||||
const Table = require('cli-table2');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
|
||||
+10
-3
@@ -8,7 +8,7 @@ const util = require('./util');
|
||||
const program = require('commander');
|
||||
const inquirer = require('inquirer');
|
||||
const { stripIndent } = require('common-tags');
|
||||
const Table = require('cli-table');
|
||||
const Table = require('cli-table2');
|
||||
|
||||
// Make things colorful!
|
||||
require('colors');
|
||||
@@ -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);
|
||||
@@ -321,7 +328,7 @@ program
|
||||
.action(searchUsers);
|
||||
|
||||
program
|
||||
.command('set-role <userID> <role>')
|
||||
.command('set-role <userID>')
|
||||
.description('sets the role on a user')
|
||||
.action(setUserRole);
|
||||
|
||||
|
||||
+5
-2
@@ -4,7 +4,8 @@ require('../services/env');
|
||||
const debug = require('debug')('talk:util');
|
||||
const { uniq } = require('lodash');
|
||||
|
||||
const util = (module.exports = {});
|
||||
// Setup the utilities.
|
||||
const util = {};
|
||||
|
||||
/**
|
||||
* Stores an array of functions that should be executed in the event that the
|
||||
@@ -15,7 +16,7 @@ util.toshutdown = [];
|
||||
|
||||
/**
|
||||
* Calls all the shutdown functions and then ends the process.
|
||||
* @param {Number} [defaultCode=0] default return code upon sucesfull shutdown.
|
||||
* @param {Number} [defaultCode=0] default return code upon successful shutdown.
|
||||
*/
|
||||
util.shutdown = (defaultCode = 0, signal = null) => {
|
||||
if (signal) {
|
||||
@@ -63,3 +64,5 @@ process.on('unhandledRejection', err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
module.exports = util;
|
||||
|
||||
@@ -10,6 +10,7 @@ import Configure from 'routes/Configure';
|
||||
import StreamSettings from './routes/Configure/containers/StreamSettings';
|
||||
import ModerationSettings from './routes/Configure/containers/ModerationSettings';
|
||||
import TechSettings from './routes/Configure/containers/TechSettings';
|
||||
import OrganizationSettings from './routes/Configure/containers/OrganizationSettings';
|
||||
|
||||
import { ModerationLayout, Moderation } from 'routes/Moderation';
|
||||
|
||||
@@ -25,6 +26,7 @@ const routes = (
|
||||
<Route path="stream" component={StreamSettings} />
|
||||
<Route path="moderation" component={ModerationSettings} />
|
||||
<Route path="tech" component={TechSettings} />
|
||||
<Route path="organization" component={OrganizationSettings} />
|
||||
<IndexRedirect to="stream" />
|
||||
</Route>
|
||||
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
:global {
|
||||
html, body, #root, #root > div {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: #FAFAFA;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import ToastContainer from './ToastContainer';
|
||||
import './App.css';
|
||||
import 'material-design-lite';
|
||||
|
||||
import AppRouter from '../AppRouter';
|
||||
|
||||
@@ -14,7 +14,8 @@ const ApproveButton = ({ active, minimal, onClick, className, disabled }) => {
|
||||
className={cn(
|
||||
styles.root,
|
||||
{ [styles.minimal]: minimal, [styles.active]: active },
|
||||
className
|
||||
className,
|
||||
'talk-admin-approve-button'
|
||||
)}
|
||||
onClick={onClick}
|
||||
disabled={disabled || active}
|
||||
|
||||
@@ -19,7 +19,7 @@ class BanUserDialog extends React.Component {
|
||||
}
|
||||
|
||||
handleMessageChange = e => {
|
||||
const { value: message } = e;
|
||||
const { target: { value: message } } = e;
|
||||
this.setState({ message });
|
||||
};
|
||||
|
||||
@@ -30,6 +30,12 @@ class BanUserDialog extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
handlePerform = () => {
|
||||
this.props.onPerform({
|
||||
message: this.state.message,
|
||||
});
|
||||
};
|
||||
|
||||
renderStep0() {
|
||||
const { onCancel, username, info } = this.props;
|
||||
|
||||
@@ -63,7 +69,7 @@ class BanUserDialog extends React.Component {
|
||||
}
|
||||
|
||||
renderStep1() {
|
||||
const { onCancel, onPerform } = this.props;
|
||||
const { onCancel } = this.props;
|
||||
const { message } = this.state;
|
||||
|
||||
return (
|
||||
@@ -95,7 +101,7 @@ class BanUserDialog extends React.Component {
|
||||
<Button
|
||||
className={cn('talk-ban-user-dialog-button-confirm')}
|
||||
cStyle="black"
|
||||
onClick={onPerform}
|
||||
onClick={this.handlePerform}
|
||||
raised
|
||||
>
|
||||
{t('bandialog.send')}
|
||||
|
||||
@@ -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;
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Label from 'coral-ui/components/Label';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import { t } from 'coral-framework/services/i18n';
|
||||
import FlagLabel from 'coral-ui/components/FlagLabel';
|
||||
import cn from 'classnames';
|
||||
import styles from './CommentLabels.css';
|
||||
@@ -63,10 +64,14 @@ const CommentLabels = ({
|
||||
<FlagLabel iconName="person">{getUserFlaggedType(actions)}</FlagLabel>
|
||||
)}
|
||||
{hasSuspectedWords(actions) && (
|
||||
<FlagLabel iconName="sms_failed">Suspect</FlagLabel>
|
||||
<FlagLabel iconName="sms_failed">
|
||||
{t('flags.reasons.comment.suspect_word')}
|
||||
</FlagLabel>
|
||||
)}
|
||||
{hasHistoryFlag(actions) && (
|
||||
<FlagLabel iconName="sentiment_very_dissatisfied">History</FlagLabel>
|
||||
<FlagLabel iconName="sentiment_very_dissatisfied">
|
||||
{t('flags.reasons.comment.trust')}
|
||||
</FlagLabel>
|
||||
)}
|
||||
</div>
|
||||
<Slot
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
.external {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.separator h5 {
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.slot > * {
|
||||
margin-bottom: 8px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './External.css';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty';
|
||||
|
||||
const External = ({ slot }) => (
|
||||
<IfSlotIsNotEmpty slot={slot}>
|
||||
<div>
|
||||
<div className={styles.external}>
|
||||
<Slot fill={slot} className={styles.slot} />
|
||||
</div>
|
||||
<div className={styles.separator}>
|
||||
<h5>Or</h5>
|
||||
</div>
|
||||
</div>
|
||||
</IfSlotIsNotEmpty>
|
||||
);
|
||||
|
||||
External.propTypes = {
|
||||
slot: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default External;
|
||||
@@ -7,7 +7,6 @@ import styles from './Header.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { Logo } from './Logo';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import ModerationIndicator from '../routes/Moderation/containers/Indicator';
|
||||
import CommunityIndicator from '../routes/Community/containers/Indicator';
|
||||
|
||||
const CoralHeader = ({
|
||||
@@ -32,7 +31,6 @@ const CoralHeader = ({
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
{t('configure.moderate')}
|
||||
<ModerationIndicator root={root} data={data} />
|
||||
</IndexLink>
|
||||
)}
|
||||
<Link
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
.indicator {
|
||||
display: inline-block;
|
||||
background-color: #E46D59;
|
||||
border-radius: 10px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin-top: -4px;
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
.karmaTooltip {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 2px 4px 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 16px;
|
||||
color: #0D5B8F;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color:rgba(0,0,0,0);
|
||||
|
||||
> i {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
}
|
||||
|
||||
.icon:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu {
|
||||
background-color: white;
|
||||
border: solid 1px #999;
|
||||
border-radius: 3px;
|
||||
padding: 10px;
|
||||
position: absolute;
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
|
||||
z-index: 10;
|
||||
top: 32px;
|
||||
left: -100px;
|
||||
width: 150px;
|
||||
text-align: left;
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
.menu::before{
|
||||
content: '';
|
||||
border: 10px solid transparent;
|
||||
border-top-color: #999;
|
||||
position: absolute;
|
||||
left: 96px;
|
||||
top: -20px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.menu::after{
|
||||
content: '';
|
||||
border: 10px solid transparent;
|
||||
border-top-color: white;
|
||||
position: absolute;
|
||||
left: 96px;
|
||||
top: -19px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.menu ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 4px 5px;
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
font-size: .9em;
|
||||
line-height: normal;
|
||||
letter-spacing: .4px;
|
||||
min-width: 25px;
|
||||
display: block;
|
||||
|
||||
/* &.reliable { background-color: #03AB61; } */
|
||||
/* &.neutral { background-color: #616161; } */
|
||||
&.unreliable { background-color: #F44336; }
|
||||
}
|
||||
|
||||
.descriptionList {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.strongItem {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.descriptionItem {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #2B7EB5;
|
||||
text-decoration: underline;
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import { Icon } from 'coral-ui';
|
||||
import styles from './KarmaTooltip.css';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const initialState = { menuVisible: false };
|
||||
|
||||
class KarmaTooltip extends React.Component {
|
||||
static propTypes = {
|
||||
thresholds: PropTypes.shape({
|
||||
reliable: PropTypes.number.isRequired,
|
||||
unreliable: PropTypes.number.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
state = initialState;
|
||||
|
||||
toogleMenu = () => {
|
||||
this.setState({ menuVisible: !this.state.menuVisible });
|
||||
};
|
||||
|
||||
hideMenu = () => {
|
||||
this.setState({ menuVisible: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { thresholds: { unreliable } } = this.props;
|
||||
const { menuVisible } = this.state;
|
||||
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.hideMenu}>
|
||||
<div className={cn(styles.karmaTooltip, 'talk-admin-karma-tooltip')}>
|
||||
<span
|
||||
onClick={this.toogleMenu}
|
||||
className={cn(styles.icon, 'talk-admin-karma-tooltip-icon')}
|
||||
>
|
||||
<Icon name="info" />
|
||||
</span>
|
||||
|
||||
{menuVisible && (
|
||||
<div className={cn(styles.menu, 'talk-admin-karma-tooltip-menu')}>
|
||||
<strong>{t('user_detail.user_karma_score')}</strong>
|
||||
<ul>
|
||||
{/* NOTE: we may display this data in the future, keeping around for that eventuality */}
|
||||
{/* <li>
|
||||
<span>Reliable</span>{' '}
|
||||
<span className={cn(styles.label, styles.reliable)}>
|
||||
≥ {reliable}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>Neutral</span>{' '}
|
||||
<span className={cn(styles.label, styles.neutral)}>
|
||||
< {reliable}, > {unreliable}
|
||||
</span>
|
||||
</li> */}
|
||||
<li>
|
||||
<span>{t('user_detail.unreliable')}</span>{' '}
|
||||
<span className={cn(styles.label, styles.unreliable)}>
|
||||
≤ {unreliable}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<a
|
||||
className={styles.link}
|
||||
href={t('user_detail.karma_docs_link')}
|
||||
target="_blank"
|
||||
>
|
||||
{t('user_detail.learn_more')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default KarmaTooltip;
|
||||
@@ -14,7 +14,8 @@ const RejectButton = ({ active, minimal, onClick, className, disabled }) => {
|
||||
className={cn(
|
||||
styles.root,
|
||||
{ [styles.minimal]: minimal, [styles.active]: active },
|
||||
className
|
||||
className,
|
||||
'talk-admin-reject-button'
|
||||
)}
|
||||
onClick={onClick}
|
||||
disabled={disabled || active}
|
||||
|
||||
@@ -4,6 +4,7 @@ import styles from './SignIn.css';
|
||||
import { Button, TextField, Alert } from 'coral-ui';
|
||||
import cn from 'classnames';
|
||||
import Recaptcha from 'coral-framework/components/Recaptcha';
|
||||
import External from './External';
|
||||
|
||||
class SignIn extends React.Component {
|
||||
recaptcha = null;
|
||||
@@ -33,48 +34,55 @@ class SignIn extends React.Component {
|
||||
render() {
|
||||
const { email, password, errorMessage, requireRecaptcha } = this.props;
|
||||
return (
|
||||
<form className="talk-admin-login-sign-in" onSubmit={this.handleSubmit}>
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
id="email"
|
||||
label="Email Address"
|
||||
value={email}
|
||||
onChange={this.handleEmailChange}
|
||||
/>
|
||||
<TextField
|
||||
id="password"
|
||||
label="Password"
|
||||
value={password}
|
||||
onChange={this.handlePasswordChange}
|
||||
type="password"
|
||||
/>
|
||||
{requireRecaptcha && (
|
||||
<div className={styles.recaptcha}>
|
||||
<Recaptcha
|
||||
ref={this.handleRecaptchaRef}
|
||||
onVerify={this.props.onRecaptchaVerify}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
className={cn(styles.signInButton, 'talk-admin-login-sign-in-button')}
|
||||
type="submit"
|
||||
cStyle="black"
|
||||
full
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
<p className={styles.forgotPasswordCTA}>
|
||||
Forgot your password?{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.forgotPasswordLink}
|
||||
onClick={this.handleForgotPasswordLink}
|
||||
<div className="talk-admin-login-sign-in">
|
||||
<External slot="authExternalAdminSignIn" />
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
id="email"
|
||||
label="Email Address"
|
||||
value={email}
|
||||
onChange={this.handleEmailChange}
|
||||
/>
|
||||
<TextField
|
||||
id="password"
|
||||
label="Password"
|
||||
value={password}
|
||||
onChange={this.handlePasswordChange}
|
||||
type="password"
|
||||
/>
|
||||
{requireRecaptcha && (
|
||||
<div className={styles.recaptcha}>
|
||||
<Recaptcha
|
||||
ref={this.handleRecaptchaRef}
|
||||
onVerify={this.props.onRecaptchaVerify}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
className={cn(
|
||||
styles.signInButton,
|
||||
'talk-admin-login-sign-in-button'
|
||||
)}
|
||||
type="submit"
|
||||
cStyle="black"
|
||||
full
|
||||
>
|
||||
Request a new one.
|
||||
</a>
|
||||
</p>
|
||||
</form>
|
||||
Sign In
|
||||
</Button>
|
||||
<p className={styles.forgotPasswordCTA}>
|
||||
{/* TODO: translate */}
|
||||
Forgot your password?{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.forgotPasswordLink}
|
||||
onClick={this.handleForgotPasswordLink}
|
||||
>
|
||||
Request a new one.
|
||||
</a>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,44 +35,49 @@
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.karmaStat {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.stat:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.statItem,
|
||||
.statReportResult {
|
||||
.statItem, .statReportResult, .statKarmaResult {
|
||||
padding: 3px 5px;
|
||||
background-color: #D8D8D8;
|
||||
border-radius: 3px;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
font-size: 0.9em;
|
||||
line-height: normal;
|
||||
letter-spacing: 0.4px;
|
||||
min-width: 60px;
|
||||
min-width: 25px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.statResult {
|
||||
font-size: 1.5em;
|
||||
padding: 5px 0;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.statReportResult {
|
||||
.statReportResult, .statKarmaResult {
|
||||
color: white;
|
||||
margin: 5px 0;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.statReportResult.reliable {
|
||||
background-color: #749C48;
|
||||
.statReportResult.reliable, .statKarmaResult.good {
|
||||
background-color: #03AB61;
|
||||
}
|
||||
|
||||
.statReportResult.neutral {
|
||||
.statReportResult.neutral, .statKarmaResult.neutral {
|
||||
background-color: #616161;
|
||||
}
|
||||
|
||||
.statReportResult.unreliable {
|
||||
.statReportResult.unreliable, .statKarmaResult.bad {
|
||||
background-color: #F44336;
|
||||
}
|
||||
|
||||
@@ -143,3 +148,7 @@
|
||||
border-color: #E45241;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.userDetailItem {
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
@@ -3,14 +3,10 @@ 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 {
|
||||
getReliability,
|
||||
isSuspended,
|
||||
isBanned,
|
||||
} from 'coral-framework/utils/user';
|
||||
import { isSuspended, isBanned, getKarma } from 'coral-framework/utils/user';
|
||||
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
import {
|
||||
@@ -25,29 +21,10 @@ import {
|
||||
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
|
||||
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
import UserInfoTooltip from './UserInfoTooltip';
|
||||
import KarmaTooltip from './KarmaTooltip';
|
||||
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);
|
||||
};
|
||||
@@ -99,7 +76,13 @@ class UserDetail extends React.Component {
|
||||
renderLoaded() {
|
||||
const {
|
||||
root,
|
||||
root: { me, user, totalComments, rejectedComments },
|
||||
root: {
|
||||
me,
|
||||
user,
|
||||
totalComments,
|
||||
rejectedComments,
|
||||
settings: { karmaThresholds },
|
||||
},
|
||||
activeTab,
|
||||
selectedCommentIds,
|
||||
toggleSelect,
|
||||
@@ -110,6 +93,10 @@ class UserDetail extends React.Component {
|
||||
unbanUser,
|
||||
unsuspendUser,
|
||||
modal,
|
||||
acceptComment,
|
||||
rejectComment,
|
||||
bulkAccept,
|
||||
bulkReject,
|
||||
} = this.props;
|
||||
|
||||
// if totalComments is 0, you're dividing by zero
|
||||
@@ -193,7 +180,7 @@ class UserDetail extends React.Component {
|
||||
|
||||
<div>
|
||||
<ul className={styles.userDetailList}>
|
||||
<li>
|
||||
<li className={styles.userDetailItem}>
|
||||
<Icon name="assignment_ind" />
|
||||
<span className={styles.userDetailItem}>
|
||||
{t('user_detail.member_since')}:
|
||||
@@ -201,11 +188,24 @@ class UserDetail extends React.Component {
|
||||
{new Date(user.created_at).toLocaleString()}
|
||||
</li>
|
||||
|
||||
{user.profiles.map(({ id }) => (
|
||||
<li key={id}>
|
||||
<Icon name="email" />
|
||||
<li className={styles.userDetailItem}>
|
||||
<Icon name="email" />
|
||||
<span className={styles.userDetailItem}>
|
||||
{t('user_detail.email')}:
|
||||
</span>
|
||||
{user.email}{' '}
|
||||
<ButtonCopyToClipboard
|
||||
className={styles.copyButton}
|
||||
icon="content_copy"
|
||||
copyText={user.email}
|
||||
/>
|
||||
</li>
|
||||
|
||||
{user.profiles.map(({ provider, id }) => (
|
||||
<li key={id} className={styles.userDetailItem}>
|
||||
<Icon name="device_hub" />
|
||||
<span className={styles.userDetailItem}>
|
||||
{t('user_detail.email')}:
|
||||
{capitalize(provider)} {t('user_detail.id')}:
|
||||
</span>
|
||||
{id}{' '}
|
||||
<ButtonCopyToClipboard
|
||||
@@ -232,18 +232,21 @@ class UserDetail extends React.Component {
|
||||
{rejectedPercent.toFixed(1)}%
|
||||
</span>
|
||||
</li>
|
||||
<li className={styles.stat}>
|
||||
<span className={styles.statItem}>
|
||||
{t('user_detail.reports')}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
styles.statReportResult,
|
||||
styles[getReliability(user.reliable.flagger)]
|
||||
)}
|
||||
>
|
||||
{capitalize(getReliability(user.reliable.flagger))}
|
||||
</span>
|
||||
<li className={cn(styles.stat, styles.karmaStat)}>
|
||||
<div>
|
||||
<span className={styles.statItem}>
|
||||
{t('user_detail.karma')}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
styles.statKarmaResult,
|
||||
styles[getKarma(user.reliable.commenter)]
|
||||
)}
|
||||
>
|
||||
{user.reliable.commenterKarma}
|
||||
</span>
|
||||
</div>
|
||||
<KarmaTooltip thresholds={karmaThresholds.comment} />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -286,7 +289,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 +307,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 +325,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;
|
||||
@@ -12,7 +12,7 @@ import { compose } from 'react-apollo';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class BanUserDialogContainer extends Component {
|
||||
banUser = async () => {
|
||||
banUser = async ({ message }) => {
|
||||
const {
|
||||
userId,
|
||||
commentId,
|
||||
@@ -21,7 +21,7 @@ class BanUserDialogContainer extends Component {
|
||||
setCommentStatus,
|
||||
hideBanUserDialog,
|
||||
} = this.props;
|
||||
await banUser({ id: userId, message: '' });
|
||||
await banUser({ id: userId, message });
|
||||
hideBanUserDialog();
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
await setCommentStatus({ commentId, status: 'REJECTED' });
|
||||
|
||||
@@ -2,21 +2,20 @@ import { gql } from 'react-apollo';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import Header from '../components/Header';
|
||||
import CommunityIndicator from '../routes/Community/containers/Indicator';
|
||||
import ModerationIndicator from '../routes/Moderation/containers/Indicator';
|
||||
// TODO: eventually we will readd modqueue counts
|
||||
// import ModerationIndicator from '../routes/Moderation/containers/Indicator';
|
||||
import { getDefinitionName } from 'coral-framework/utils';
|
||||
|
||||
export default withQuery(
|
||||
gql`
|
||||
query TalkAdmin_Header($nullID: ID) {
|
||||
...${getDefinitionName(ModerationIndicator.fragments.root)}
|
||||
query TalkAdmin_Header {
|
||||
...${getDefinitionName(CommunityIndicator.fragments.root)}
|
||||
}
|
||||
${ModerationIndicator.fragments.root}
|
||||
${CommunityIndicator.fragments.root}
|
||||
`,
|
||||
{
|
||||
options: {
|
||||
variables: { nullID: null },
|
||||
// variables: { nullID: null },
|
||||
},
|
||||
}
|
||||
)(Header);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withSignIn } from 'coral-framework/hocs';
|
||||
import { withSignIn, withPopupAuthHandler } from 'coral-framework/hocs';
|
||||
import { compose } from 'recompose';
|
||||
import SignIn from '../components/SignIn';
|
||||
|
||||
@@ -55,4 +55,4 @@ SignInContainer.propTypes = {
|
||||
requireRecaptcha: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default compose(withSignIn)(SignInContainer);
|
||||
export default compose(withSignIn, withPopupAuthHandler)(SignInContainer);
|
||||
|
||||
@@ -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`
|
||||
@@ -178,12 +179,14 @@ export const withUserDetailQuery = withQuery(
|
||||
id
|
||||
username
|
||||
created_at
|
||||
email
|
||||
profiles {
|
||||
id
|
||||
provider
|
||||
}
|
||||
reliable {
|
||||
flagger
|
||||
commenter
|
||||
commenterKarma
|
||||
}
|
||||
state {
|
||||
status {
|
||||
@@ -224,6 +227,14 @@ export const withUserDetailQuery = withQuery(
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'user')}
|
||||
}
|
||||
settings {
|
||||
karmaThresholds {
|
||||
comment {
|
||||
reliable
|
||||
unreliable
|
||||
}
|
||||
}
|
||||
}
|
||||
me {
|
||||
id
|
||||
}
|
||||
@@ -245,7 +256,6 @@ export const withUserDetailQuery = withQuery(
|
||||
options: ({ userId, statuses }) => {
|
||||
return {
|
||||
variables: { author_id: userId, statuses },
|
||||
fetchPolicy: 'network-only',
|
||||
};
|
||||
},
|
||||
skip: ownProps => !ownProps.userId,
|
||||
|
||||
@@ -24,6 +24,25 @@ const userRoleFragment = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* calculateReliability will determine the reliability of a karma score based on
|
||||
* the settings for the karma type.
|
||||
*
|
||||
* @param {Number} karma - the current karma value/score for the given user
|
||||
* @param {Object} thresholds - the karma thresholds to base the karma computation on
|
||||
*/
|
||||
const calculateReliability = (karma, { reliable, unreliable }) => {
|
||||
if (karma >= reliable) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (karma <= unreliable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default {
|
||||
mutations: {
|
||||
SetUserRole: ({ variables: { id, role } }) => ({
|
||||
@@ -156,7 +175,9 @@ export default {
|
||||
}
|
||||
const updated = update(prev, {
|
||||
users: {
|
||||
nodes: { $apply: nodes => nodes.filter(node => node.id !== id) },
|
||||
nodes: {
|
||||
$apply: nodes => nodes.filter(node => node.id !== id),
|
||||
},
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
@@ -185,7 +206,9 @@ export default {
|
||||
const updated = update(prev, {
|
||||
...decrement,
|
||||
flaggedUsers: {
|
||||
nodes: { $apply: nodes => nodes.filter(node => node.id !== id) },
|
||||
nodes: {
|
||||
$apply: nodes => nodes.filter(node => node.id !== id),
|
||||
},
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
@@ -291,5 +314,62 @@ export default {
|
||||
},
|
||||
},
|
||||
}),
|
||||
SetCommentStatus: ({ variables: { status } }) => ({
|
||||
updateQueries: {
|
||||
CoralAdmin_UserDetail: prev => {
|
||||
const increment = {
|
||||
user: {
|
||||
reliable: {
|
||||
commenter: {
|
||||
$set: calculateReliability(
|
||||
prev.user.reliable.commenterKarma - 1,
|
||||
prev.settings.karmaThresholds.comment
|
||||
),
|
||||
},
|
||||
commenterKarma: {
|
||||
$apply: count => count - 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
rejectedComments: {
|
||||
$apply: count => (count < prev.totalComments ? count + 1 : count),
|
||||
},
|
||||
};
|
||||
|
||||
const decrement = {
|
||||
user: {
|
||||
reliable: {
|
||||
commenter: {
|
||||
$set: calculateReliability(
|
||||
prev.user.reliable.commenterKarma + 1,
|
||||
prev.settings.karmaThresholds.comment
|
||||
),
|
||||
},
|
||||
commenterKarma: {
|
||||
$apply: count => count + 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -133,6 +133,7 @@ th.header:nth-child(2), th.header:nth-child(3) {
|
||||
|
||||
.roleDropdown {
|
||||
width: 150px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.roleOption {
|
||||
|
||||
@@ -130,7 +130,9 @@ class People extends React.Component {
|
||||
{user.username}
|
||||
</button>
|
||||
<span className={styles.email}>
|
||||
{user.profiles.map(({ id }) => id)}
|
||||
{user.email
|
||||
? user.email
|
||||
: user.profiles.map(p => p.id).join(', ')}
|
||||
</span>
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
@@ -200,7 +202,7 @@ class People extends React.Component {
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<Dropdown
|
||||
className={cn(
|
||||
toggleClassName={cn(
|
||||
'talk-admin-community-people-dd-role',
|
||||
styles.roleDropdown
|
||||
)}
|
||||
|
||||
@@ -8,7 +8,14 @@ import SaveChangesDialog from './SaveChangesDialog';
|
||||
|
||||
class Configure extends React.Component {
|
||||
render() {
|
||||
const { canSave, currentUser, root, savePending, settings } = this.props;
|
||||
const {
|
||||
canSave,
|
||||
currentUser,
|
||||
root,
|
||||
savePending,
|
||||
settings,
|
||||
clearPending,
|
||||
} = this.props;
|
||||
|
||||
if (!can(currentUser, 'UPDATE_CONFIG')) {
|
||||
return <p>{t('configure.access_message')}</p>;
|
||||
@@ -17,6 +24,9 @@ class Configure extends React.Component {
|
||||
const passProps = {
|
||||
root,
|
||||
settings,
|
||||
savePending,
|
||||
clearPending,
|
||||
canSave,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -41,6 +51,9 @@ class Configure extends React.Component {
|
||||
<Item itemId="tech" icon="code">
|
||||
{t('configure.tech_settings')}
|
||||
</Item>
|
||||
<Item itemId="organization" icon="people">
|
||||
{t('configure.organization_information')}
|
||||
</Item>
|
||||
</List>
|
||||
<div className={styles.saveBox}>
|
||||
{canSave ? (
|
||||
@@ -81,6 +94,7 @@ Configure.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
saveDialog: PropTypes.bool,
|
||||
hideSaveDialog: PropTypes.func.isRequired,
|
||||
clearPending: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Configure;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
.label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.detailList {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.detailLabel {
|
||||
color: #000;
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.detailValue {
|
||||
padding: 6px 0;
|
||||
border: solid 1px transparent;
|
||||
display: block;
|
||||
font-size: 1.1em;
|
||||
border-radius: 2px;
|
||||
color: #424242;
|
||||
box-sizing: border-box;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.editable {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
border-color: grey;
|
||||
}
|
||||
|
||||
.detailItem {
|
||||
margin-bottom: 16px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.button, .button:disabled {
|
||||
background: white;
|
||||
border: solid 1px grey;
|
||||
}
|
||||
|
||||
.actionBox {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.cancelButton {
|
||||
padding: 10px;
|
||||
display: block;
|
||||
color: #4f5c67;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.changedSave {
|
||||
background-color: #00796B;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.errorList {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.errorItem {
|
||||
padding: 5px 10px;
|
||||
margin-bottom: 20px;
|
||||
color: #b71c1c;
|
||||
border-radius: 2px;
|
||||
display: inline-block;
|
||||
background: #F9D3CE;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex-grow: 1;
|
||||
padding-right: 40px;
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Button } from 'coral-ui';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './OrganizationSettings.css';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import ConfigurePage from './ConfigurePage';
|
||||
import ConfigureCard from 'coral-framework/components/ConfigureCard';
|
||||
import validate from 'coral-framework/helpers/validate';
|
||||
import errorMsj from 'coral-framework/helpers/error';
|
||||
|
||||
class OrganizationSettings extends React.Component {
|
||||
state = { editing: false, errors: [] };
|
||||
|
||||
addError = err => {
|
||||
if (this.state.errors.indexOf(err) === -1) {
|
||||
this.setState(({ errors }) => ({
|
||||
errors: errors.concat(err),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
removeError = err => {
|
||||
this.setState(({ errors }) => ({
|
||||
errors: errors.filter(i => i !== err),
|
||||
}));
|
||||
};
|
||||
|
||||
toggleEditing = () => {
|
||||
this.setState(({ editing }) => ({
|
||||
editing: !editing,
|
||||
}));
|
||||
};
|
||||
|
||||
disableEditing = () => {
|
||||
this.setState(() => ({
|
||||
editing: false,
|
||||
}));
|
||||
};
|
||||
|
||||
updateName = event => {
|
||||
const updater = { organizationName: { $set: event.target.value } };
|
||||
this.props.updatePending({ updater });
|
||||
};
|
||||
|
||||
updateEmail = event => {
|
||||
let error = null;
|
||||
const email = event.target.value;
|
||||
|
||||
// Add a blocker error
|
||||
if (!validate.email(email)) {
|
||||
error = true;
|
||||
this.addError('email');
|
||||
} else {
|
||||
this.removeError('email');
|
||||
}
|
||||
|
||||
const updater = { organizationContactEmail: { $set: email } };
|
||||
const errorUpdater = { organizationContactEmail: { $set: error } };
|
||||
|
||||
this.props.updatePending({ updater, errorUpdater });
|
||||
};
|
||||
|
||||
cancelEditing = () => {
|
||||
this.disableEditing();
|
||||
this.props.clearPending();
|
||||
};
|
||||
|
||||
save = async () => {
|
||||
await this.props.savePending();
|
||||
this.disableEditing();
|
||||
};
|
||||
displayErrors = (errors = []) => (
|
||||
<ul className={styles.errorList}>
|
||||
{errors.map((errKey, i) => (
|
||||
<li key={`${i}_${errKey}`} className={styles.errorItem}>
|
||||
{errorMsj[errKey]}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
||||
render() {
|
||||
const { settings, slotPassthrough, canSave } = this.props;
|
||||
const hasErrors = this.state.errors.length;
|
||||
return (
|
||||
<ConfigurePage title={t('configure.organization_information')}>
|
||||
<p>{t('configure.organization_info_copy')}</p>
|
||||
<p>{t('configure.organization_info_copy_2')}</p>
|
||||
<ConfigureCard>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
{this.displayErrors(this.state.errors)}
|
||||
<ul className={styles.detailList}>
|
||||
<li className={styles.detailItem}>
|
||||
<label
|
||||
className={styles.detailLabel}
|
||||
id={t('configure.organization_name')}
|
||||
>
|
||||
{t('configure.organization_name')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className={cn(styles.detailValue, {
|
||||
[styles.editable]: this.state.editing,
|
||||
})}
|
||||
onChange={this.updateName}
|
||||
value={settings.organizationName}
|
||||
id={t('configure.organization_name')}
|
||||
readOnly={!this.state.editing}
|
||||
/>
|
||||
</li>
|
||||
<li className={styles.detailItem}>
|
||||
<label
|
||||
className={styles.detailLabel}
|
||||
id={t('configure.organization_contact_email')}
|
||||
>
|
||||
{t('configure.organization_contact_email')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className={cn(styles.detailValue, {
|
||||
[styles.editable]: this.state.editing,
|
||||
})}
|
||||
onChange={this.updateEmail}
|
||||
value={settings.organizationContactEmail || ''}
|
||||
id={t('configure.organization_contact_email')}
|
||||
readOnly={!this.state.editing}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{!this.state.editing ? (
|
||||
<div className={styles.actionBox}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
icon="settings"
|
||||
onClick={this.toggleEditing}
|
||||
full
|
||||
>
|
||||
{t('configure.edit_info')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.actionBox}>
|
||||
{canSave && !hasErrors ? (
|
||||
<Button
|
||||
raised
|
||||
onClick={this.save}
|
||||
className={styles.changedSave}
|
||||
icon="check"
|
||||
full
|
||||
>
|
||||
{t('configure.save')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button className={styles.button} disabled icon="check" full>
|
||||
{t('configure.save')}
|
||||
</Button>
|
||||
)}
|
||||
<a className={styles.cancelButton} onClick={this.cancelEditing}>
|
||||
{t('cancel')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ConfigureCard>
|
||||
<Slot fill="adminOrganizationSettings" passthrough={slotPassthrough} />
|
||||
</ConfigurePage>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
OrganizationSettings.propTypes = {
|
||||
savePending: PropTypes.func.isRequired,
|
||||
clearPending: PropTypes.func.isRequired,
|
||||
updatePending: PropTypes.func.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
slotPassthrough: PropTypes.object.isRequired,
|
||||
canSave: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default OrganizationSettings;
|
||||
@@ -82,6 +82,20 @@ class StreamSettings extends React.Component {
|
||||
this.props.updatePending({ updater });
|
||||
};
|
||||
|
||||
updateDisableCommenting = () => {
|
||||
const updater = {
|
||||
disableCommenting: {
|
||||
$set: !this.props.settings.disableCommenting,
|
||||
},
|
||||
};
|
||||
this.props.updatePending({ updater });
|
||||
};
|
||||
|
||||
updateDisableCommentingMessage = value => {
|
||||
const updater = { disableCommentingMessage: { $set: value } };
|
||||
this.props.updatePending({ updater });
|
||||
};
|
||||
|
||||
updateAutoClose = () => {
|
||||
const updater = {
|
||||
autoCloseStream: { $set: !this.props.settings.autoCloseStream },
|
||||
@@ -192,6 +206,25 @@ class StreamSettings extends React.Component {
|
||||
|
||||
{t('configure.edit_comment_timeframe_text_post')}
|
||||
</ConfigureCard>
|
||||
<ConfigureCard
|
||||
checked={settings.disableCommenting}
|
||||
onCheckbox={this.updateDisableCommenting}
|
||||
title={t('configure.disable_commenting_title')}
|
||||
>
|
||||
<p>{t('configure.disable_commenting_desc')}</p>
|
||||
<div
|
||||
className={cn(
|
||||
styles.configSettingDisableCommenting,
|
||||
settings.disableCommenting ? null : styles.hidden
|
||||
)}
|
||||
>
|
||||
<MarkdownEditor
|
||||
className={styles.descriptionBox}
|
||||
onChange={this.updateDisableCommentingMessage}
|
||||
value={settings.disableCommentingMessage}
|
||||
/>
|
||||
</div>
|
||||
</ConfigureCard>
|
||||
<ConfigureCard
|
||||
checked={settings.autoCloseStream}
|
||||
onCheckbox={this.updateAutoClose}
|
||||
|
||||
@@ -16,10 +16,12 @@ import {
|
||||
hideSaveDialog,
|
||||
} from '../../../actions/configure';
|
||||
import Configure from '../components/Configure';
|
||||
import OrganizationSettings from './OrganizationSettings';
|
||||
import { withRouter } from 'react-router';
|
||||
|
||||
class ConfigureContainer extends React.Component {
|
||||
state = { nextRoute: '' };
|
||||
nextRoute = '';
|
||||
unregisterLeaveHook = null;
|
||||
|
||||
savePending = async () => {
|
||||
await this.props.updateSettings(this.props.pending);
|
||||
@@ -39,18 +41,16 @@ class ConfigureContainer extends React.Component {
|
||||
};
|
||||
|
||||
gotoNextRoute = () => {
|
||||
const { nextRoute } = this.state;
|
||||
if (nextRoute) {
|
||||
this.props.router.push(nextRoute);
|
||||
this.setState({ nextRoute: '' });
|
||||
if (this.nextRoute) {
|
||||
this.props.router.push(this.nextRoute);
|
||||
this.nextRoute = '';
|
||||
}
|
||||
};
|
||||
|
||||
handleSectionChange = async section => {
|
||||
const nextRoute = `/admin/configure/${section}`;
|
||||
|
||||
if (this.shouldShowSaveDialog()) {
|
||||
await this.setState({ nextRoute });
|
||||
if (this.hasPendingData()) {
|
||||
this.nextRoute = nextRoute;
|
||||
this.props.showSaveDialog();
|
||||
} else {
|
||||
// Just go to the section
|
||||
@@ -58,20 +58,37 @@ class ConfigureContainer extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
shouldShowSaveDialog = () => {
|
||||
navigationPrompt = e => {
|
||||
if (this.hasPendingData()) {
|
||||
const confirmationMessage = 'Changes that you made may not be saved.';
|
||||
e.returnValue = confirmationMessage; // Gecko, Trident, Chrome 34+
|
||||
return confirmationMessage; // Gecko, WebKit, Chrome <34
|
||||
}
|
||||
};
|
||||
|
||||
hasPendingData = () => {
|
||||
return !!Object.keys(this.props.pending).length;
|
||||
};
|
||||
|
||||
routeLeave = ({ pathname }) => {
|
||||
if (this.shouldShowSaveDialog()) {
|
||||
this.setState({ nextRoute: pathname });
|
||||
if (this.hasPendingData()) {
|
||||
this.nextRoute = pathname;
|
||||
this.props.showSaveDialog();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.router.setRouteLeaveHook(this.props.route, this.routeLeave);
|
||||
this.unregisterLeaveHook = this.props.router.setRouteLeaveHook(
|
||||
this.props.route,
|
||||
this.routeLeave
|
||||
);
|
||||
window.addEventListener('beforeunload', this.navigationPrompt);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unregisterLeaveHook();
|
||||
window.removeEventListener('beforeunload', this.navigationPrompt);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -83,18 +100,21 @@ class ConfigureContainer extends React.Component {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const activeSection = this.props.routes[3].path;
|
||||
|
||||
return (
|
||||
<Configure
|
||||
saveChanges={this.saveChanges}
|
||||
discardChanges={this.discardChanges}
|
||||
saveDialog={this.props.saveDialog}
|
||||
activeSection={this.props.routes[3].path}
|
||||
activeSection={activeSection}
|
||||
hideSaveDialog={this.props.hideSaveDialog}
|
||||
canSave={this.props.canSave}
|
||||
currentUser={this.props.currentUser}
|
||||
root={this.props.root}
|
||||
settings={this.props.mergedSettings}
|
||||
handleSectionChange={this.handleSectionChange}
|
||||
clearPending={this.props.clearPending}
|
||||
savePending={this.savePending}
|
||||
>
|
||||
{this.props.children}
|
||||
@@ -110,10 +130,12 @@ const withConfigureQuery = withQuery(
|
||||
...${getDefinitionName(StreamSettings.fragments.settings)}
|
||||
...${getDefinitionName(TechSettings.fragments.settings)}
|
||||
...${getDefinitionName(ModerationSettings.fragments.settings)}
|
||||
...${getDefinitionName(OrganizationSettings.fragments.settings)}
|
||||
}
|
||||
...${getDefinitionName(StreamSettings.fragments.root)}
|
||||
...${getDefinitionName(TechSettings.fragments.root)}
|
||||
...${getDefinitionName(ModerationSettings.fragments.root)}
|
||||
...${getDefinitionName(OrganizationSettings.fragments.root)}
|
||||
}
|
||||
${StreamSettings.fragments.root}
|
||||
${StreamSettings.fragments.settings}
|
||||
@@ -121,6 +143,8 @@ const withConfigureQuery = withQuery(
|
||||
${TechSettings.fragments.settings}
|
||||
${ModerationSettings.fragments.root}
|
||||
${ModerationSettings.fragments.settings}
|
||||
${OrganizationSettings.fragments.root}
|
||||
${OrganizationSettings.fragments.settings}
|
||||
`,
|
||||
{
|
||||
options: () => ({
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import OrganizationSettings from '../components/OrganizationSettings';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
import { getSlotFragmentSpreads } from 'coral-framework/utils';
|
||||
import { updatePending } from '../../../actions/configure';
|
||||
import { mapProps } from 'recompose';
|
||||
|
||||
const slots = ['adminOrganizationSettings'];
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
errors: state.configure.errors,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
updatePending,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
export default compose(
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment TalkAdmin_OrganizationSettings_root on RootQuery {
|
||||
__typename
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
}
|
||||
`,
|
||||
settings: gql`
|
||||
fragment TalkAdmin_OrganizationSettings_settings on Settings {
|
||||
organizationName
|
||||
organizationContactEmail
|
||||
${getSlotFragmentSpreads(slots, 'settings')}
|
||||
}
|
||||
`,
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
|
||||
slotPassthrough: {
|
||||
root,
|
||||
settings,
|
||||
updatePending,
|
||||
errors,
|
||||
},
|
||||
updatePending,
|
||||
settings,
|
||||
errors,
|
||||
...rest,
|
||||
}))
|
||||
)(OrganizationSettings);
|
||||
@@ -39,6 +39,8 @@ export default compose(
|
||||
autoCloseStream
|
||||
closedTimeout
|
||||
closedMessage
|
||||
disableCommenting
|
||||
disableCommentingMessage
|
||||
${getSlotFragmentSpreads(slots, 'settings')}
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import styles from './Install.css';
|
||||
import { Wizard, WizardNav } from 'coral-ui';
|
||||
import Layout from 'coral-admin/src/components/Layout';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import InitialStep from './Steps/InitialStep';
|
||||
import AddOrganizationName from './Steps/AddOrganizationName';
|
||||
import OrganizationDetails from './Steps/OrganizationDetails';
|
||||
import CreateYourAccount from './Steps/CreateYourAccount';
|
||||
import PermittedDomainsStep from './Steps/PermittedDomainsStep';
|
||||
import FinalStep from './Steps/FinalStep';
|
||||
|
||||
export default class Install extends Component {
|
||||
class Install extends React.Component {
|
||||
handleDomainsChange = value => {
|
||||
this.props.updatePermittedDomains(value);
|
||||
};
|
||||
@@ -55,7 +56,7 @@ export default class Install extends Component {
|
||||
goToStep={this.props.goToStep}
|
||||
>
|
||||
<InitialStep />
|
||||
<AddOrganizationName
|
||||
<OrganizationDetails
|
||||
install={install}
|
||||
handleSettingsChange={this.handleSettingsChange}
|
||||
handleSettingsSubmit={this.handleSettingsSubmit}
|
||||
@@ -81,3 +82,18 @@ export default class Install extends Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Install.propTypes = {
|
||||
updatePermittedDomains: PropTypes.func.isRequired,
|
||||
updateSettingsFormData: PropTypes.func.isRequired,
|
||||
updateUserFormData: PropTypes.func.isRequired,
|
||||
submitSettings: PropTypes.func.isRequired,
|
||||
submitUser: PropTypes.func.isRequired,
|
||||
install: PropTypes.object.isRequired,
|
||||
nextStep: PropTypes.func.isRequired,
|
||||
previousStep: PropTypes.func.isRequired,
|
||||
goToStep: PropTypes.func.isRequired,
|
||||
finishInstall: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Install;
|
||||
|
||||
+11
@@ -21,6 +21,17 @@ const AddOrganizationName = props => {
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.organizationName}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className={styles.TextField}
|
||||
id="organizationContactEmail"
|
||||
type="email"
|
||||
label={t('install.create.organization_contact_email')}
|
||||
onChange={handleSettingsChange}
|
||||
showErrors={install.showErrors}
|
||||
errorMsg={install.errors.organizationContactEmail}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="talk-install-step-2-save-button"
|
||||
type="submit"
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { compose } from 'react-apollo';
|
||||
import Install from '../components/Install';
|
||||
|
||||
import {
|
||||
@@ -18,7 +17,7 @@ import {
|
||||
updatePermittedDomains,
|
||||
} from '../../../actions/install';
|
||||
|
||||
class InstallContainer extends Component {
|
||||
class InstallContainer extends React.Component {
|
||||
componentDidMount() {
|
||||
const { checkInstall } = this.props;
|
||||
checkInstall(() => {
|
||||
@@ -27,7 +26,21 @@ class InstallContainer extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Install {...this.props} />;
|
||||
return (
|
||||
<Install
|
||||
install={this.props.install}
|
||||
goToStep={this.props.goToStep}
|
||||
nextStep={this.props.nextStep}
|
||||
submitUser={this.props.submitUser}
|
||||
checkInstall={this.props.checkInstall}
|
||||
previousStep={this.props.previousStep}
|
||||
finishInstall={this.props.finishInstall}
|
||||
submitSettings={this.props.submitSettings}
|
||||
updateUserFormData={this.props.updateUserFormData}
|
||||
updateSettingsFormData={this.props.updateSettingsFormData}
|
||||
updatePermittedDomains={this.props.updatePermittedDomains}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +48,20 @@ InstallContainer.contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
InstallContainer.propTypes = {
|
||||
install: PropTypes.object.isRequired,
|
||||
goToStep: PropTypes.func.isRequired,
|
||||
nextStep: PropTypes.func.isRequired,
|
||||
submitUser: PropTypes.func.isRequired,
|
||||
checkInstall: PropTypes.func.isRequired,
|
||||
previousStep: PropTypes.func.isRequired,
|
||||
finishInstall: PropTypes.func.isRequired,
|
||||
submitSettings: PropTypes.func.isRequired,
|
||||
updateUserFormData: PropTypes.func.isRequired,
|
||||
updateSettingsFormData: PropTypes.func.isRequired,
|
||||
updatePermittedDomains: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
install: state.install,
|
||||
});
|
||||
@@ -56,6 +83,4 @@ const mapDispatchToProps = dispatch =>
|
||||
dispatch
|
||||
);
|
||||
|
||||
export default compose(connect(mapStateToProps, mapDispatchToProps))(
|
||||
InstallContainer
|
||||
);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(InstallContainer);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.root {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 18px;
|
||||
@@ -13,7 +12,6 @@
|
||||
margin-top: 13px;
|
||||
min-height: 0;
|
||||
outline: 0;
|
||||
|
||||
/*
|
||||
Fix rendering issues in Safari by promoting this
|
||||
into its own layer.
|
||||
@@ -21,7 +19,6 @@
|
||||
https://www.pivotaltracker.com/story/show/151142211
|
||||
*/
|
||||
transform: translateZ(0);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
@@ -39,7 +36,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
}
|
||||
|
||||
.author {
|
||||
@@ -74,7 +70,7 @@
|
||||
max-width: 500px;
|
||||
font-weight: 300;
|
||||
font-size: 16px;
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.created {
|
||||
@@ -85,13 +81,16 @@
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.deleted {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.moderateArticle {
|
||||
font-size: 14px;
|
||||
margin: 10px 0;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
max-width: 500px;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
color: #063b9a;
|
||||
@@ -99,17 +98,15 @@
|
||||
font-weight: 500;
|
||||
letter-spacing: .5px;
|
||||
margin-left: 10px;
|
||||
|
||||
font-size: 13px;
|
||||
margin-left: 5px;
|
||||
padding-bottom: 0px;
|
||||
border-bottom: solid 1px;
|
||||
line-height: 16px;
|
||||
|
||||
&:hover {
|
||||
opacity: .9;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,12 +131,10 @@
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
@@ -168,7 +163,6 @@
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-right: 14px;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
@@ -177,7 +171,6 @@
|
||||
@media (--big-viewport) {
|
||||
.root {
|
||||
margin-bottom: 30px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
@@ -186,7 +179,6 @@
|
||||
|
||||
.commentContent {
|
||||
display: flex;
|
||||
|
||||
blockquote {
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -63,10 +63,6 @@ class Moderation extends Component {
|
||||
this.props.toggleStorySearch(true);
|
||||
};
|
||||
|
||||
getActiveTabCount = (props = this.props) => {
|
||||
return props.root[`${props.activeTab}Count`];
|
||||
};
|
||||
|
||||
moderate = accept => {
|
||||
const {
|
||||
acceptComment,
|
||||
@@ -139,12 +135,14 @@ class Moderation extends Component {
|
||||
|
||||
const comments = root[activeTab];
|
||||
|
||||
const activeTabCount = this.getActiveTabCount();
|
||||
const menuItems = Object.keys(queueConfig).map(queue => ({
|
||||
key: queue,
|
||||
name: queueConfig[queue].name,
|
||||
icon: queueConfig[queue].icon,
|
||||
count: root[`${queue}Count`],
|
||||
indicator:
|
||||
['premod', 'reported'].includes(queue) && root[queue].nodes.length > 0,
|
||||
// TODO: Eventually we'll reintroduce counting
|
||||
// count: root[`${props.queue}Count`]
|
||||
}));
|
||||
|
||||
const slotPassthrough = {
|
||||
@@ -189,7 +187,6 @@ class Moderation extends Component {
|
||||
loadMore={this.loadMore}
|
||||
commentBelongToQueue={this.props.commentBelongToQueue}
|
||||
isLoadingMore={this.state.isLoadingMore}
|
||||
commentCount={activeTabCount}
|
||||
currentUserId={this.props.currentUser.id}
|
||||
viewUserDetail={viewUserDetail}
|
||||
selectCommentId={props.selectCommentId}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import CountBadge from '../../../components/CountBadge';
|
||||
import Indicator from '../../../components/Indicator';
|
||||
import styles from './ModerationMenu.css';
|
||||
import { Icon } from 'coral-ui';
|
||||
import { Link } from 'react-router';
|
||||
@@ -24,6 +24,7 @@ const ModerationMenu = ({ asset = {}, items, getModPath, activeTab }) => {
|
||||
>
|
||||
{items.map(queue => (
|
||||
<Link
|
||||
id={`talk-admin-moderate-tab-${queue.key}`}
|
||||
key={queue.key}
|
||||
to={getModPath(queue.key, asset.id)}
|
||||
className={cn('mdl-tabs__tab', styles.tab, {
|
||||
@@ -32,7 +33,7 @@ const ModerationMenu = ({ asset = {}, items, getModPath, activeTab }) => {
|
||||
activeClassName={styles.active}
|
||||
>
|
||||
<Icon name={queue.icon} className={styles.tabIcon} /> {queue.name}{' '}
|
||||
<CountBadge count={queue.count} />
|
||||
{queue.indicator && <Indicator />}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from 'react-virtualized';
|
||||
import throttle from 'lodash/throttle';
|
||||
import key from 'keymaster';
|
||||
import cn from 'classnames';
|
||||
|
||||
const hasComment = (nodes, id) => nodes.some(node => node.id === id);
|
||||
|
||||
@@ -204,7 +205,7 @@ class ModerationQueue extends React.Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prev) {
|
||||
const { commentCount, selectedCommentId } = this.props;
|
||||
const { selectedCommentId, hasNextPage } = this.props;
|
||||
|
||||
const switchedToMultiMode = prev.singleView && !this.props.singleView;
|
||||
const switchedMode = prev.singleView !== this.props.singleView;
|
||||
@@ -212,7 +213,6 @@ class ModerationQueue extends React.Component {
|
||||
prev.selectedCommentId !== selectedCommentId && selectedCommentId;
|
||||
const moderatedLastComment =
|
||||
prev.comments.length > 0 && this.getCommentCountWithoutDagling() === 0;
|
||||
const hasMoreComment = commentCount > 0;
|
||||
|
||||
if (switchedToMultiMode) {
|
||||
// Reflow virtual list.
|
||||
@@ -223,7 +223,7 @@ class ModerationQueue extends React.Component {
|
||||
this.scrollToSelectedComment();
|
||||
}
|
||||
|
||||
if (moderatedLastComment && hasMoreComment) {
|
||||
if (moderatedLastComment && hasNextPage) {
|
||||
this.props.loadMore();
|
||||
}
|
||||
}
|
||||
@@ -240,10 +240,7 @@ class ModerationQueue extends React.Component {
|
||||
const index = view.findIndex(
|
||||
({ id }) => id === this.props.selectedCommentId
|
||||
);
|
||||
if (
|
||||
index === view.length - 1 &&
|
||||
this.getCommentCountWithoutDagling() !== this.props.commentCount
|
||||
) {
|
||||
if (index === view.length - 1 && this.props.hasNextPage) {
|
||||
await this.props.loadMore();
|
||||
this.selectDown();
|
||||
return;
|
||||
@@ -384,6 +381,11 @@ class ModerationQueue extends React.Component {
|
||||
...props
|
||||
} = this.props;
|
||||
|
||||
const rootClassName = cn(
|
||||
styles.root,
|
||||
`talk-admin-moderate-queue-${this.props.activeTab}`
|
||||
);
|
||||
|
||||
if (comments.length === 0) {
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
@@ -405,7 +407,7 @@ class ModerationQueue extends React.Component {
|
||||
|
||||
const comment = comments[index];
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={rootClassName}>
|
||||
<Comment
|
||||
root={this.props.root}
|
||||
key={comment.id}
|
||||
@@ -427,7 +429,7 @@ class ModerationQueue extends React.Component {
|
||||
const view = this.state.view;
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={rootClassName}>
|
||||
<ViewMore
|
||||
viewMore={() => this.viewNewComments()}
|
||||
count={comments.length - view.length}
|
||||
@@ -467,7 +469,6 @@ ModerationQueue.propTypes = {
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
commentBelongToQueue: PropTypes.func.isRequired,
|
||||
cleanUpQueue: PropTypes.func.isRequired,
|
||||
commentCount: PropTypes.number.isRequired,
|
||||
loadMore: PropTypes.func.isRequired,
|
||||
singleView: PropTypes.bool,
|
||||
isLoadingMore: PropTypes.bool,
|
||||
|
||||
@@ -314,11 +314,11 @@ class ModerationContainer extends Component {
|
||||
|
||||
const currentQueueConfig = Object.assign({}, this.props.queueConfig);
|
||||
|
||||
if (premodEnabled && root.newCount === 0) {
|
||||
if (premodEnabled && root.new.nodes.length === 0) {
|
||||
delete currentQueueConfig.new;
|
||||
}
|
||||
|
||||
if (!premodEnabled && root.premodCount === 0) {
|
||||
if (!premodEnabled && root.premod.nodes.length === 0) {
|
||||
delete currentQueueConfig.premod;
|
||||
}
|
||||
|
||||
@@ -402,7 +402,7 @@ const COMMENT_RESET_SUBSCRIPTION = gql`
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $sortOrder: SORT_ORDER, $asset_id: ID, $tags:[String!], $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sortOrder: $sortOrder, action_type: $action_type, tags: $tags}) {
|
||||
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sortOrder: $sortOrder, action_type: $action_type, tags: $tags, excludeDeleted: true}) {
|
||||
nodes {
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
}
|
||||
@@ -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(', ')}],`
|
||||
@@ -455,9 +456,14 @@ const withModQueueQuery = withQuery(
|
||||
}
|
||||
`
|
||||
)}
|
||||
${Object.keys(queueConfig).map(
|
||||
${
|
||||
''
|
||||
/*
|
||||
TODO: eventually we'll reintroduce counting..
|
||||
Object.keys(queueConfig).map(
|
||||
queue => `
|
||||
${queue}Count: commentCount(query: {
|
||||
excludeDeleted: true,
|
||||
statuses: ${
|
||||
queueConfig[queue].statuses
|
||||
? `[${queueConfig[queue].statuses.join(', ')}],`
|
||||
@@ -476,7 +482,8 @@ const withModQueueQuery = withQuery(
|
||||
asset_id: $asset_id,
|
||||
})
|
||||
`
|
||||
)}
|
||||
)*/
|
||||
}
|
||||
asset(id: $asset_id) @skip(if: $allAssets) {
|
||||
id
|
||||
title
|
||||
|
||||
@@ -92,9 +92,6 @@
|
||||
|
||||
.statusDropdown {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.statusDropdownOption {
|
||||
min-width: 100px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,20 +22,12 @@ class Stories extends Component {
|
||||
const closed = !!(closedAt && new Date(closedAt).getTime() < Date.now());
|
||||
return (
|
||||
<Dropdown
|
||||
className={styles.statusDropdown}
|
||||
toggleClassName={styles.statusDropdown}
|
||||
value={closed}
|
||||
onChange={value => this.props.onStatusChange(value, id)}
|
||||
>
|
||||
<Option
|
||||
value={false}
|
||||
label={t('streams.open')}
|
||||
className={styles.statusDropdownOption}
|
||||
/>
|
||||
<Option
|
||||
value={true}
|
||||
label={t('streams.closed')}
|
||||
className={styles.statusDropdownOption}
|
||||
/>
|
||||
<Option value={false} label={t('streams.open')} />
|
||||
<Option value={true} label={t('streams.closed')} />
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,32 +3,36 @@ import { getStaticConfiguration } from 'coral-framework/services/staticConfigura
|
||||
import { createPostMessage } from 'coral-framework/services/postMessage';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
try {
|
||||
const staticConfig = getStaticConfiguration();
|
||||
const { STATIC_ORIGIN: origin } = staticConfig;
|
||||
const postMessage = createPostMessage(origin);
|
||||
const staticConfig = getStaticConfiguration();
|
||||
const { STATIC_ORIGIN: origin } = staticConfig;
|
||||
const postMessage = createPostMessage(origin);
|
||||
|
||||
// Get the auth element and parse it as JSON by decoding it.
|
||||
const auth = document.getElementById('auth');
|
||||
const doc = document.implementation.createHTMLDocument('');
|
||||
doc.body.innerHTML = auth.innerText;
|
||||
// Get the auth element and parse it as JSON by decoding it.
|
||||
const auth = document.getElementById('auth');
|
||||
const doc = document.implementation.createHTMLDocument('');
|
||||
doc.body.innerHTML = auth.innerText;
|
||||
|
||||
// Auth state is contained within the node.
|
||||
const { err, data } = JSON.parse(doc.body.textContent);
|
||||
if (err) {
|
||||
// TODO: send back the error message.
|
||||
console.error(err);
|
||||
// Auth state is contained within the node.
|
||||
const { err, data } = JSON.parse(doc.body.textContent);
|
||||
if (err) {
|
||||
const errDiv = document.createElement('div');
|
||||
if (err.message) {
|
||||
errDiv.innerText = `${err.name}: ${err.message}`;
|
||||
} else {
|
||||
// The data will contain a user and a token.
|
||||
const { user, token } = data;
|
||||
|
||||
// Send the state back.
|
||||
postMessage.post(HANDLE_SUCCESSFUL_LOGIN, { user, token });
|
||||
errDiv.innerText = JSON.stringify(err);
|
||||
}
|
||||
} finally {
|
||||
// Always close the window.
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
}, 50);
|
||||
document.body.appendChild(errDiv);
|
||||
throw err;
|
||||
}
|
||||
|
||||
// The data will contain a user and a token.
|
||||
const { user, token } = data;
|
||||
|
||||
// Send the state back.
|
||||
postMessage.post(HANDLE_SUCCESSFUL_LOGIN, { user, token });
|
||||
|
||||
// Close the window when all went well.
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
}, 50);
|
||||
});
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -8,6 +8,14 @@ import reducers from './reducers';
|
||||
import TalkProvider from 'coral-framework/components/TalkProvider';
|
||||
import pluginsConfig from 'pluginsConfig';
|
||||
|
||||
// Resolves touch handling issues encountered on IOS Safari under certain
|
||||
// circumstances. It may be related to issues reported here:
|
||||
//
|
||||
// https://stackoverflow.com/questions/12363742/touchstart-event-is-not-firing-inside-iframe-ios-6
|
||||
//
|
||||
// Further details: https://www.pivotaltracker.com/story/show/157794038
|
||||
document.body.addEventListener('touchstart', () => {});
|
||||
|
||||
async function main() {
|
||||
const context = await createContext({
|
||||
reducers,
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
color: #757575;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.commentSummary {
|
||||
|
||||
@@ -11,16 +11,6 @@ import { getTotalReactionsCount } from 'coral-framework/utils';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class Comment extends React.Component {
|
||||
goToStory = () => {
|
||||
this.props.navigate(this.props.comment.asset.url);
|
||||
};
|
||||
|
||||
goToConversation = () => {
|
||||
this.props.navigate(
|
||||
`${this.props.comment.asset.url}?commentId=${this.props.comment.id}`
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { comment, root } = this.props;
|
||||
const reactionCount = getTotalReactionsCount(comment.action_summaries);
|
||||
@@ -76,8 +66,8 @@ class Comment extends React.Component {
|
||||
<div className="my-comment-asset">
|
||||
<a
|
||||
className={cn(styles.assetURL, 'my-comment-anchor')}
|
||||
href="#"
|
||||
onClick={this.goToStory}
|
||||
href={this.props.comment.asset.url}
|
||||
target="_parent"
|
||||
>
|
||||
{t('common.story')}:{' '}
|
||||
{comment.asset.title ? comment.asset.title : comment.asset.url}
|
||||
@@ -87,7 +77,13 @@ class Comment extends React.Component {
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
<li>
|
||||
<a onClick={this.goToConversation} className={styles.viewLink}>
|
||||
<a
|
||||
className={styles.viewLink}
|
||||
href={`${this.props.comment.asset.url}?commentId=${
|
||||
this.props.comment.id
|
||||
}`}
|
||||
target="_parent"
|
||||
>
|
||||
<Icon name="open_in_new" className={styles.iconView} />
|
||||
{t('view_conversation')}
|
||||
</a>
|
||||
|
||||
@@ -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(
|
||||
@@ -740,10 +745,27 @@ export default class Comment extends React.Component {
|
||||
|
||||
const id = `c_${comment.id}`;
|
||||
|
||||
// props that are passed down the slots.
|
||||
const slotPassthrough = {
|
||||
action: 'deleted',
|
||||
comment,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={rootClassName} id={id}>
|
||||
{this.renderComment()}
|
||||
{activeReplyBox === comment.id && this.renderReplyBox()}
|
||||
{isCommentDeleted(comment) ? (
|
||||
<Slot
|
||||
fill="commentTombstone"
|
||||
defaultComponent={CommentTombstone}
|
||||
size={1}
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
) : (
|
||||
<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');
|
||||
}
|
||||
@@ -37,6 +39,7 @@ class CommentTombstone extends React.Component {
|
||||
|
||||
CommentTombstone.propTypes = {
|
||||
action: PropTypes.string,
|
||||
comment: PropTypes.object,
|
||||
onUndo: PropTypes.func,
|
||||
};
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import StreamError from './StreamError';
|
||||
import Comment from '../containers/Comment';
|
||||
import BannedAccount from '../../../components/BannedAccount';
|
||||
import ChangeUsername from '../containers/ChangeUsername';
|
||||
import Markdown from 'coral-framework/components/Markdown';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import InfoBox from './InfoBox';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
@@ -181,7 +182,9 @@ class Stream extends React.Component {
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
activeReplyBox={activeReplyBox}
|
||||
notify={notify}
|
||||
disableReply={asset.isClosed}
|
||||
disableReply={
|
||||
asset.isClosed || asset.settings.disableCommenting
|
||||
}
|
||||
postComment={postComment}
|
||||
currentUser={currentUser}
|
||||
postFlag={postFlag}
|
||||
@@ -215,7 +218,7 @@ class Stream extends React.Component {
|
||||
currentUser,
|
||||
} = this.props;
|
||||
const { keepCommentBox } = this.state;
|
||||
const open = !asset.isClosed;
|
||||
const open = !(asset.isClosed || asset.settings.disableCommenting);
|
||||
|
||||
const banned = get(currentUser, 'status.banned.status');
|
||||
const suspensionUntil = get(currentUser, 'status.suspension.until');
|
||||
@@ -293,7 +296,13 @@ class Stream extends React.Component {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p>{asset.settings.closedMessage}</p>
|
||||
<div>
|
||||
{asset.isClosed ? (
|
||||
<p>{asset.settings.closedMessage}</p>
|
||||
) : (
|
||||
<Markdown content={asset.settings.disableCommentingMessage} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Slot fill="stream" passthrough={slotPassthrough} />
|
||||
|
||||
@@ -24,6 +24,7 @@ const slots = [
|
||||
'commentAuthorName',
|
||||
'commentAuthorTags',
|
||||
'commentTimestamp',
|
||||
'commentTombstone',
|
||||
'commentContent',
|
||||
];
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
@@ -433,6 +434,8 @@ const fragments = {
|
||||
questionBoxIcon
|
||||
closedTimeout
|
||||
closedMessage
|
||||
disableCommenting
|
||||
disableCommentingMessage
|
||||
charCountEnable
|
||||
charCount
|
||||
requireEmailConfirmation
|
||||
|
||||
@@ -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,8 @@ 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!
|
||||
@@ -70,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.
|
||||
@@ -105,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,
|
||||
|
||||
@@ -37,7 +37,8 @@ export default class Popup extends Component {
|
||||
this.onBlur();
|
||||
};
|
||||
|
||||
// Use `onunload` instead of `onbeforeunload` which is not supported in IOS Safari.
|
||||
// Use `onunload` instead of `onbeforeunload` which is not supported in iOS
|
||||
// Safari.
|
||||
this.ref.onunload = () => {
|
||||
this.onUnload();
|
||||
|
||||
@@ -46,10 +47,15 @@ export default class Popup extends Component {
|
||||
}
|
||||
|
||||
this.resetCallbackInterval = setInterval(() => {
|
||||
if (this.ref && this.ref.onload === null) {
|
||||
clearInterval(this.resetCallbackInterval);
|
||||
this.resetCallbackInterval = null;
|
||||
this.setCallbacks();
|
||||
try {
|
||||
if (this.ref && this.ref.onload === null) {
|
||||
clearInterval(this.resetCallbackInterval);
|
||||
this.resetCallbackInterval = null;
|
||||
this.setCallbacks();
|
||||
}
|
||||
} catch (err) {
|
||||
// We could be getting a security exception here if the login page
|
||||
// gets redirected to another domain to authenticate.
|
||||
}
|
||||
}, 50);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
const PropTypes = require('prop-types');
|
||||
import PropTypes from 'prop-types';
|
||||
import { ApolloProvider } from 'react-apollo';
|
||||
|
||||
class TalkProvider extends React.Component {
|
||||
|
||||
@@ -25,6 +25,9 @@ export default {
|
||||
'UnsuspendUserResponse',
|
||||
'UpdateAssetSettingsResponse',
|
||||
'UpdateAssetStatusResponse',
|
||||
'UpdateSettingsResponse'
|
||||
'UpdateSettingsResponse',
|
||||
'ChangePasswordResponse',
|
||||
'UpdateEmailAddressResponse',
|
||||
'AttachLocalAuthResponse'
|
||||
),
|
||||
};
|
||||
|
||||
@@ -116,7 +116,7 @@ export const withRemoveTag = withMutation(
|
||||
asset_id: assetId,
|
||||
item_type: itemType,
|
||||
},
|
||||
o3timisticResponse: {
|
||||
optimisticResponse: {
|
||||
removeTag: {
|
||||
__typename: 'ModifyTagResponse',
|
||||
errors: null,
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
+12
-2
@@ -1,9 +1,9 @@
|
||||
/* global __webpack_public_path__ */ // eslint-disable-line no-unused-vars
|
||||
/* global __webpack_public_path__, __webpack_nonce__ */ // eslint-disable-line no-unused-vars
|
||||
|
||||
import { getStaticConfiguration } from 'coral-framework/services/staticConfiguration';
|
||||
|
||||
// Load the static url from the static configuration.
|
||||
const { STATIC_URL } = getStaticConfiguration();
|
||||
const { STATIC_URL, SCRIPT_NONCE } = getStaticConfiguration();
|
||||
|
||||
// Update the static url for the imported public path so dynamically imported
|
||||
// chunks will use the correct path as defined by the process.env.STATIC_URL
|
||||
@@ -14,3 +14,13 @@ const { STATIC_URL } = getStaticConfiguration();
|
||||
// https://webpack.js.org/configuration/output/#output-publicpath
|
||||
//
|
||||
__webpack_public_path__ = STATIC_URL + 'static/';
|
||||
|
||||
// All dynamically included scripts that support nonce's will add this to their
|
||||
// script tags.
|
||||
//
|
||||
// The __webpack_nonce__ can be referenced: https://webpack.js.org/guides/csp/
|
||||
//
|
||||
// Pending issues:
|
||||
// - https://github.com/webpack-contrib/style-loader/pull/319
|
||||
//
|
||||
__webpack_nonce__ = SCRIPT_NONCE;
|
||||
@@ -59,6 +59,7 @@ const withSetUsername = hoistStatics(WrappedComponent => {
|
||||
}
|
||||
const changeSet = { success: false, loading: false, error };
|
||||
this.setState(changeSet);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import ta from 'timeago.js';
|
||||
import { negotiateLanguages } from 'fluent-langneg/compat';
|
||||
|
||||
import has from 'lodash/has';
|
||||
import get from 'lodash/get';
|
||||
import merge from 'lodash/merge';
|
||||
import first from 'lodash/first';
|
||||
import isUndefined from 'lodash/isUndefined';
|
||||
|
||||
import moment from 'moment';
|
||||
import 'moment/locale/ar';
|
||||
@@ -9,33 +12,49 @@ 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';
|
||||
|
||||
// timeago
|
||||
import ta from 'timeago.js';
|
||||
import arTA from 'timeago.js/locales/ar';
|
||||
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';
|
||||
|
||||
// locales
|
||||
import ar from '../../../locales/ar.yml';
|
||||
import en from '../../../locales/en.yml';
|
||||
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 = {
|
||||
// the list of languages that are whitelisted. If false, all languages that are
|
||||
// supported by Talk will be enabled.
|
||||
const whitelistedLanguages =
|
||||
process.env.TALK_WHITELISTED_LANGUAGES &&
|
||||
process.env.TALK_WHITELISTED_LANGUAGES.split(',').map(l => l.trim());
|
||||
|
||||
// The default language. If the whitelisted languages is specified and the
|
||||
// default language is not in that list, then the first language in the
|
||||
// whitelisted list will be used as the default.
|
||||
export const defaultLocale = whitelistedLanguages
|
||||
? !whitelistedLanguages.includes(process.env.TALK_DEFAULT_LANG)
|
||||
? whitelistedLanguages[0]
|
||||
: process.env.TALK_DEFAULT_LANG
|
||||
: process.env.TALK_DEFAULT_LANG;
|
||||
|
||||
export const translations = {
|
||||
...ar,
|
||||
...en,
|
||||
...da,
|
||||
@@ -48,69 +67,66 @@ const translations = {
|
||||
...zh_TW,
|
||||
};
|
||||
|
||||
let lang;
|
||||
let timeagoInstance;
|
||||
export const supportedLocales = Object.keys(translations);
|
||||
|
||||
function setLocale(storage, locale) {
|
||||
try {
|
||||
if (storage) {
|
||||
storage.setItem('locale', locale);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
let LOCALE;
|
||||
let TIMEAGO_INSTANCE;
|
||||
|
||||
function getLocale(storage) {
|
||||
try {
|
||||
return (
|
||||
(storage && storage.getItem('locale')) ||
|
||||
navigator.language ||
|
||||
defaultLanguage
|
||||
).split('-')[0];
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// 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.
|
||||
const detectLanguage = () =>
|
||||
first(
|
||||
negotiateLanguages(
|
||||
navigator.languages,
|
||||
whitelistedLanguages || supportedLocales,
|
||||
{
|
||||
defaultLocale,
|
||||
strategy: 'lookup',
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export function setupTranslations() {
|
||||
// Setup the translation framework with the storage.
|
||||
const storage = createStorage('localStorage');
|
||||
// locale
|
||||
LOCALE = detectLanguage();
|
||||
|
||||
const locale = getLocale(storage);
|
||||
setLocale(storage, locale);
|
||||
|
||||
// Setting moment
|
||||
moment.locale(locale);
|
||||
|
||||
// Extract language key.
|
||||
lang = locale.split('-')[0];
|
||||
|
||||
// Check if we have a translation in this language.
|
||||
if (!(lang in translations)) {
|
||||
lang = defaultLanguage;
|
||||
}
|
||||
// moment
|
||||
moment.locale(LOCALE);
|
||||
|
||||
// timeago
|
||||
ta.register('ar', arTA);
|
||||
ta.register('es', esTA);
|
||||
ta.register('da', daTA);
|
||||
ta.register('de', deTA);
|
||||
ta.register('fr', frTA);
|
||||
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();
|
||||
ta.register('nl-NL', nlTA);
|
||||
ta.register('pt-BR', pt_BRTA);
|
||||
ta.register('zh-CN', zh_CNTA);
|
||||
ta.register('zh-TW', zh_TWTA);
|
||||
TIMEAGO_INSTANCE = ta();
|
||||
}
|
||||
|
||||
/**
|
||||
* loadTranslations will load the new language pack into the existing ones.
|
||||
*
|
||||
* @param {Object} newTranslations translation object to merge into the existing
|
||||
* languages.
|
||||
*/
|
||||
export function loadTranslations(newTranslations) {
|
||||
// Merge the new translations into the existing translations.
|
||||
merge(translations, newTranslations);
|
||||
|
||||
// Push new languages into the supportedLocales array.
|
||||
Object.keys(newTranslations).forEach(language => {
|
||||
if (!supportedLocales.includes(language)) {
|
||||
supportedLocales.push(language);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function timeago(time) {
|
||||
return timeagoInstance.format(new Date(time), lang);
|
||||
return TIMEAGO_INSTANCE.format(new Date(time), LOCALE);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,24 +140,24 @@ export function timeago(time) {
|
||||
*/
|
||||
export function t(key, ...replacements) {
|
||||
let translation;
|
||||
if (has(translations[lang], key)) {
|
||||
translation = get(translations[lang], key);
|
||||
if (has(translations[LOCALE], key)) {
|
||||
translation = get(translations[LOCALE], key);
|
||||
} else if (has(translations['en'], key)) {
|
||||
translation = get(translations['en'], key);
|
||||
console.warn(`${lang}.${key} language key not set`);
|
||||
console.warn(`${LOCALE}.${key} language key not set`);
|
||||
}
|
||||
|
||||
if (translation) {
|
||||
// replace any {n} with the arguments passed to this method
|
||||
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`);
|
||||
if (!translation) {
|
||||
console.warn(`${LOCALE}.${key} and en.${key} language key not set`);
|
||||
return key;
|
||||
}
|
||||
|
||||
// Handle replacements in the translation string.
|
||||
return translation.replace(
|
||||
/{(\d+)}/g,
|
||||
(match, number) =>
|
||||
!isUndefined(replacements[number]) ? replacements[number] : match
|
||||
);
|
||||
}
|
||||
|
||||
export default t;
|
||||
|
||||
@@ -56,10 +56,10 @@ export function createPostMessage(origin, scope = 'client') {
|
||||
// Send the message.
|
||||
target.postMessage(msg, origin);
|
||||
},
|
||||
subscribe: (handler, target = window) => {
|
||||
subscribe(handler, target = window) {
|
||||
// If this handler is already attached to the target, detach it.
|
||||
if (has(listeners, [target, handler])) {
|
||||
this.unsubscribeFromMessages(handler, target);
|
||||
this.unsubscribe(handler, target);
|
||||
}
|
||||
|
||||
// Wrap the listener with a origin check.
|
||||
@@ -71,7 +71,7 @@ export function createPostMessage(origin, scope = 'client') {
|
||||
// Attach the listener to the target.
|
||||
target.addEventListener('message', listener);
|
||||
},
|
||||
unsubscribe: (handler, target = window) => {
|
||||
unsubscribe(handler, target = window) {
|
||||
if (!has(listeners, [target, handler])) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
border: none;
|
||||
touch-action: manipulation;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -261,3 +273,23 @@ export function translateError(error) {
|
||||
}
|
||||
return error.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* handlePopupAuth will optionally open a popup with the requested uri if the
|
||||
* window is not already a popup.
|
||||
*
|
||||
* @param {String} uri the url to open the window? to
|
||||
* @param {String} title the title of the new window? to open
|
||||
* @param {String} features the features to use when opening a window?
|
||||
*/
|
||||
export function handlePopupAuth(
|
||||
uri,
|
||||
title = 'Login', // TODO: translate
|
||||
features = 'menubar=0,resizable=0,width=500,height=550,top=200,left=500'
|
||||
) {
|
||||
if (window.opener) {
|
||||
window.location = uri;
|
||||
} else {
|
||||
window.open(uri, title, features);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user