mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87ffa45050 | ||
|
|
4a4357bc4c | ||
|
|
bb11de562f | ||
|
|
5125bb5d2d | ||
|
|
1c87181b17 | ||
|
|
142f463211 | ||
|
|
96733ade4d | ||
|
|
9598e6d426 | ||
|
|
91c4197fd7 | ||
|
|
8042083f6c | ||
|
|
8232ae3e76 | ||
|
|
4e63cff57f | ||
|
|
0206804bf4 | ||
|
|
270c070075 | ||
|
|
dc0c3b4a67 | ||
|
|
b51bb7486e | ||
|
|
7f9503d6aa | ||
|
|
f81e973315 | ||
|
|
1dc3b855aa | ||
|
|
aef5076ac1 | ||
|
|
2294286483 | ||
|
|
6b7580acaf | ||
|
|
b2dc178fdb | ||
|
|
dd916a3ef9 | ||
|
|
4eff55a0d1 | ||
|
|
e77243536c | ||
|
|
97a8a6dadd | ||
|
|
05c0998541 | ||
|
|
18570a97ea | ||
|
|
28e264868a | ||
|
|
4dfc90cac4 | ||
|
|
7b4829bfac | ||
|
|
2e74f79b79 | ||
|
|
f1b8d71cba | ||
|
|
edfe60b8e0 | ||
|
|
46078117c8 | ||
|
|
879820d340 | ||
|
|
88d9cd193b | ||
|
|
624d768f38 | ||
|
|
254033a294 | ||
|
|
637630b8dd | ||
|
|
80987a1e46 | ||
|
|
e9e70f7e75 | ||
|
|
ffde113a74 | ||
|
|
e358dce594 | ||
|
|
3fcad35492 | ||
|
|
4694f986c0 | ||
|
|
aef096fa4c | ||
|
|
a6518b183f | ||
|
|
93ccec6614 | ||
|
|
691e0f5256 | ||
|
|
0e1ace115e | ||
|
|
ae361d5110 | ||
|
|
cfe5b12abd | ||
|
|
1e2c224204 | ||
|
|
fbe2ec30aa | ||
|
|
2f7f6f4f53 | ||
|
|
f77493afb1 | ||
|
|
793b473681 | ||
|
|
ad56d3b99a | ||
|
|
e6b8cd3220 | ||
|
|
98725fcb19 | ||
|
|
ee163af833 | ||
|
|
8ec933e844 | ||
|
|
13f81ad819 | ||
|
|
8d185fe309 | ||
|
|
edbe49618a | ||
|
|
2ec7b53cea | ||
|
|
c448d8d2cd | ||
|
|
fb3233cf01 | ||
|
|
30c38fa696 | ||
|
|
e4358ac3a1 | ||
|
|
de6f325204 | ||
|
|
13667deaed | ||
|
|
610bf36947 | ||
|
|
126d55e6d8 | ||
|
|
6f0af18619 | ||
|
|
3da201c22d | ||
|
|
5ae9d719f3 | ||
|
|
91bce6c74a | ||
|
|
232ed4f4ef | ||
|
|
7720f1d178 | ||
|
|
533cd3217d | ||
|
|
d16ce17e36 | ||
|
|
e8026afb41 | ||
|
|
81509b660e | ||
|
|
4ed04d1c39 | ||
|
|
17e90b0313 | ||
|
|
a509405f43 | ||
|
|
3d61713f16 | ||
|
|
56987bdcd6 | ||
|
|
24c1abaa06 | ||
|
|
fe75d9543c | ||
|
|
681f9ecf5a | ||
|
|
394ebb8f2d | ||
|
|
6982509233 | ||
|
|
06b64ad250 | ||
|
|
9cc089b3b2 | ||
|
|
ae674a4e62 | ||
|
|
ec117a67aa | ||
|
|
f32c9fa6b1 | ||
|
|
761e372016 | ||
|
|
8d147c9e7b | ||
|
|
4a4c502fdd | ||
|
|
bfea8b00e1 | ||
|
|
94dbfd6802 | ||
|
|
c5dc8454a2 | ||
|
|
ff9a212cfb | ||
|
|
0b3b252fc3 | ||
|
|
6f616a1e78 | ||
|
|
3a436bd530 | ||
|
|
cb7b4133f9 | ||
|
|
217c2f1bfa | ||
|
|
bad72e0ef4 | ||
|
|
cf0551d3e7 | ||
|
|
f83aea0431 | ||
|
|
c2133eb009 | ||
|
|
20187ad365 | ||
|
|
02326cd3a7 | ||
|
|
6fe453415d | ||
|
|
05b29f2f06 | ||
|
|
28afcdd007 | ||
|
|
a128c7cb76 | ||
|
|
cf61ca0485 | ||
|
|
7c9fd2acb3 | ||
|
|
7e5ff7bf96 | ||
|
|
34e3266a6f | ||
|
|
bc11eaa9c8 | ||
|
|
969d2a1588 | ||
|
|
cd412056ad | ||
|
|
0865c4d5ad | ||
|
|
dca9b19240 | ||
|
|
5b9d24a029 | ||
|
|
b646320b92 | ||
|
|
864012e5ab | ||
|
|
e86cfaaa5b | ||
|
|
93d6ad9e55 | ||
|
|
f8fbedd727 | ||
|
|
2cf047a393 | ||
|
|
45ad7eeff7 | ||
|
|
7437595002 | ||
|
|
9efa5479a8 | ||
|
|
7867b0113e | ||
|
|
17d50c6ebb | ||
|
|
11051fbdb9 | ||
|
|
ee9786de02 | ||
|
|
0226991521 | ||
|
|
7d4217d2d7 | ||
|
|
81b98187f1 | ||
|
|
a7e580fab3 | ||
|
|
a55cee5cc1 | ||
|
|
69e194f648 | ||
|
|
939df89842 | ||
|
|
8586da97fb | ||
|
|
2757791672 | ||
|
|
06ed792005 | ||
|
|
3bd8d5627b | ||
|
|
595dda11bf | ||
|
|
a154ccfc60 | ||
|
|
cc66be033a | ||
|
|
0b624927b5 | ||
|
|
92ce39bfa8 | ||
|
|
4ee9ce00f2 | ||
|
|
0a02eda343 | ||
|
|
84ddede7d3 | ||
|
|
f9ca66d165 | ||
|
|
82d520da0c | ||
|
|
45b570e163 | ||
|
|
bba52b4bbd | ||
|
|
896b12a7b7 | ||
|
|
3df1bed645 | ||
|
|
6ca7f3b53e | ||
|
|
beb301cf22 | ||
|
|
999a0710f5 | ||
|
|
3a92aa33a9 | ||
|
|
7a505ac5fc | ||
|
|
be75980edb | ||
|
|
ab96417d3d | ||
|
|
8a5118728b | ||
|
|
e39e0b7875 | ||
|
|
d5df9ba8e4 | ||
|
|
e67aea48af | ||
|
|
935ba6e9fe | ||
|
|
4881c33942 | ||
|
|
e89b5da864 | ||
|
|
d80a2d84c5 | ||
|
|
2fe17d29e3 | ||
|
|
a53194b4a0 | ||
|
|
c9e5fd99b5 | ||
|
|
cd4d666a46 | ||
|
|
ab476f93b6 | ||
|
|
c5536da565 | ||
|
|
1b233e8cb6 | ||
|
|
2599000e60 | ||
|
|
8a3cbac442 | ||
|
|
5e259af72b | ||
|
|
d7cf733a42 | ||
|
|
f69163d9c2 | ||
|
|
246e3efe7f | ||
|
|
b504d2ad2b | ||
|
|
de48880e01 | ||
|
|
8f727cc6db | ||
|
|
59982d4329 | ||
|
|
4f87c88e04 | ||
|
|
3ec7bf09bf | ||
|
|
1fb1fc6ba9 | ||
|
|
543dde12aa | ||
|
|
ef11a27e1d | ||
|
|
752c16f513 | ||
|
|
a1fcb6e171 | ||
|
|
d4e02b620e | ||
|
|
292e33c105 | ||
|
|
e66a2eb8e0 | ||
|
|
61836423ac | ||
|
|
6844960300 | ||
|
|
34fa2fb891 | ||
|
|
a1a867ec55 | ||
|
|
b0ea4960e3 | ||
|
|
36988d7e20 | ||
|
|
19920deeda | ||
|
|
64094c40db | ||
|
|
1fc7500837 | ||
|
|
773c843834 | ||
|
|
a4c2b879ea | ||
|
|
74271853e2 | ||
|
|
b65e7a0ad9 | ||
|
|
678f535500 | ||
|
|
47a2c5a174 | ||
|
|
cb59d51a74 | ||
|
|
b9420406ed | ||
|
|
5491ac034c | ||
|
|
3799495929 | ||
|
|
5463959347 | ||
|
|
eab17b4c1d | ||
|
|
ad1ea9a810 | ||
|
|
101450273a | ||
|
|
5a2212846d | ||
|
|
6ee4a258ff | ||
|
|
8e3395d0e8 | ||
|
|
5b436e3c4a | ||
|
|
9185d3df43 | ||
|
|
822c605103 | ||
|
|
273cc9927a | ||
|
|
bce56f2719 | ||
|
|
1928bc3819 | ||
|
|
11ca430ad9 | ||
|
|
dc8f4a1765 | ||
|
|
ec493f9a3a | ||
|
|
58899293af | ||
|
|
bf37ab8c78 | ||
|
|
881d7abfa6 | ||
|
|
bf54959ef5 | ||
|
|
937ee5ea67 | ||
|
|
5f526eb49c | ||
|
|
f3b379922b | ||
|
|
f23bdd91bf | ||
|
|
24f9afd531 | ||
|
|
78eb92e3ab | ||
|
|
b7ff6f004d | ||
|
|
95934b87a4 | ||
|
|
f4575e2075 | ||
|
|
694a9bb438 | ||
|
|
1aeae553e5 | ||
|
|
cc0dcc1620 | ||
|
|
483c21990f | ||
|
|
1f0630d875 | ||
|
|
44a8e6eb8f | ||
|
|
96865be27a | ||
|
|
99955b82f5 | ||
|
|
0b01700235 | ||
|
|
92e910a53f | ||
|
|
195bc1715b | ||
|
|
a43c173a12 | ||
|
|
a3ea964c15 | ||
|
|
68eddfa6bb | ||
|
|
1fa1077bb2 | ||
|
|
9c9e3bf798 | ||
|
|
9ba2ca9156 | ||
|
|
c4aa2fc141 | ||
|
|
d9b0071edc | ||
|
|
7bef612ad0 | ||
|
|
ae4cc3ba20 | ||
|
|
973e907275 | ||
|
|
fa3633b29a | ||
|
|
16c5a90736 | ||
|
|
26bc9fc94f | ||
|
|
2e96ea0170 | ||
|
|
a4987f2947 | ||
|
|
1256007eba | ||
|
|
313c738b8b | ||
|
|
a73b93b0c3 | ||
|
|
1ecc2a8f40 | ||
|
|
65778c31ff | ||
|
|
f6e661090c | ||
|
|
bd2fcf837a | ||
|
|
44b5a122c5 | ||
|
|
b43b67e8fa | ||
|
|
638043632c | ||
|
|
3415163cfb | ||
|
|
db1934e897 | ||
|
|
076a07fba4 | ||
|
|
ff24f521be | ||
|
|
d4cfe5034f | ||
|
|
ad418f49ba | ||
|
|
08978ba942 | ||
|
|
baca86f5be | ||
|
|
db64f21beb | ||
|
|
3ec6823a15 | ||
|
|
b4e8f2212c | ||
|
|
5ebfd14c84 | ||
|
|
5285670a56 | ||
|
|
cf370d330d | ||
|
|
2f9f72ccf3 | ||
|
|
cf6073e6a2 | ||
|
|
98b6ce066b | ||
|
|
5cd2cce7d8 | ||
|
|
025604c4cd | ||
|
|
6edb5923d7 | ||
|
|
cd2426511c | ||
|
|
7e4feda033 | ||
|
|
d08aaecd12 | ||
|
|
d98e64d6d7 | ||
|
|
84a79be9c1 | ||
|
|
dbe80c40d7 | ||
|
|
db5eedf6a1 | ||
|
|
1e743ea29d | ||
|
|
8fc69b0551 | ||
|
|
88a80c8ffa | ||
|
|
b2db0bb877 | ||
|
|
6c35f02e6b | ||
|
|
b6ba59ac51 | ||
|
|
74d065ea84 | ||
|
|
cd9c260995 | ||
|
|
0242e7b124 | ||
|
|
4bf02d39a9 | ||
|
|
030deb1b81 | ||
|
|
40ec1a42dc | ||
|
|
aa40e7033b | ||
|
|
d6fba2d515 | ||
|
|
497225139d | ||
|
|
da285fb7e5 | ||
|
|
17e02deecb | ||
|
|
55e829ca66 | ||
|
|
14e7dc0872 | ||
|
|
f35c870da8 | ||
|
|
1ce9219dc5 | ||
|
|
d70d5db685 | ||
|
|
58c5026fb7 | ||
|
|
8a2fa2432f | ||
|
|
8fcbd4b4c4 | ||
|
|
eb331551c5 | ||
|
|
346a099c1b | ||
|
|
df64c98b48 | ||
|
|
9c6f4c2799 | ||
|
|
12cf5826f2 | ||
|
|
3821477ef0 | ||
|
|
8aa8517d1d | ||
|
|
40782a5d0b | ||
|
|
0209cc5b13 | ||
|
|
a700e91911 | ||
|
|
96dff8ed25 | ||
|
|
f346cf4caf | ||
|
|
a84ad511e8 | ||
|
|
3b2a9e5c75 | ||
|
|
c9ec5ca2ce | ||
|
|
c27def3167 | ||
|
|
cb6fe6fd16 | ||
|
|
9d9adc8195 | ||
|
|
164c4e30b8 | ||
|
|
82280a7da7 | ||
|
|
4a05a45379 | ||
|
|
cbe2725f00 | ||
|
|
7d7e3f60d3 | ||
|
|
fb519286c3 | ||
|
|
4f844dfd58 | ||
|
|
5cde5f89d4 | ||
|
|
b64ab62807 | ||
|
|
a23f4fd5db | ||
|
|
804711cac5 | ||
|
|
7c8305695c | ||
|
|
21722d36ea | ||
|
|
867a0d1ea7 | ||
|
|
adbf041bbe | ||
|
|
cc06808e9b | ||
|
|
8c7e8080c3 | ||
|
|
40228a8ff4 | ||
|
|
bb1b265cae | ||
|
|
6b486c86a5 | ||
|
|
6c591e803b | ||
|
|
2f1689d2ca | ||
|
|
1a8006e3a3 | ||
|
|
d58bc5cfb1 | ||
|
|
608db5cdb0 | ||
|
|
3d571a60c2 | ||
|
|
713f3230fe | ||
|
|
747817edd8 | ||
|
|
418a51cbb4 | ||
|
|
f74641a548 | ||
|
|
cbd3b2814e | ||
|
|
4c2424c323 | ||
|
|
bec3e94bbc | ||
|
|
784175ae0a | ||
|
|
6a6b7eab7c | ||
|
|
3bdded34b5 | ||
|
|
106b2faaf1 | ||
|
|
3d8fc116a7 | ||
|
|
ce70c9c925 | ||
|
|
1f548e1c5e | ||
|
|
35207888da | ||
|
|
50158d16aa | ||
|
|
3c03999814 | ||
|
|
9ed17dd949 | ||
|
|
e25b2d01c2 | ||
|
|
8db3f72bb4 | ||
|
|
5136683018 | ||
|
|
94fa21c57f | ||
|
|
8dfa9de3f8 | ||
|
|
d2c8fa03a5 | ||
|
|
60c4f56132 | ||
|
|
56fab6be6c | ||
|
|
4fcdec92d8 | ||
|
|
a36960a6f0 | ||
|
|
613688456a | ||
|
|
ecb457fef3 | ||
|
|
73fce8624c | ||
|
|
965c6f2934 | ||
|
|
c01de4f24d | ||
|
|
f49c095dfa | ||
|
|
6a02f666ea | ||
|
|
414b62ff66 | ||
|
|
403c1996bf | ||
|
|
2cb8bf324f | ||
|
|
07bec45ff8 | ||
|
|
028f2a0d11 | ||
|
|
c402ef4a03 | ||
|
|
ed16cd1d60 | ||
|
|
0dcb0bcaab | ||
|
|
a0d2a4147c | ||
|
|
b6a2d791ec | ||
|
|
79f885244a | ||
|
|
7af9458710 | ||
|
|
7598784acc | ||
|
|
8613ced71d | ||
|
|
a81ae2b7f8 | ||
|
|
5e4d010246 | ||
|
|
1b3a74a011 | ||
|
|
84f1003b2b | ||
|
|
22e0aef306 | ||
|
|
9ad92e38c0 | ||
|
|
5bb5f98b17 | ||
|
|
6efda40e29 | ||
|
|
8e0c53ef3b | ||
|
|
bfb9ea1eea | ||
|
|
93e850ba3f | ||
|
|
da6b5c7f36 | ||
|
|
ccda23ec60 | ||
|
|
0425c39bfe | ||
|
|
f826db1edf | ||
|
|
77839c1fbe | ||
|
|
9f1c59da11 | ||
|
|
940185d9b6 | ||
|
|
f5fd162b37 | ||
|
|
810d43cc6f | ||
|
|
a6adb0ceba | ||
|
|
2cfbc1bb12 | ||
|
|
4df803187f | ||
|
|
fd88ae9433 | ||
|
|
0937e8a946 | ||
|
|
686074f6cd | ||
|
|
931c21539b | ||
|
|
8fbbe62cdb | ||
|
|
96323de147 | ||
|
|
21e3e24d9f | ||
|
|
31c524688c | ||
|
|
b80184d9ee | ||
|
|
5dbbf80068 | ||
|
|
c7d21438df | ||
|
|
77be539710 | ||
|
|
68fb511223 | ||
|
|
6200ba39c2 | ||
|
|
bc030930cc | ||
|
|
907554044d | ||
|
|
2f5fc03e9c | ||
|
|
e713b39846 | ||
|
|
ed94039286 | ||
|
|
f321d9ecaf | ||
|
|
77ecc5e8df | ||
|
|
05b0dc2d80 | ||
|
|
5b885bfb3c | ||
|
|
820153429f | ||
|
|
969b3dbcd5 | ||
|
|
28935cc25d | ||
|
|
cacab7aa23 | ||
|
|
6298658194 | ||
|
|
a32a205069 | ||
|
|
75c9269d8d | ||
|
|
615c3e7aa3 | ||
|
|
4612ccb049 | ||
|
|
cfff0becbb | ||
|
|
042e76a19f | ||
|
|
0b2faf4bc2 | ||
|
|
7bd0330be1 | ||
|
|
40625b6184 | ||
|
|
59137f5f54 | ||
|
|
933a65a508 | ||
|
|
57ae878452 | ||
|
|
5b37a04341 | ||
|
|
7b4baba28e | ||
|
|
2d0cafee50 | ||
|
|
0708025f81 | ||
|
|
3c854d0468 | ||
|
|
c22de3832c | ||
|
|
cd638e7f85 | ||
|
|
0397906907 | ||
|
|
44b8809db1 | ||
|
|
1b55684a73 | ||
|
|
e0dc871367 | ||
|
|
7bcad6d0c0 | ||
|
|
44f187dafa | ||
|
|
518e4115ee | ||
|
|
15f5e27aa4 | ||
|
|
ccefffb7fa | ||
|
|
2979b16462 | ||
|
|
2ae520fac4 | ||
|
|
4eb2d83c04 | ||
|
|
0b9ba9a0e3 | ||
|
|
1a3979bbeb | ||
|
|
6efe975980 | ||
|
|
e36252cb72 | ||
|
|
db44863658 | ||
|
|
929f4535a6 | ||
|
|
b2daf7114a | ||
|
|
6cfb2aa016 | ||
|
|
2c46de9451 | ||
|
|
d7e9295309 | ||
|
|
420c1728c6 | ||
|
|
a67c4a8142 | ||
|
|
4c8ef59495 | ||
|
|
ef78654d23 | ||
|
|
c2406df46f | ||
|
|
af80d795ad | ||
|
|
e8edfa04dd | ||
|
|
866d6d8643 | ||
|
|
4ce2e6f691 | ||
|
|
25b72cbcab | ||
|
|
e5891822fb | ||
|
|
4f93086909 | ||
|
|
f450661991 | ||
|
|
e8a2d09df2 | ||
|
|
01d03e77f8 | ||
|
|
435067f619 | ||
|
|
60a704f694 | ||
|
|
86683ee21c | ||
|
|
ac5062fc01 | ||
|
|
3b15cd76c1 | ||
|
|
e76127d9ee | ||
|
|
4ae9920732 | ||
|
|
f48062e38f | ||
|
|
0fb6645104 | ||
|
|
ff71285fa9 | ||
|
|
48b5afafab | ||
|
|
8a5059934c | ||
|
|
40a3a24ba0 | ||
|
|
98facd1019 | ||
|
|
2e9e7c297b | ||
|
|
e72622e913 | ||
|
|
6caf2f26f5 | ||
|
|
861f82a168 | ||
|
|
497fe606dc | ||
|
|
e6b28a55af | ||
|
|
033c6464e9 | ||
|
|
a4a22c0758 | ||
|
|
1b8276cd92 | ||
|
|
72151e0afd | ||
|
|
9cc9969320 | ||
|
|
005602a738 | ||
|
|
09dcca27a8 | ||
|
|
17f41fa5d0 | ||
|
|
62cdf4988f | ||
|
|
485e02fb1c | ||
|
|
090920131f | ||
|
|
f50b3f365d | ||
|
|
4320263bcc | ||
|
|
d26e2a328f | ||
|
|
6a99a983c5 | ||
|
|
801fc8bc15 | ||
|
|
be33948497 | ||
|
|
930ff603c3 | ||
|
|
aa754657a2 | ||
|
|
5fcfb45a10 | ||
|
|
77274be2e4 | ||
|
|
ff7fac38ff | ||
|
|
a749a13786 | ||
|
|
be62a2780c | ||
|
|
0f58c1b780 | ||
|
|
dcddfa4f39 | ||
|
|
59da1ec1af | ||
|
|
284598439d | ||
|
|
95da5697fc | ||
|
|
7962df6983 | ||
|
|
a1f6800d99 | ||
|
|
87cb2b53c7 | ||
|
|
047beb1ae9 | ||
|
|
515867bf14 | ||
|
|
87ea7b5dae | ||
|
|
8113c4f8b8 | ||
|
|
1d31099c9d | ||
|
|
3f2a97f42b | ||
|
|
6f0b0727e8 | ||
|
|
179edb88be | ||
|
|
5f5bc631b6 | ||
|
|
f558e38a45 | ||
|
|
8d147dbf47 | ||
|
|
91dd9b157a | ||
|
|
11b98370b6 | ||
|
|
bc273d16f7 | ||
|
|
8cb645651c | ||
|
|
86c221d6f9 | ||
|
|
98b517affb | ||
|
|
25dbcf7186 | ||
|
|
86c36f7d39 | ||
|
|
8192a3bfc4 | ||
|
|
58269a7cae | ||
|
|
0055c82166 | ||
|
|
5f9c6b3b88 | ||
|
|
e055a3e4d4 | ||
|
|
57d7920c59 | ||
|
|
cc9b030fb3 | ||
|
|
da6ac290aa | ||
|
|
3610e96c74 | ||
|
|
300e518c2d | ||
|
|
eaf403353f | ||
|
|
d1c0167e9a | ||
|
|
3fd1d6705d | ||
|
|
de69cf5790 | ||
|
|
a3158a494c | ||
|
|
8cb75e3bb1 | ||
|
|
de54b916ef | ||
|
|
6426d3ebd8 | ||
|
|
11687b06d0 | ||
|
|
cf87b52177 | ||
|
|
9be6a3cce6 | ||
|
|
288768793a | ||
|
|
bd93d2d5b5 | ||
|
|
6b54957760 | ||
|
|
5afa4fa269 | ||
|
|
ba685a05a2 | ||
|
|
4304cad787 | ||
|
|
d969da540f | ||
|
|
20c29cd81c | ||
|
|
72e80a4693 | ||
|
|
80818b0861 | ||
|
|
20bec20437 | ||
|
|
2451d4b6d6 | ||
|
|
744348dee5 | ||
|
|
d2fafafb84 | ||
|
|
1a7f7dddd4 | ||
|
|
7ccb1e26a4 | ||
|
|
c91ac80df6 | ||
|
|
30271ac945 | ||
|
|
6e20a43e29 | ||
|
|
16448a025e | ||
|
|
8fb4f58eab | ||
|
|
b5843343a9 | ||
|
|
910425caea | ||
|
|
37cb8d3690 | ||
|
|
70d081df4a | ||
|
|
6555e0fcaa | ||
|
|
e24deda936 | ||
|
|
b8ddfe4ae4 | ||
|
|
35bfec6da6 | ||
|
|
4c5adc26cf | ||
|
|
6e946ca31d | ||
|
|
7c55088d9f | ||
|
|
0bb61da860 | ||
|
|
435afff2df | ||
|
|
8305223639 | ||
|
|
57ba7e63db | ||
|
|
847310f214 | ||
|
|
3879ab1bff | ||
|
|
9f79b3f2c6 | ||
|
|
71d12627c8 | ||
|
|
11812714a3 | ||
|
|
3d31bec671 | ||
|
|
593468f0c1 | ||
|
|
b6520d0b91 | ||
|
|
d0cfbf60be | ||
|
|
b9b6f035f8 | ||
|
|
a2cc18e79e | ||
|
|
f0dd8b9690 | ||
|
|
b9769bd309 | ||
|
|
12b98d0fc6 | ||
|
|
8aad744818 | ||
|
|
cdd77339b0 | ||
|
|
91b11c838b | ||
|
|
f3f481fcf0 | ||
|
|
a55f03f23c | ||
|
|
278f7772a4 | ||
|
|
6e89ba8ffb | ||
|
|
6037202158 | ||
|
|
4e1087c661 | ||
|
|
5b38ef3422 | ||
|
|
09170671f9 | ||
|
|
e98dfaf813 | ||
|
|
588a3d9065 | ||
|
|
5d58c4f82b | ||
|
|
b2466a1890 | ||
|
|
094b8b5366 | ||
|
|
16dceee5bf | ||
|
|
925d3b8b31 | ||
|
|
c5093ab61b | ||
|
|
f125fac077 | ||
|
|
ba62cdeab6 | ||
|
|
f3b5cfa20b | ||
|
|
9122c7b393 | ||
|
|
d63422a4c2 | ||
|
|
73355ab542 | ||
|
|
88c0772d52 | ||
|
|
58897f7571 | ||
|
|
d750ed5d6c | ||
|
|
8fe94e5ead | ||
|
|
a0305e6cb1 | ||
|
|
e47ab0fa37 | ||
|
|
1fd64e873d | ||
|
|
918ce04e83 | ||
|
|
09eaba5a2e | ||
|
|
6d48c62679 | ||
|
|
81026af3ee | ||
|
|
0e2a49ef1f | ||
|
|
c0293a4487 | ||
|
|
17a0aebdf7 | ||
|
|
19320eb063 | ||
|
|
00a40c9e34 | ||
|
|
1b05f146fe | ||
|
|
31e5eb8685 | ||
|
|
d1951c8504 | ||
|
|
9158faf295 | ||
|
|
77c7bb3cab | ||
|
|
beafb42c15 | ||
|
|
9437c5f5a3 | ||
|
|
f0572d1e34 | ||
|
|
894940cd0b | ||
|
|
105ed1454d | ||
|
|
1a9f9c2056 | ||
|
|
8ce0b784e2 | ||
|
|
22c876fa02 | ||
|
|
735ecbcef3 | ||
|
|
37dc3a27dc | ||
|
|
12a1ed0de3 | ||
|
|
eb0265bbb3 | ||
|
|
36438c7f38 | ||
|
|
bd08debb40 | ||
|
|
7e41619732 | ||
|
|
c869869436 | ||
|
|
8b4fe46120 | ||
|
|
1707cd7d98 | ||
|
|
23e71221a6 | ||
|
|
c12b3ca2b2 | ||
|
|
4fe3d582b8 | ||
|
|
ecf040ebf7 | ||
|
|
632f80a202 | ||
|
|
025c82a6cc | ||
|
|
02374345e1 | ||
|
|
627170a970 | ||
|
|
3a04e492c2 | ||
|
|
e45b09b32a | ||
|
|
fa3373ec94 | ||
|
|
5baa20b06f | ||
|
|
8571fdc957 | ||
|
|
25f4f8c1c8 | ||
|
|
69117f2fa5 | ||
|
|
9b5c0c2ca7 | ||
|
|
e6832dc2b4 | ||
|
|
7422575aba | ||
|
|
820087f49c | ||
|
|
907ffea1f9 | ||
|
|
d6818366ec | ||
|
|
912ccd6dd9 | ||
|
|
ab3133c8aa | ||
|
|
2f5c4906f7 | ||
|
|
898e28fe1b | ||
|
|
139d036b42 | ||
|
|
0406fe7d1c | ||
|
|
cbf33b29fb | ||
|
|
322740bda0 | ||
|
|
e33ccb57b6 | ||
|
|
9ce94c97d0 | ||
|
|
31c643e4eb | ||
|
|
0a3acbc7ba | ||
|
|
ec285dc87a | ||
|
|
a3e2448f25 | ||
|
|
855e972bec | ||
|
|
361dc6c441 | ||
|
|
083b8638e8 | ||
|
|
b649fe3071 | ||
|
|
011a6e6465 | ||
|
|
6a46703e98 | ||
|
|
2485386b45 | ||
|
|
5bba2ea0ae | ||
|
|
fd53de94a1 | ||
|
|
26a660248e | ||
|
|
8c9bf5d4c8 | ||
|
|
fa96531eab | ||
|
|
e7f96bd04c | ||
|
|
2aa043d39a | ||
|
|
03fe54c68e | ||
|
|
24b3980423 | ||
|
|
91e7dadfda | ||
|
|
1bca2a96e5 | ||
|
|
2f4c084811 | ||
|
|
4579537709 | ||
|
|
6f6fe69129 | ||
|
|
57b9b2f70e | ||
|
|
7c676e8926 | ||
|
|
d268e2d3e4 | ||
|
|
15425f3e49 | ||
|
|
4d943629d9 | ||
|
|
7ad0575e6d | ||
|
|
60620951cf | ||
|
|
7dd63dc812 | ||
|
|
b79a07275f | ||
|
|
6eb442d59a | ||
|
|
62eb77fa4b | ||
|
|
46a1857abe | ||
|
|
38352e9e3c | ||
|
|
9098537838 | ||
|
|
ab7cfffa50 | ||
|
|
bf1bae8999 | ||
|
|
eb86fdbd11 | ||
|
|
9ca237f49d | ||
|
|
6935330536 | ||
|
|
60af5135ca | ||
|
|
9cfa5b6a0f | ||
|
|
50748cc032 | ||
|
|
7d7a5a8b59 | ||
|
|
d7b4f57350 | ||
|
|
8258ccde9b | ||
|
|
096c9a4894 | ||
|
|
e895413038 | ||
|
|
cb266e651b | ||
|
|
b6fa8b92a1 | ||
|
|
85f3598e99 | ||
|
|
e5f6188bd2 | ||
|
|
7e2e29c947 | ||
|
|
7404962741 | ||
|
|
8080994975 | ||
|
|
e8e8b007e3 | ||
|
|
2bb56290c4 | ||
|
|
9426f280cc | ||
|
|
6a5c643ef2 | ||
|
|
22ee7b1d34 | ||
|
|
234c705f35 | ||
|
|
74d38f16ed | ||
|
|
a7e9936671 | ||
|
|
9617948493 | ||
|
|
7129896bd6 | ||
|
|
e2e718494c | ||
|
|
284009df55 | ||
|
|
f918bbea20 | ||
|
|
4467f6110f | ||
|
|
a166404347 | ||
|
|
34922a6d42 | ||
|
|
3d2f3ebc59 | ||
|
|
c4cdc672b7 | ||
|
|
b84a488518 | ||
|
|
8b4b1728bc | ||
|
|
233c514251 | ||
|
|
4b134cbfc2 | ||
|
|
c446098a1e | ||
|
|
a9e8f390b8 | ||
|
|
b26e938ac5 | ||
|
|
ee04d9a848 | ||
|
|
0ca8b978a1 | ||
|
|
846df0a5e0 | ||
|
|
e7934d9468 | ||
|
|
3deb56750f | ||
|
|
12497a1331 | ||
|
|
45dde4636d | ||
|
|
8c2070e345 | ||
|
|
606d5734f9 | ||
|
|
87ff631eed | ||
|
|
43d4db085b | ||
|
|
b600172380 | ||
|
|
0b547b173a | ||
|
|
950efba3fe | ||
|
|
af592e326f | ||
|
|
499a7fe686 | ||
|
|
c2aa2077f4 | ||
|
|
91aeb9dbae | ||
|
|
26476f42f7 | ||
|
|
9a89dfaa29 | ||
|
|
634da8012f | ||
|
|
3434fb4fed | ||
|
|
a6bc29af9a | ||
|
|
d86ff8418d | ||
|
|
e71bcbe58d | ||
|
|
77c838ea22 | ||
|
|
39313a20fa | ||
|
|
ad4427286d | ||
|
|
dc82db6eda | ||
|
|
4bb2b3d731 | ||
|
|
4c06d5659d | ||
|
|
d812a0f696 | ||
|
|
ba11eb233a | ||
|
|
e76d109f93 | ||
|
|
78db4077e1 | ||
|
|
0519b55707 | ||
|
|
5602bf222c | ||
|
|
181d258b5e | ||
|
|
36e855237f | ||
|
|
376ffd8486 | ||
|
|
214d9e5047 | ||
|
|
bfd6cfbaa4 | ||
|
|
9ec7027f3c | ||
|
|
4acb6aeb6c | ||
|
|
c3bccaf4a8 | ||
|
|
a867b27f53 | ||
|
|
e162a97919 | ||
|
|
f63a7915ea | ||
|
|
18710e2345 | ||
|
|
ff29fa04e5 | ||
|
|
7a9023744a | ||
|
|
b1c03973e6 | ||
|
|
9e4759cbfd | ||
|
|
94c98f000c | ||
|
|
c2154b6efa | ||
|
|
c46b29aed9 | ||
|
|
43e02e1574 | ||
|
|
ffd88e0ef0 | ||
|
|
9820e575b2 | ||
|
|
1e329c8967 | ||
|
|
73034a20f0 | ||
|
|
8f7171b54e | ||
|
|
b6e4581ba2 | ||
|
|
78444a0bb4 | ||
|
|
4d7e64e1ec | ||
|
|
b8e340c471 | ||
|
|
39c1fcf83a | ||
|
|
4b2d9fc93a | ||
|
|
0a324a2268 | ||
|
|
fc27e02140 | ||
|
|
f40f209395 | ||
|
|
e924ed9189 | ||
|
|
4053a25239 | ||
|
|
de094ee948 | ||
|
|
c7ee8307e6 | ||
|
|
431bbd4f03 | ||
|
|
0f69df26f6 | ||
|
|
c140a62822 | ||
|
|
cad25eedd1 | ||
|
|
87af05c0d9 | ||
|
|
c54a8bdc4f | ||
|
|
d857585c7f | ||
|
|
3999b806c7 | ||
|
|
6c1ed826d0 | ||
|
|
4224f18ba7 | ||
|
|
6feaa94fba | ||
|
|
d639596047 | ||
|
|
f5409b11bf | ||
|
|
28a348f2cc | ||
|
|
6c1667d80b | ||
|
|
5d1eff0ddb | ||
|
|
142c5b32f1 | ||
|
|
5221ebbe9a | ||
|
|
79cb8b0fb2 | ||
|
|
90290cfa2d | ||
|
|
8b669a31c5 | ||
|
|
aaa1e0dbc3 | ||
|
|
eab1e6ca59 | ||
|
|
4b6a3b5b9d | ||
|
|
f4fc8915e3 | ||
|
|
1774f98519 | ||
|
|
4883bb17b9 | ||
|
|
37dd6ae044 | ||
|
|
d526a404ae | ||
|
|
aaad7aa86f | ||
|
|
74a2ed4a41 | ||
|
|
bad0363628 | ||
|
|
ffbcd93c3f | ||
|
|
61bc372640 | ||
|
|
ee3d576a49 | ||
|
|
b758dc91cb | ||
|
|
cdf3c44083 | ||
|
|
26cba9af63 | ||
|
|
f8e0556904 | ||
|
|
5815d869b5 | ||
|
|
c15cff7121 | ||
|
|
f8f882fbfd | ||
|
|
931fb81aa8 | ||
|
|
cf4e817e79 | ||
|
|
a06f76fd4c | ||
|
|
f47abfc3ad | ||
|
|
275fbf4c5a |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"presets": [
|
||||
["es2015", {modules: false}]
|
||||
["es2015", {"modules": false}]
|
||||
],
|
||||
"plugins": [
|
||||
"transform-class-properties",
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
# job_defaults applies all the defaults for each job.
|
||||
job_defaults: &job_defaults
|
||||
working_directory: ~/coralproject/talk
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
|
||||
# integration_environment is the environment that configures the tests.
|
||||
integration_environment: &integration_environment
|
||||
NODE_ENV: test
|
||||
CIRCLE_TEST_REPORTS: /tmp/circleci-test-results
|
||||
E2E_MAX_RETRIES: 3
|
||||
|
||||
# integration_job runs the integration tests and saves the test results.
|
||||
integration_job: &integration_job
|
||||
<<: *job_defaults
|
||||
environment:
|
||||
<<: *integration_environment
|
||||
docker:
|
||||
# TODO: replace with node:8-browsers when build issues are resolved.
|
||||
# - image: circleci/node:8-browsers
|
||||
- image: coralproject/ci
|
||||
- image: circleci/mongo:3
|
||||
- image: circleci/redis:4-alpine
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/coralproject/talk
|
||||
- run:
|
||||
name: Setup the database with defaults
|
||||
command: ./bin/cli setup --defaults
|
||||
- run:
|
||||
name: Run the integration tests
|
||||
command: bash .circleci/e2e.sh
|
||||
- store_test_results:
|
||||
when: always
|
||||
path: /tmp/circleci-test-results
|
||||
- store_artifacts:
|
||||
when: always
|
||||
path: /tmp/circleci-test-results
|
||||
|
||||
|
||||
version: 2
|
||||
jobs:
|
||||
# npm_dependencies will install the dependencies used by all other steps.
|
||||
npm_dependencies:
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/coralproject/talk
|
||||
- restore_cache:
|
||||
key: dependency-cache-{{ checksum "yarn.lock" }}
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: |
|
||||
yarn global add node-gyp &&
|
||||
yarn install --frozen-lockfile
|
||||
- save_cache:
|
||||
key: dependency-cache-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- ./node_modules
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths: node_modules
|
||||
|
||||
# lint will perform file linting.
|
||||
lint:
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/coralproject/talk
|
||||
- run:
|
||||
name: Perform linting
|
||||
command: yarn lint
|
||||
|
||||
# build_assets will build the static assets.
|
||||
build_assets:
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/coralproject/talk
|
||||
- restore_cache:
|
||||
keys:
|
||||
- build-cache-{{ .Branch }}-{{ .Revision }}
|
||||
- build-cache-{{ .Branch }}-
|
||||
- build-cache-
|
||||
- run:
|
||||
name: Build static assets
|
||||
command: yarn build
|
||||
- save_cache:
|
||||
key: build-cache-{{ .Branch }}-{{ .Revision }}
|
||||
paths:
|
||||
- ./node_modules/.cache/babel-loader
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths: dist
|
||||
|
||||
# test_unit will run the unit tests.
|
||||
test_unit:
|
||||
<<: *job_defaults
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
- image: circleci/mongo:3
|
||||
- image: circleci/redis:4-alpine
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/coralproject/talk
|
||||
- run:
|
||||
name: Setup the test results directory
|
||||
command: mkdir -p /tmp/circleci-test-results
|
||||
- run:
|
||||
name: Run the client unit tests
|
||||
command: yarn test:client --ci
|
||||
environment:
|
||||
JEST_JUNIT_OUTPUT: /tmp/circleci-test-results/jest/test-results.xml
|
||||
JEST_REPORTER: jest-junit
|
||||
- run:
|
||||
name: Run the server unit tests
|
||||
command: yarn test:server
|
||||
environment:
|
||||
MOCHA_FILE: /tmp/circleci-test-results/mocha/test-results.xml
|
||||
MOCHA_REPORTER: mocha-junit-reporter
|
||||
- store_test_results:
|
||||
when: always
|
||||
path: /tmp/circleci-test-results
|
||||
|
||||
# test_integration_chrome_local will run the integration tests locally with
|
||||
# chrome headless.
|
||||
test_integration_chrome_local:
|
||||
<<: *integration_job
|
||||
environment:
|
||||
<<: *integration_environment
|
||||
E2E_BROWSERS: chrome
|
||||
|
||||
# test_integration_firefox_local will run the integration tests locally with
|
||||
# firefox headless.
|
||||
test_integration_firefox_local:
|
||||
<<: *integration_job
|
||||
environment:
|
||||
<<: *integration_environment
|
||||
E2E_BROWSERS: firefox
|
||||
|
||||
# test_integration_chrome will run the integration tests with chrome in
|
||||
# browserstack.
|
||||
test_integration_chrome:
|
||||
<<: *integration_job
|
||||
environment:
|
||||
<<: *integration_environment
|
||||
BROWSERSTACK: true
|
||||
E2E_BROWSERS: chrome
|
||||
|
||||
# test_integration_firefox will run the integration tests with firefox in
|
||||
# browserstack.
|
||||
test_integration_firefox:
|
||||
<<: *integration_job
|
||||
environment:
|
||||
<<: *integration_environment
|
||||
BROWSERSTACK: true
|
||||
E2E_BROWSERS: firefox
|
||||
|
||||
# test_integration_edge will run the integration tests with edge in
|
||||
# browserstack.
|
||||
test_integration_edge:
|
||||
<<: *integration_job
|
||||
environment:
|
||||
<<: *integration_environment
|
||||
BROWSERSTACK: true
|
||||
E2E_BROWSERS: edge
|
||||
|
||||
# test_integration_ie will run the integration tests with ie in
|
||||
# browserstack.
|
||||
test_integration_ie:
|
||||
<<: *integration_job
|
||||
environment:
|
||||
<<: *integration_environment
|
||||
BROWSERSTACK: true
|
||||
E2E_BROWSERS: ie
|
||||
# TODO: remove when more reliable
|
||||
E2E_MAX_RETRIES: 1
|
||||
|
||||
# test_integration_safari will run the integration tests with safari in
|
||||
# browserstack.
|
||||
test_integration_safari:
|
||||
<<: *integration_job
|
||||
environment:
|
||||
<<: *integration_environment
|
||||
BROWSERSTACK: true
|
||||
E2E_BROWSERS: safari
|
||||
# TODO: remove when more reliable
|
||||
E2E_MAX_RETRIES: 1
|
||||
|
||||
# deploy will deploy the application as a docker image.
|
||||
deploy:
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
- run:
|
||||
name: Deploy the code
|
||||
command: bash ./scripts/docker.sh deploy
|
||||
|
||||
# filter_deploy will add the filters for a deploy job in a workflow to make it
|
||||
# only execute on a deploy related job.
|
||||
filter_deploy: &filter_deploy
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- next
|
||||
tags:
|
||||
only: /v[0-9]+(\.[0-9]+)*/
|
||||
|
||||
# filter_develop will add the filters for a development related commit.
|
||||
filter_develop: &filter_develop
|
||||
filters:
|
||||
branches:
|
||||
ignore:
|
||||
- master
|
||||
- next
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
||||
# All PR's will hit this workflow.
|
||||
build-and-test:
|
||||
jobs:
|
||||
- npm_dependencies:
|
||||
<<: *filter_develop
|
||||
- lint:
|
||||
<<: *filter_develop
|
||||
requires:
|
||||
- npm_dependencies
|
||||
- test_unit:
|
||||
<<: *filter_develop
|
||||
requires:
|
||||
- npm_dependencies
|
||||
- build_assets:
|
||||
<<: *filter_develop
|
||||
requires:
|
||||
- npm_dependencies
|
||||
- test_integration_chrome_local:
|
||||
<<: *filter_develop
|
||||
requires:
|
||||
- build_assets
|
||||
# TODO: uncomment when more reliable
|
||||
# - test_integration_firefox_local:
|
||||
# <<: *filter_develop
|
||||
# requires:
|
||||
# - build_assets
|
||||
deploy-tagged:
|
||||
jobs:
|
||||
- npm_dependencies:
|
||||
<<: *filter_deploy
|
||||
- lint:
|
||||
<<: *filter_deploy
|
||||
requires:
|
||||
- npm_dependencies
|
||||
- test_unit:
|
||||
<<: *filter_deploy
|
||||
requires:
|
||||
- npm_dependencies
|
||||
- build_assets:
|
||||
<<: *filter_deploy
|
||||
requires:
|
||||
- npm_dependencies
|
||||
- test_integration_chrome:
|
||||
<<: *filter_deploy
|
||||
requires:
|
||||
- build_assets
|
||||
- test_integration_firefox:
|
||||
<<: *filter_deploy
|
||||
requires:
|
||||
- build_assets
|
||||
- test_integration_edge:
|
||||
<<: *filter_deploy
|
||||
requires:
|
||||
- build_assets
|
||||
# TODO: uncomment when more reliable
|
||||
# - test_integration_ie:
|
||||
# <<: *filter_deploy
|
||||
# requires:
|
||||
# - build_assets
|
||||
# - test_integration_safari:
|
||||
# <<: *filter_deploy
|
||||
# requires:
|
||||
# - build_assets
|
||||
- deploy:
|
||||
<<: *filter_deploy
|
||||
requires:
|
||||
- lint
|
||||
- test_unit
|
||||
- test_integration_chrome
|
||||
- test_integration_firefox
|
||||
- test_integration_edge
|
||||
# TODO: uncomment when more reliable
|
||||
# - test_integration_ie
|
||||
# - test_integration_safari
|
||||
@@ -19,11 +19,11 @@ if [[ "${E2E_DISABLE}" == "true" ]]; then
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ "${CIRCLE_BRANCH}" == "master" && -n "$BROWSERSTACK_KEY" ]]; then
|
||||
if [[ "$BROWSERSTACK" == "true" && -n "$BROWSERSTACK_KEY" ]]; then
|
||||
echo Testing on browserstack
|
||||
yarn e2e --reports-folder "$REPORTS_FOLDER" --bs-key "$BROWSERSTACK_KEY" --retries "$E2E_MAX_RETRIES" --timeout "$E2E_WAIT_FOR_TIMEOUT" --browsers "$E2E_BROWSERS"
|
||||
node scripts/e2e.js --reports-folder "$REPORTS_FOLDER" --retries "$E2E_MAX_RETRIES" --timeout "$E2E_WAIT_FOR_TIMEOUT" --browsers "$E2E_BROWSERS" --browserstack
|
||||
else
|
||||
# When browserstack is not available test locally using chrome headless.
|
||||
# When browserstack is not available test locally.
|
||||
echo Testing locally
|
||||
yarn e2e --reports-folder "$REPORTS_FOLDER" --retries "$E2E_MAX_RETRIES" --headless
|
||||
node scripts/e2e.js --reports-folder "$REPORTS_FOLDER" --retries "$E2E_MAX_RETRIES" --timeout "$E2E_WAIT_FOR_TIMEOUT" --browsers "$E2E_BROWSERS" --headless
|
||||
fi
|
||||
+1
-30
@@ -1,34 +1,5 @@
|
||||
**/*.html
|
||||
dist
|
||||
docs
|
||||
node_modules
|
||||
plugins/*
|
||||
public
|
||||
|
||||
!plugins/talk-plugin-akismet
|
||||
!plugins/talk-plugin-auth
|
||||
!plugins/talk-plugin-author-menu
|
||||
!plugins/talk-plugin-comment-content
|
||||
!plugins/talk-plugin-deep-reply-count
|
||||
!plugins/talk-plugin-facebook-auth
|
||||
!plugins/talk-plugin-featured-comments
|
||||
!plugins/talk-plugin-flag-details
|
||||
!plugins/talk-plugin-ignore-user
|
||||
!plugins/talk-plugin-like
|
||||
!plugins/talk-plugin-love
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-mod
|
||||
!plugins/talk-plugin-moderation-actions
|
||||
!plugins/talk-plugin-offtopic
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-respect
|
||||
!plugins/talk-plugin-sort-most-liked
|
||||
!plugins/talk-plugin-sort-most-loved
|
||||
!plugins/talk-plugin-sort-most-replied
|
||||
!plugins/talk-plugin-sort-most-respected
|
||||
!plugins/talk-plugin-sort-newest
|
||||
!plugins/talk-plugin-sort-oldest
|
||||
!plugins/talk-plugin-subscriber
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-viewing-options
|
||||
**/*.min.js
|
||||
|
||||
@@ -1,5 +1,34 @@
|
||||
### Expected behavior
|
||||
<!--
|
||||
|
||||
### Actual behavior
|
||||
Thank you for filing an issue on Coral Talk!
|
||||
|
||||
### Steps to reproduce behavior
|
||||
Please fill out the questions below so we can take action on your issue as soon as we can.
|
||||
|
||||
If you're filing a feature request, you do not need to follow the outline below. Instead please include "Feature Idea" in your issue title and explain a specific example in which that feature would be useful.
|
||||
|
||||
-->
|
||||
|
||||
#### Do you want to request a **feature** or report a **bug**?
|
||||
|
||||
|
||||
#### Intended outcome:
|
||||
<!--
|
||||
What you were trying to accomplish when the bug occurred?
|
||||
-->
|
||||
|
||||
#### Actual outcome:
|
||||
<!--
|
||||
What happened instead?
|
||||
|
||||
Please provide as much detail as possible, including a screenshot or copy-paste of any related error messages, logs, or other output that might be related. Places to look for information include your browser console, server console, and network logs. The more information you can give the better.
|
||||
-->
|
||||
|
||||
#### How to reproduce the issue:
|
||||
<!--
|
||||
Instructions for how the issue can be reproduced by someone from our team or by a contributor. Be as specific as possible, and only mention what is necessary to reproduce the bug. If possible, try to isolate the exact circumstances in which the bug occurs and avoid speculation over what the cause might be.
|
||||
-->
|
||||
|
||||
#### Version and environment
|
||||
<!--
|
||||
List what version of Talk you're using, as well as any other relevant environment information, such as operating system or browser
|
||||
-->
|
||||
|
||||
+36
-21
@@ -6,6 +6,7 @@ npm-debug.log*
|
||||
dump.rdb
|
||||
|
||||
client/coral-framework/graphql/introspection.json
|
||||
docs/source/_data/introspection.json
|
||||
|
||||
.env
|
||||
*.cfg
|
||||
@@ -14,6 +15,7 @@ client/coral-framework/graphql/introspection.json
|
||||
*.swp
|
||||
*.DS_STORE
|
||||
.prettierrc.json
|
||||
.vscode
|
||||
|
||||
coverage/
|
||||
test/e2e/reports/
|
||||
@@ -22,36 +24,49 @@ test/e2e/selenium-debug.log
|
||||
browserstack.err
|
||||
|
||||
plugins.json
|
||||
|
||||
plugins/*
|
||||
!plugins/talk-plugin-akismet
|
||||
!plugins/talk-plugin-facebook-auth
|
||||
!plugins/talk-plugin-auth
|
||||
!plugins/talk-plugin-respect
|
||||
!plugins/talk-plugin-offtopic
|
||||
!plugins/talk-plugin-like
|
||||
!plugins/talk-plugin-mod
|
||||
!plugins/talk-plugin-love
|
||||
!plugins/talk-plugin-viewing-options
|
||||
!plugins/talk-plugin-author-menu
|
||||
!plugins/talk-plugin-comment-content
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-deep-reply-count
|
||||
!plugins/talk-plugin-downvote
|
||||
!plugins/talk-plugin-facebook-auth
|
||||
!plugins/talk-plugin-featured-comments
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-sort-newest
|
||||
!plugins/talk-plugin-sort-oldest
|
||||
!plugins/talk-plugin-sort-most-replied
|
||||
!plugins/talk-plugin-flag-details
|
||||
!plugins/talk-plugin-google-auth
|
||||
!plugins/talk-plugin-ignore-user
|
||||
!plugins/talk-plugin-like
|
||||
!plugins/talk-plugin-love
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-mod
|
||||
!plugins/talk-plugin-moderation-actions
|
||||
!plugins/talk-plugin-notifications
|
||||
!plugins/talk-plugin-notifications-category-featured
|
||||
!plugins/talk-plugin-notifications-category-reply
|
||||
!plugins/talk-plugin-notifications-category-staff
|
||||
!plugins/talk-plugin-notifications-digest-daily
|
||||
!plugins/talk-plugin-notifications-digest-hourly
|
||||
!plugins/talk-plugin-offtopic
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-profile-settings
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-respect
|
||||
!plugins/talk-plugin-slack-notifications
|
||||
!plugins/talk-plugin-sort-most-downvoted
|
||||
!plugins/talk-plugin-sort-most-liked
|
||||
!plugins/talk-plugin-sort-most-loved
|
||||
!plugins/talk-plugin-sort-most-replied
|
||||
!plugins/talk-plugin-sort-most-respected
|
||||
!plugins/talk-plugin-author-menu
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-ignore-user
|
||||
!plugins/talk-plugin-moderation-actions
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-deep-reply-count
|
||||
!plugins/talk-plugin-sort-most-upvoted
|
||||
!plugins/talk-plugin-sort-newest
|
||||
!plugins/talk-plugin-sort-oldest
|
||||
!plugins/talk-plugin-subscriber
|
||||
!plugins/talk-plugin-flag-details
|
||||
!plugins/talk-plugin-slack-notifications
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-upvote
|
||||
!plugins/talk-plugin-viewing-options
|
||||
!plugins/talk-plugin-rich-text
|
||||
|
||||
**/node_modules/*
|
||||
yarn-error.log
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"linters": {
|
||||
"*.js": [
|
||||
"git-exec-and-restage eslint --fix --"
|
||||
],
|
||||
"bin/cli*": [
|
||||
"git-exec-and-restage eslint --fix --"
|
||||
],
|
||||
"*.yml": [
|
||||
"yamllint"
|
||||
]
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"exec": "npm-run-all --parallel generate-introspection start:development",
|
||||
"verbose": true,
|
||||
"ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"],
|
||||
"ignore": ["test/*", "client/*", "dist/*", "plugins/*/client", "docs/*"],
|
||||
"ext": "js,json,graphql,yml",
|
||||
"watch": [
|
||||
".",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"exceptions": [
|
||||
"https://nodesecurity.io/advisories/531",
|
||||
"https://nodesecurity.io/advisories/532"
|
||||
"https://nodesecurity.io/advisories/532",
|
||||
"https://nodesecurity.io/advisories/566"
|
||||
]
|
||||
}
|
||||
|
||||
+3
-3
@@ -54,12 +54,12 @@ We are looking for _documentarians_ to:
|
||||
* take the lead in making sections, or the over all structure better.
|
||||
|
||||
Our documentation is stored in markdown files in the [docs](docs) directory. We
|
||||
use Jekyll to provide our docs. To preview:
|
||||
use [Hexo](https://hexo.io/) to provide our docs. To preview:
|
||||
|
||||
```shell
|
||||
cd docs
|
||||
bundle install
|
||||
bundle exec jekyll serve
|
||||
yarn
|
||||
yarn start
|
||||
```
|
||||
|
||||
Then visit http://127.0.0.1:4000/talk/.
|
||||
|
||||
+4
-1
@@ -15,10 +15,13 @@ COPY . /usr/src/app
|
||||
# Ensure the runtime of the container is in production mode.
|
||||
ENV NODE_ENV production
|
||||
|
||||
# Store the current git revision.
|
||||
ARG REVISION_HASH
|
||||
ENV REVISION_HASH=${REVISION_HASH}
|
||||
|
||||
# Install app dependencies and build static assets.
|
||||
RUN yarn global add node-gyp && \
|
||||
yarn install --frozen-lockfile && \
|
||||
cli plugins reconcile && \
|
||||
yarn build && \
|
||||
yarn cache clean
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
FROM coralproject/talk:latest
|
||||
|
||||
# Setup the build arguments
|
||||
ONBUILD ARG TALK_ADDTL_COMMENTS_ON_LOAD_MORE=10
|
||||
ONBUILD ARG TALK_ASSET_COMMENTS_LOAD_DEPTH=10
|
||||
ONBUILD ARG TALK_REPLY_COMMENTS_LOAD_DEPTH=3
|
||||
ONBUILD ARG TALK_THREADING_LEVEL=3
|
||||
ONBUILD ARG TALK_DEFAULT_STREAM_TAB=all
|
||||
ONBUILD ARG TALK_DEFAULT_LANG=en
|
||||
@@ -15,5 +18,6 @@ ONBUILD COPY . /usr/src/app
|
||||
# clear out the development dependencies again. After this we of course need to
|
||||
# clear out the yarn cache, this saves quite a lot of size.
|
||||
ONBUILD RUN cli plugins reconcile && \
|
||||
yarn && \
|
||||
yarn build && \
|
||||
yarn cache clean
|
||||
@@ -1,4 +1,4 @@
|
||||
# Talk · [](https://circleci.com/gh/coralproject/talk) · [](https://nodesecurity.io/orgs/coralproject/projects/07ce2e4c-99fb-48f8-b50b-69d2d2c081b8) · [](CONTRIBUTING.md#pull-requests)
|
||||
# Talk · [](https://circleci.com/gh/coralproject/talk) · [](https://nodesecurity.io/orgs/coralproject/projects/7bd7d26c-47ed-4a5f-8c4a-b919bf1c2946) · [](CONTRIBUTING.md#pull-requests)
|
||||
|
||||
Online comments are broken. Our open-source commenting platform, Talk, rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here](https://coralproject.net/products/talk.html).
|
||||
|
||||
@@ -12,19 +12,23 @@ You're just one click away from trying Talk - all you need is a Heroku account a
|
||||
|
||||
## Technical Documentation
|
||||
|
||||
From getting up and running, to advanced configuration, to how to scale Talk, our [Talk Technical Docs](https://coralproject.github.io/talk/) have everything you need to know.
|
||||
From getting up and running, to advanced configuration, to how to scale Talk, our [Talk Technical Docs](https://docs.coralproject.net/talk/) have everything you need to know.
|
||||
|
||||
## Product Guide
|
||||
|
||||
Learn more about Talk, including a deep dive into features for commenters and moderators, and FAQs in our [Talk Product Guide](https://coralproject.github.io/talk/how-talk-works).
|
||||
Learn more about Talk, including a deep dive into features for commenters and moderators, and FAQs in our [Talk Product Guide](https://docs.coralproject.net/talk/how-talk-works).
|
||||
|
||||
## Relevant Links
|
||||
## Pre-Launch Guide
|
||||
|
||||
You’ve installed Talk on your server, and you’re preparing to launch it on your site. The real community work starts now, before you go live. You have a unique opportunity pre-launch to set your community up for success. Read our [Talk Community Guide](https://blog.coralproject.net/youve-installed-talk-now-what/).
|
||||
|
||||
## More Resources
|
||||
|
||||
- [Talk Product Roadmap](https://www.pivotaltracker.com/n/projects/1863625)
|
||||
- [Our Blog](https://blog.coralproject.net/)
|
||||
- [Community Forums](https://community.coralproject.net/)
|
||||
- [Community Guides for Journalism](https://guides.coralproject.net/)
|
||||
- [More About Us](https://coralproject.net/)
|
||||
- [Talk Roadmap](https://www.pivotaltracker.com/n/projects/1863625)
|
||||
|
||||
## End-to-End Testing
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
const express = require('express');
|
||||
const morgan = require('morgan');
|
||||
const trace = require('./middleware/trace');
|
||||
const logging = require('./middleware/logging');
|
||||
const path = require('path');
|
||||
const uuid = require('uuid');
|
||||
const merge = require('lodash/merge');
|
||||
const helmet = require('helmet');
|
||||
const plugins = require('./services/plugins');
|
||||
const compression = require('compression');
|
||||
const { HELMET_CONFIGURATION } = require('./config');
|
||||
const { MOUNT_PATH } = require('./url');
|
||||
const routes = require('./routes');
|
||||
@@ -14,12 +13,9 @@ const { ENABLE_TRACING, APOLLO_ENGINE_KEY, PORT } = require('./config');
|
||||
|
||||
const app = express();
|
||||
|
||||
// Request Identity Middleware
|
||||
app.use((req, res, next) => {
|
||||
req.id = uuid.v4();
|
||||
|
||||
next();
|
||||
});
|
||||
// Add the trace middleware first, it will create a request ID for each request
|
||||
// downstream.
|
||||
app.use(trace);
|
||||
|
||||
//==============================================================================
|
||||
// PLUGIN PRE APPLICATION MIDDLEWARE
|
||||
@@ -39,7 +35,7 @@ plugins.get('server', 'app').forEach(({ plugin, app: callback }) => {
|
||||
|
||||
// Add the logging middleware only if we aren't testing.
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
app.use(morgan('dev'));
|
||||
app.use(logging.log);
|
||||
}
|
||||
|
||||
if (ENABLE_TRACING && APOLLO_ENGINE_KEY) {
|
||||
@@ -72,9 +68,6 @@ app.use(
|
||||
)
|
||||
);
|
||||
|
||||
// Compress the responses if appropriate.
|
||||
app.use(compression());
|
||||
|
||||
//==============================================================================
|
||||
// VIEW CONFIGURATION
|
||||
//==============================================================================
|
||||
|
||||
+19
-13
@@ -13,6 +13,7 @@ const CommentModel = require('../models/comment');
|
||||
const AssetsService = require('../services/assets');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const scraper = require('../services/scraper');
|
||||
const Context = require('../graph/context');
|
||||
const inquirer = require('inquirer');
|
||||
const { URL } = require('url');
|
||||
|
||||
@@ -52,22 +53,27 @@ async function refreshAssets(ageString) {
|
||||
const ageMs = parseDuration(ageString);
|
||||
const age = new Date(now - ageMs);
|
||||
|
||||
let assets = await AssetModel.find({
|
||||
$or: [
|
||||
{
|
||||
scraped: {
|
||||
$lte: age,
|
||||
let assets = await AssetModel.find(
|
||||
{
|
||||
$or: [
|
||||
{
|
||||
scraped: {
|
||||
$lte: age,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
scraped: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
{
|
||||
scraped: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ id: 1 }
|
||||
);
|
||||
|
||||
// Create a graph context.
|
||||
const ctx = Context.forSystem();
|
||||
|
||||
// Queue all the assets for scraping.
|
||||
await Promise.all(assets.map(scraper.create));
|
||||
|
||||
await Promise.all(assets.map(({ id }) => scraper.create(ctx, id)));
|
||||
console.log('Assets were queued to be scraped');
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
|
||||
+3
-7
@@ -6,8 +6,7 @@
|
||||
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const scraper = require('../services/scraper');
|
||||
const mailer = require('../services/mailer');
|
||||
const jobs = require('../jobs');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const kue = require('../services/kue');
|
||||
|
||||
@@ -21,11 +20,8 @@ function processJobs() {
|
||||
// started.
|
||||
util.onshutdown([() => kue.Task.shutdown()]);
|
||||
|
||||
// Start the scraper processor.
|
||||
scraper.process();
|
||||
|
||||
// Start the mail processor.
|
||||
mailer.process();
|
||||
// Start the jobs processor.
|
||||
jobs.process();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+31
-98
@@ -135,22 +135,15 @@ function reconcilePackages({ quiet = false, upgradeRemote = false }) {
|
||||
return { local, fetchable, upgradable };
|
||||
}
|
||||
|
||||
async function reconcileRemotePlugins({ skipLocal, dryRun, upgradeRemote }) {
|
||||
console.log(
|
||||
`\n[${skipLocal ? '1/2' : '2/3'}] ${emoji.get(
|
||||
'mag'
|
||||
)} Reconciling plugins...`.yellow
|
||||
);
|
||||
async function reconcileRemotePlugins({ dryRun, upgradeRemote }) {
|
||||
console.log(`\n['1/2'] ${emoji.get('mag')} Reconciling plugins...`.yellow);
|
||||
const { fetchable, upgradable } = reconcilePackages({ upgradeRemote });
|
||||
|
||||
console.log(
|
||||
`[${skipLocal ? '2/2' : '3/3'}] ${emoji.get('truck')} Fetching plugins...\n`
|
||||
.yellow
|
||||
);
|
||||
console.log(`['2/2'] ${emoji.get('truck')} Fetching plugins...\n`.yellow);
|
||||
|
||||
if (fetchable.length > 0) {
|
||||
console.log(
|
||||
`$ yarn add --ignore-scripts ${fetchable.map(
|
||||
`$ yarn add --ignore-scripts --ignore-workspace-root-check ${fetchable.map(
|
||||
({ name, version }) => `${name}@${version}`.cyan
|
||||
)}`
|
||||
);
|
||||
@@ -159,6 +152,7 @@ async function reconcileRemotePlugins({ skipLocal, dryRun, upgradeRemote }) {
|
||||
let args = [
|
||||
'add',
|
||||
'--ignore-scripts',
|
||||
'--ignore-workspace-root-check',
|
||||
...fetchable.map(({ name, version }) => `${name}@${version}`),
|
||||
];
|
||||
|
||||
@@ -206,98 +200,41 @@ async function reconcileRemotePlugins({ skipLocal, dryRun, upgradeRemote }) {
|
||||
return { upgradable, fetchable };
|
||||
}
|
||||
|
||||
async function reconcileLocalPlugins({ skipRemote, dryRun }) {
|
||||
console.log(
|
||||
`\n[${skipRemote ? '1/1' : '1/3'}] ${emoji.get(
|
||||
'pick'
|
||||
)} Installing local plugin dependencies...\n`.yellow
|
||||
);
|
||||
const { local } = reconcilePackages({ quiet: true });
|
||||
|
||||
for (let i in local) {
|
||||
let { name } = local[i];
|
||||
|
||||
if (!fs.existsSync(path.join(dir, 'plugins', name, 'package.json'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let wd = path.join(dir, 'plugins', name);
|
||||
|
||||
console.log(`$ cd ${wd.cyan} && yarn`);
|
||||
|
||||
if (!dryRun) {
|
||||
let args = [];
|
||||
|
||||
let output = spawn.sync('yarn', args, {
|
||||
stdio: ['ignore', 'pipe', 'inherit'],
|
||||
cwd: wd,
|
||||
});
|
||||
|
||||
if (output.status) {
|
||||
throw new Error(
|
||||
'Could not install local plugin dependencies, errors occurred during install'
|
||||
);
|
||||
}
|
||||
|
||||
console.log(output.stdout.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This traverses the local plugins and installs any dependencies listed there,
|
||||
// this only is really needed for plugins that are installed via docker because
|
||||
// core plugins will have their dependencies already included in core.
|
||||
async function reconcilePluginDeps({
|
||||
skipLocal,
|
||||
skipRemote,
|
||||
dryRun,
|
||||
upgradeRemote,
|
||||
}) {
|
||||
async function reconcilePluginDeps({ dryRun, upgradeRemote }) {
|
||||
try {
|
||||
let startTime = new Date();
|
||||
|
||||
// We don't need to do anything if we skip everything....
|
||||
if (skipLocal && skipRemote) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Traverse local plugins and install dependencies if enabled.
|
||||
if (!skipLocal) {
|
||||
await reconcileLocalPlugins({ skipRemote, dryRun });
|
||||
}
|
||||
|
||||
// Locate any external plugins and install them.
|
||||
if (!skipRemote) {
|
||||
const results = await reconcileRemotePlugins({
|
||||
skipLocal,
|
||||
skipRemote,
|
||||
dryRun,
|
||||
upgradeRemote,
|
||||
});
|
||||
const results = await reconcileRemotePlugins({
|
||||
dryRun,
|
||||
upgradeRemote,
|
||||
});
|
||||
|
||||
let status;
|
||||
if (dryRun) {
|
||||
status = '[dry-run] success'.green;
|
||||
} else {
|
||||
status = 'success'.green;
|
||||
}
|
||||
|
||||
let message;
|
||||
if (results.upgradable.length === 0 && results.fetchable.length === 0) {
|
||||
message = 'Already up-to-date.';
|
||||
} else if (results.upgradable.length === 0) {
|
||||
message = `Fetched ${results.fetchable.length} new plugins.`;
|
||||
} else if (results.fetchable.length === 0) {
|
||||
message = `Upgraded ${results.upgradable.length} new plugins.`;
|
||||
} else {
|
||||
message = `Fetched ${results.fetchable.length} new plugins, upgraded ${
|
||||
results.upgradable.length
|
||||
} plugins.`;
|
||||
}
|
||||
|
||||
console.log(`\n${status} ${message}`);
|
||||
let status;
|
||||
if (dryRun) {
|
||||
status = '[dry-run] success'.green;
|
||||
} else {
|
||||
status = 'success'.green;
|
||||
}
|
||||
|
||||
let message;
|
||||
if (results.upgradable.length === 0 && results.fetchable.length === 0) {
|
||||
message = 'Already up-to-date.';
|
||||
} else if (results.upgradable.length === 0) {
|
||||
message = `Fetched ${results.fetchable.length} new plugins.`;
|
||||
} else if (results.fetchable.length === 0) {
|
||||
message = `Upgraded ${results.upgradable.length} new plugins.`;
|
||||
} else {
|
||||
message = `Fetched ${results.fetchable.length} new plugins, upgraded ${
|
||||
results.upgradable.length
|
||||
} plugins.`;
|
||||
}
|
||||
|
||||
console.log(`\n${status} ${message}`);
|
||||
|
||||
let endTime = new Date();
|
||||
|
||||
let totalTime = ((endTime.getTime() - startTime.getTime()) / 1000).toFixed(
|
||||
@@ -440,16 +377,12 @@ program
|
||||
|
||||
program
|
||||
.command('reconcile')
|
||||
.description(
|
||||
'reconciles local plugin dependencies and downloads external plugins'
|
||||
)
|
||||
.description('reconciles dependencies by downloading external plugins')
|
||||
.option('-u, --upgrade-remote', 'upgrades remote dependencies')
|
||||
.option(
|
||||
'-d, --dry-run',
|
||||
'does not actually change anything on the filesystem acts only as a simulation'
|
||||
)
|
||||
.option('--skip-local', 'skips the local dependancy reconciliation')
|
||||
.option('--skip-remote', 'skips the remote plugin reconciliation')
|
||||
.action(reconcilePluginDeps);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
+7
-1
@@ -10,6 +10,12 @@ const serve = require('../serve');
|
||||
|
||||
program
|
||||
.option('-j, --jobs', 'enable job processing on this thread')
|
||||
.option(
|
||||
'--disabled-jobs <jobs>',
|
||||
'disable jobs specified if the -j option is passed, specified as a comma separated list',
|
||||
val => val.split(','),
|
||||
[]
|
||||
)
|
||||
.option(
|
||||
'-w, --websockets',
|
||||
'enable the websocket (subscriptions) handler on this thread'
|
||||
@@ -17,7 +23,7 @@ program
|
||||
.parse(process.argv);
|
||||
|
||||
// Start serving.
|
||||
serve({ jobs: program.jobs, websockets: program.websockets }).catch(err => {
|
||||
serve(program).catch(err => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
|
||||
+10
-6
@@ -14,7 +14,8 @@ 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.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
@@ -40,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) {
|
||||
@@ -184,7 +187,8 @@ const performSetup = async () => {
|
||||
},
|
||||
]);
|
||||
|
||||
let { user: newUser } = await SetupService.setup({
|
||||
const ctx = Context.forSystem();
|
||||
let { user: newUser } = await SetupService.setup(ctx, {
|
||||
settings: settings.toObject(),
|
||||
user: {
|
||||
email: user.email,
|
||||
|
||||
+69
-75
@@ -7,8 +7,6 @@
|
||||
const util = require('./util');
|
||||
const program = require('commander');
|
||||
const inquirer = require('inquirer');
|
||||
const { graphql } = require('graphql');
|
||||
const helpers = require('../services/migration/helpers');
|
||||
const { stripIndent } = require('common-tags');
|
||||
const Table = require('cli-table');
|
||||
|
||||
@@ -21,31 +19,15 @@ inquirer.registerPrompt(
|
||||
require('inquirer-autocomplete-prompt')
|
||||
);
|
||||
|
||||
const schema = require('../graph/schema');
|
||||
const Context = require('../graph/context');
|
||||
const UsersService = require('../services/users');
|
||||
const UserModel = require('../models/user');
|
||||
const CommentModel = require('../models/comment');
|
||||
const ActionModel = require('../models/action');
|
||||
const USER_ROLES = require('../models/enum/user_roles');
|
||||
const mongoose = require('../services/mongoose');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
|
||||
/**
|
||||
* transforms a specific action to a removal action on the target model.
|
||||
*/
|
||||
const actionDecrTransformer = ({ item_id, action_type, group_id }) => ({
|
||||
query: { id: item_id },
|
||||
update: {
|
||||
$inc: {
|
||||
[`action_counts.${action_type.toLowerCase()}`]: -1,
|
||||
[`action_counts.${action_type.toLowerCase()}_${group_id.toLowerCase()}`]: -1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Deletes a user and cleans up their associated verifications.
|
||||
*/
|
||||
@@ -76,66 +58,29 @@ async function deleteUser(userID) {
|
||||
return util.shutdown();
|
||||
}
|
||||
|
||||
const { transformSingleWithCursor } = helpers({
|
||||
queryBatchSize: 10000,
|
||||
updateBatchSize: 10000,
|
||||
});
|
||||
const ctx = Context.forSystem();
|
||||
|
||||
console.warn("Removing user's actions");
|
||||
|
||||
// Remove all actions against comments.
|
||||
await transformSingleWithCursor(
|
||||
ActionModel.collection.find({ user_id: user.id, item_type: 'COMMENTS' }),
|
||||
actionDecrTransformer,
|
||||
CommentModel
|
||||
const { data, errors } = await ctx.graphql(
|
||||
`
|
||||
mutation DeleteUser($user_id: ID!) {
|
||||
delUser(id: $user_id) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ user_id: user.id }
|
||||
);
|
||||
if (errors) {
|
||||
throw errors;
|
||||
}
|
||||
|
||||
// Remove all actions against users.
|
||||
await transformSingleWithCursor(
|
||||
ActionModel.collection.find({ user_id: user.id, item_type: 'USERS' }),
|
||||
actionDecrTransformer,
|
||||
UserModel
|
||||
);
|
||||
if (data.errors) {
|
||||
throw data.errors;
|
||||
}
|
||||
|
||||
// Remove all the user's actions.
|
||||
await ActionModel.where({ user_id: user.id })
|
||||
.setOptions({ multi: true })
|
||||
.remove();
|
||||
|
||||
console.warn("Removing user's comments");
|
||||
|
||||
// Removes all the user's reply counts on each of the comments that they
|
||||
// have commented on.
|
||||
await transformSingleWithCursor(
|
||||
CommentModel.collection.aggregate([
|
||||
{ $match: { author_id: user.id } },
|
||||
{
|
||||
$group: {
|
||||
_id: '$parent_id',
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
]),
|
||||
({ _id: parent_id, count }) => ({
|
||||
query: { id: parent_id },
|
||||
update: {
|
||||
$inc: {
|
||||
reply_count: -1 * count,
|
||||
},
|
||||
},
|
||||
}),
|
||||
CommentModel
|
||||
);
|
||||
|
||||
// Remove all the user's comments.
|
||||
await CommentModel.where({ author_id: user.id })
|
||||
.setOptions({ multi: true })
|
||||
.remove();
|
||||
|
||||
console.warn('Removing the user');
|
||||
|
||||
// Remove the user.
|
||||
await user.remove();
|
||||
console.log('User was deleted.');
|
||||
|
||||
util.shutdown();
|
||||
} catch (err) {
|
||||
@@ -197,7 +142,7 @@ async function searchUsers() {
|
||||
value = '';
|
||||
}
|
||||
|
||||
const { data, errors } = await graphql(schema, searchQuery, {}, ctx, {
|
||||
const { data, errors } = await ctx.graphql(searchQuery, {
|
||||
value,
|
||||
});
|
||||
if (errors && errors.length > 0) {
|
||||
@@ -312,10 +257,59 @@ async function verifyUserEmail(userID, email) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* createUser will prompt the user for the user information when creating a
|
||||
* local user.
|
||||
*/
|
||||
async function createUser() {
|
||||
try {
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
name: 'email',
|
||||
message: 'Email',
|
||||
},
|
||||
{
|
||||
name: 'username',
|
||||
message: 'Username',
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
type: 'password',
|
||||
},
|
||||
{
|
||||
name: 'role',
|
||||
message: 'Role',
|
||||
type: 'list',
|
||||
choices: USER_ROLES,
|
||||
},
|
||||
]);
|
||||
|
||||
const { email, username, password, role } = answers;
|
||||
|
||||
// Create the user.
|
||||
const user = await UsersService.createLocalUser(email, password, username);
|
||||
|
||||
// Set the role.
|
||||
await UsersService.setRole(user.id, role);
|
||||
|
||||
console.log(`Created User[${user.id}]`);
|
||||
util.shutdown(0);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
program
|
||||
.command('create')
|
||||
.description('creates a local user')
|
||||
.action(createUser);
|
||||
|
||||
program
|
||||
.command('delete <userID>')
|
||||
.description('delete a user')
|
||||
|
||||
@@ -12,7 +12,7 @@ class MyPluginComponent extends React.Component {
|
||||
|
||||
<small>
|
||||
To read more about plugins check{' '}
|
||||
<a href="https://coralproject.github.io/talk/plugins-client.html">
|
||||
<a href="https://docs.coralproject.net/talk/plugins-client">
|
||||
our docs and guides!
|
||||
</a>
|
||||
</small>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
};
|
||||
```
|
||||
|
||||
To read more info on how to build client plugins. Please, go to: https://coralproject.github.io/talk/plugins-client.html
|
||||
To read more info on how to build client plugins. Please, go to: https://docs.coralproject.net/talk/plugins-client
|
||||
*/
|
||||
|
||||
import MyPluginComponent from './components/MyPluginComponent';
|
||||
|
||||
+5
-8
@@ -2,6 +2,7 @@
|
||||
require('../services/env');
|
||||
|
||||
const debug = require('debug')('talk:util');
|
||||
const { uniq } = require('lodash');
|
||||
|
||||
const util = (module.exports = {});
|
||||
|
||||
@@ -23,11 +24,7 @@ util.shutdown = (defaultCode = 0, signal = null) => {
|
||||
|
||||
debug(`${util.toshutdown.length} jobs now being called`);
|
||||
|
||||
Promise.all(
|
||||
util.toshutdown
|
||||
.map(func => (func ? func(signal) : null))
|
||||
.filter(func => func)
|
||||
)
|
||||
Promise.all(util.toshutdown.map(func => (func ? func(signal) : null)))
|
||||
.then(() => {
|
||||
debug('Shutdown complete, now exiting');
|
||||
process.exit(defaultCode);
|
||||
@@ -49,14 +46,14 @@ util.onshutdown = jobs => {
|
||||
debug(`${jobs.length} jobs registered to be called during shutdown`);
|
||||
|
||||
// Add the new jobs to shutdown to the object reference.
|
||||
util.toshutdown = util.toshutdown.concat(jobs);
|
||||
util.toshutdown = uniq(util.toshutdown.concat(jobs));
|
||||
};
|
||||
|
||||
// Attach to the SIGTERM + SIGINT handles to ensure a clean shutdown in the
|
||||
// event that we have an external event. SIGUSR2 is called when the app is asked
|
||||
// to be 'killed', same procedure here.
|
||||
process.on('SIGTERM', () => util.shutdown(0, 'SIGTERM'));
|
||||
process.on('SIGINT', () => util.shutdown(0, 'SIGINT'));
|
||||
process.once('SIGTERM', () => util.shutdown(0, 'SIGTERM'));
|
||||
process.once('SIGINT', () => util.shutdown(0, 'SIGINT'));
|
||||
process.once('SIGUSR2', () => util.shutdown(0, 'SIGUSR2'));
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
machine:
|
||||
node:
|
||||
version: 8
|
||||
services:
|
||||
- docker
|
||||
- redis
|
||||
environment:
|
||||
PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin"
|
||||
NODE_ENV: "test"
|
||||
MOCHA_FILE: "${CIRCLE_TEST_REPORTS}/junit/test-results.xml"
|
||||
MOCHA_REPORTER: "mocha-junit-reporter"
|
||||
pre:
|
||||
# TODO: use the following to add in support for MongoDB 3.4.
|
||||
# # Upgrade the database version to 3.4.
|
||||
# - sudo apt-get purge mongodb-org*
|
||||
# - sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 0C49F3730359A14518585931BC711F9BA15703C6
|
||||
# - echo "deb [ arch=amd64 ] http://repo.mongodb.org/apt/ubuntu precise/mongodb-org/3.4 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.4.list
|
||||
# - sudo apt-get update
|
||||
# - sudo apt-get install -y mongodb-org
|
||||
# - sudo service mongod restart
|
||||
|
||||
# Force sync the time.
|
||||
- date
|
||||
- sudo service ntp stop
|
||||
- sudo ntpdate -s time.nist.gov
|
||||
- sudo service ntp start
|
||||
- date
|
||||
|
||||
# Install chromium for e2e and remove old google-chrome
|
||||
- sudo rm -rf /opt/google/chrome
|
||||
- sudo rm -f /usr/bin/google-chrome*
|
||||
- sudo apt-get update
|
||||
- sudo apt-get install chromium-browser
|
||||
|
||||
dependencies:
|
||||
override:
|
||||
|
||||
# Install node dependencies.
|
||||
- yarn --version
|
||||
- yarn global add node-gyp --force
|
||||
- yarn
|
||||
|
||||
post:
|
||||
# Build the static assets.
|
||||
- yarn build
|
||||
# Lint the project here, before tests are ran.
|
||||
- yarn lint
|
||||
|
||||
database:
|
||||
post:
|
||||
# Initialize the settings in the database, this will create indicies for the
|
||||
# database.
|
||||
- ./bin/cli setup --defaults
|
||||
- sleep 2
|
||||
|
||||
test:
|
||||
override:
|
||||
# Run the tests using the junit reporter.
|
||||
- yarn test
|
||||
# Run the end to end tests
|
||||
- yarn e2e:ci
|
||||
|
||||
deployment:
|
||||
release:
|
||||
tag: /v[0-9]+(\.[0-9]+)*/
|
||||
commands:
|
||||
- bash ./scripts/docker.sh deploy
|
||||
|
||||
latest:
|
||||
branch:
|
||||
- master
|
||||
- next
|
||||
owner: coralproject
|
||||
commands:
|
||||
- bash ./scripts/docker.sh deploy
|
||||
@@ -2,10 +2,15 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Router, Route, IndexRedirect, IndexRoute } from 'react-router';
|
||||
|
||||
import Configure from 'routes/Configure';
|
||||
import Install from 'routes/Install';
|
||||
import Stories from 'routes/Stories';
|
||||
import Community from 'routes/Community';
|
||||
|
||||
import Configure from 'routes/Configure';
|
||||
import StreamSettings from './routes/Configure/containers/StreamSettings';
|
||||
import ModerationSettings from './routes/Configure/containers/ModerationSettings';
|
||||
import TechSettings from './routes/Configure/containers/TechSettings';
|
||||
|
||||
import { ModerationLayout, Moderation } from 'routes/Moderation';
|
||||
|
||||
import Layout from 'containers/Layout';
|
||||
@@ -15,7 +20,14 @@ const routes = (
|
||||
<Route exact path="/admin/install" component={Install} />
|
||||
<Route path="/admin" component={Layout}>
|
||||
<IndexRedirect to="/admin/moderate" />
|
||||
<Route path="configure" component={Configure} />
|
||||
|
||||
<Route path="configure" component={Configure}>
|
||||
<Route path="stream" component={StreamSettings} />
|
||||
<Route path="moderation" component={ModerationSettings} />
|
||||
<Route path="tech" component={TechSettings} />
|
||||
<IndexRedirect to="stream" />
|
||||
</Route>
|
||||
|
||||
<Route path="stories" component={Stories} />
|
||||
|
||||
{/* Community Routes */}
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
import bowser from 'bowser';
|
||||
import * as actions from '../constants/auth';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
|
||||
//==============================================================================
|
||||
// SIGN IN
|
||||
//==============================================================================
|
||||
|
||||
export const handleLogin = (email, password, recaptchaResponse) => (
|
||||
dispatch,
|
||||
_,
|
||||
{ rest, client, localStorage }
|
||||
) => {
|
||||
dispatch({ type: actions.LOGIN_REQUEST });
|
||||
|
||||
const params = {
|
||||
method: 'POST',
|
||||
body: {
|
||||
email,
|
||||
password,
|
||||
},
|
||||
};
|
||||
|
||||
if (recaptchaResponse) {
|
||||
params.headers = {
|
||||
'X-Recaptcha-Response': recaptchaResponse,
|
||||
};
|
||||
}
|
||||
|
||||
return rest('/auth/local', params)
|
||||
.then(({ user, token }) => {
|
||||
if (!user) {
|
||||
if (!bowser.safari && !bowser.ios && localStorage) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('exp');
|
||||
}
|
||||
return dispatch(checkLoginFailure('not logged in'));
|
||||
}
|
||||
|
||||
dispatch(handleAuthToken(token));
|
||||
client.resetWebsocket();
|
||||
dispatch(checkLoginSuccess(user));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
|
||||
if (error.translation_key === 'NOT_AUTHORIZED') {
|
||||
// invalid credentials
|
||||
dispatch({
|
||||
type: actions.LOGIN_FAILURE,
|
||||
message: t('error.email_password'),
|
||||
});
|
||||
} else if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
|
||||
dispatch({
|
||||
type: actions.LOGIN_MAXIMUM_EXCEEDED,
|
||||
message: t(`error.${error.translation_key}`),
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: actions.LOGIN_FAILURE,
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// FORGOT PASSWORD
|
||||
//==============================================================================
|
||||
|
||||
const forgotPasswordRequest = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_REQUEST,
|
||||
});
|
||||
|
||||
const forgotPasswordSuccess = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS,
|
||||
});
|
||||
|
||||
const forgotPasswordFailure = error => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const requestPasswordReset = email => (dispatch, _, { rest }) => {
|
||||
dispatch(forgotPasswordRequest(email));
|
||||
const redirectUri = location.href;
|
||||
|
||||
return rest('/account/password/reset', {
|
||||
method: 'POST',
|
||||
body: { email, loc: redirectUri },
|
||||
})
|
||||
.then(() => dispatch(forgotPasswordSuccess()))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch(forgotPasswordFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// CHECK LOGIN
|
||||
//==============================================================================
|
||||
|
||||
const checkLoginRequest = () => ({
|
||||
type: actions.CHECK_LOGIN_REQUEST,
|
||||
});
|
||||
|
||||
const checkLoginSuccess = (user, isAdmin) => ({
|
||||
type: actions.CHECK_LOGIN_SUCCESS,
|
||||
user,
|
||||
isAdmin,
|
||||
});
|
||||
|
||||
const checkLoginFailure = error => ({
|
||||
type: actions.CHECK_LOGIN_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const checkLogin = () => (
|
||||
dispatch,
|
||||
_,
|
||||
{ rest, client, localStorage }
|
||||
) => {
|
||||
dispatch(checkLoginRequest());
|
||||
return rest('/auth')
|
||||
.then(({ user }) => {
|
||||
if (!user) {
|
||||
if (!bowser.safari && !bowser.ios && localStorage) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('exp');
|
||||
}
|
||||
return dispatch(checkLoginFailure('not logged in'));
|
||||
}
|
||||
|
||||
client.resetWebsocket();
|
||||
dispatch(checkLoginSuccess(user));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch(checkLoginFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// LOGOUT
|
||||
//==============================================================================
|
||||
|
||||
export const logout = () => (dispatch, _, { rest, client, localStorage }) => {
|
||||
return rest('/auth', { method: 'DELETE' }).then(() => {
|
||||
if (localStorage) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('exp');
|
||||
}
|
||||
|
||||
// Reset the websocket.
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch({ type: actions.LOGOUT });
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// AUTH TOKEN
|
||||
//==============================================================================
|
||||
|
||||
export const handleAuthToken = token => (dispatch, _, { localStorage }) => {
|
||||
if (localStorage) {
|
||||
localStorage.setItem('exp', jwtDecode(token).exp);
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
dispatch({ type: 'HANDLE_AUTH_TOKEN' });
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
export const CONFIG_UPDATED = 'CONFIG_UPDATED';
|
||||
|
||||
export const fetchConfig = () => dispatch => {
|
||||
let json = document.getElementById('data');
|
||||
let data = JSON.parse(json.textContent);
|
||||
dispatch({ type: CONFIG_UPDATED, data });
|
||||
};
|
||||
@@ -8,6 +8,10 @@ export const clearPending = () => {
|
||||
return { type: actions.CLEAR_PENDING };
|
||||
};
|
||||
|
||||
export const setActiveSection = section => {
|
||||
return { type: actions.SET_ACTIVE_SECTION, section };
|
||||
export const showSaveDialog = () => {
|
||||
return { type: actions.SHOW_SAVE_DIALOG };
|
||||
};
|
||||
|
||||
export const hideSaveDialog = () => {
|
||||
return { type: actions.HIDE_SAVE_DIALOG };
|
||||
};
|
||||
|
||||
@@ -5,14 +5,7 @@ export const singleView = () => ({ type: actions.SINGLE_VIEW });
|
||||
|
||||
// hide shortcuts note
|
||||
export const hideShortcutsNote = () => (dispatch, _, { localStorage }) => {
|
||||
try {
|
||||
if (localStorage) {
|
||||
localStorage.setItem('coral:shortcutsNote', 'hide');
|
||||
}
|
||||
} catch (e) {
|
||||
// above will fail in Safari private mode
|
||||
}
|
||||
|
||||
localStorage.setItem('coral:shortcutsNote', 'hide');
|
||||
dispatch({ type: actions.HIDE_SHORTCUTS_NOTE });
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.headerRowItem {
|
||||
color: #595959;
|
||||
font-weight: bold;
|
||||
|
||||
@@ -5,13 +5,16 @@ import styles from './AccountHistory.css';
|
||||
import cn from 'classnames';
|
||||
import flatten from 'lodash/flatten';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import has from 'lodash/has';
|
||||
import moment from 'moment';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { Icon } from 'coral-ui';
|
||||
|
||||
const buildUserHistory = (userState = {}) => {
|
||||
return orderBy(
|
||||
flatten(
|
||||
Object.keys(userState.status)
|
||||
.filter(k => k !== '__typename')
|
||||
.filter(k => !k.startsWith('__'))
|
||||
.map(k => userState.status[k].history)
|
||||
),
|
||||
'created_at',
|
||||
@@ -19,25 +22,49 @@ const buildUserHistory = (userState = {}) => {
|
||||
);
|
||||
};
|
||||
|
||||
const buildActionResponse = (typename, until, status) => {
|
||||
/** readableDuration returns a readable duration of the suspension/ban in hours or days
|
||||
* @param {} startDate
|
||||
* @param {} endDate
|
||||
*/
|
||||
const readableDuration = (startDate, endDate) => {
|
||||
const dur = moment.duration(moment(endDate).diff(moment(startDate)));
|
||||
const durAsDays = dur.asDays().toFixed(0);
|
||||
const durAsHours = dur.asHours().toFixed(0);
|
||||
|
||||
return durAsHours > 23
|
||||
? durAsDays > 1
|
||||
? t('suspenduser.days', durAsDays)
|
||||
: t('suspenduser.day', durAsDays)
|
||||
: durAsHours > 1
|
||||
? t('suspenduser.hours', durAsHours)
|
||||
: t('suspenduser.hour', durAsHours);
|
||||
};
|
||||
|
||||
const buildActionResponse = (typename, created_at, until, status) => {
|
||||
switch (typename) {
|
||||
case 'UsernameStatusHistory':
|
||||
return `Username ${status}`;
|
||||
return t('account_history.username_status', status);
|
||||
case 'BannedStatusHistory':
|
||||
return status ? 'User banned' : 'Ban removed';
|
||||
return status
|
||||
? t('account_history.user_banned')
|
||||
: t('account_history.ban_removed');
|
||||
case 'SuspensionStatusHistory':
|
||||
return until ? 'Account Suspended' : 'Suspension removed';
|
||||
return until
|
||||
? t('account_history.suspended', readableDuration(created_at, until))
|
||||
: t('account_history.suspension_removed');
|
||||
default:
|
||||
return '-';
|
||||
}
|
||||
};
|
||||
|
||||
const getModerationValue = (userId, assignedBy = {}) => {
|
||||
if (assignedBy && userId !== assignedBy.id) {
|
||||
return assignedBy.username;
|
||||
}
|
||||
return 'SYSTEM';
|
||||
};
|
||||
const getModerationValue = assignedBy =>
|
||||
has(assignedBy, 'username') ? (
|
||||
assignedBy.username
|
||||
) : (
|
||||
<span>
|
||||
<Icon name="computer" /> {t('account_history.system')}
|
||||
</span>
|
||||
);
|
||||
|
||||
class AccountHistory extends React.Component {
|
||||
render() {
|
||||
@@ -52,9 +79,15 @@ class AccountHistory extends React.Component {
|
||||
'talk-admin-account-history-header-row'
|
||||
)}
|
||||
>
|
||||
<div className={styles.headerRowItem}>Date</div>
|
||||
<div className={styles.headerRowItem}>Action</div>
|
||||
<div className={styles.headerRowItem}>Moderation</div>
|
||||
<div className={styles.headerRowItem}>
|
||||
{t('account_history.date')}
|
||||
</div>
|
||||
<div className={styles.headerRowItem}>
|
||||
{t('account_history.action')}
|
||||
</div>
|
||||
<div className={styles.headerRowItem}>
|
||||
{t('account_history.taken_by')}
|
||||
</div>
|
||||
</div>
|
||||
{userHistory.map(
|
||||
({ __typename, created_at, assigned_by, until, status }) => (
|
||||
@@ -77,15 +110,16 @@ class AccountHistory extends React.Component {
|
||||
'talk-admin-account-history-row-status'
|
||||
)}
|
||||
>
|
||||
{buildActionResponse(__typename, until, status)}
|
||||
{buildActionResponse(__typename, created_at, until, status)}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
styles.item,
|
||||
styles.username,
|
||||
'talk-admin-account-history-row-assigned-by'
|
||||
)}
|
||||
>
|
||||
{getModerationValue(user.id, assigned_by)}
|
||||
{getModerationValue(assigned_by)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -25,37 +25,29 @@ class CommentDetails extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { data, root, comment, clearHeightCache } = this.props;
|
||||
const { root, comment, clearHeightCache } = this.props;
|
||||
const { showDetail } = this.state;
|
||||
const queryData = {
|
||||
|
||||
const slotPassthrough = {
|
||||
clearHeightCache,
|
||||
root,
|
||||
comment,
|
||||
more: showDetail,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<IfSlotIsNotEmpty
|
||||
queryData={queryData}
|
||||
slot={['adminCommentMoreDetails', 'adminCommentMoreFlagDetails']}
|
||||
passthrough={slotPassthrough}
|
||||
>
|
||||
<a onClick={this.toggleDetail} className={styles.moreDetail}>
|
||||
{showDetail ? t('modqueue.less_detail') : t('modqueue.more_detail')}
|
||||
</a>
|
||||
</IfSlotIsNotEmpty>
|
||||
<Slot
|
||||
fill="adminCommentDetailArea"
|
||||
data={data}
|
||||
clearHeightCache={clearHeightCache}
|
||||
queryData={queryData}
|
||||
more={showDetail}
|
||||
/>
|
||||
<Slot fill="adminCommentDetailArea" passthrough={slotPassthrough} />
|
||||
{showDetail && (
|
||||
<Slot
|
||||
fill="adminCommentMoreDetails"
|
||||
data={data}
|
||||
clearHeightCache={clearHeightCache}
|
||||
queryData={queryData}
|
||||
/>
|
||||
<Slot fill="adminCommentMoreDetails" passthrough={slotPassthrough} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -63,7 +55,6 @@ class CommentDetails extends Component {
|
||||
}
|
||||
|
||||
CommentDetails.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
comment: PropTypes.object.isRequired,
|
||||
clearHeightCache: PropTypes.func,
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { matchLinks } from '../utils';
|
||||
import memoize from 'lodash/memoize';
|
||||
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
|
||||
// generate a regulare expression that catches the `phrases`.
|
||||
function generateRegExp(phrases) {
|
||||
const inner = phrases
|
||||
.map(phrase =>
|
||||
phrase
|
||||
.split(/\s+/)
|
||||
.map(word => escapeRegExp(word))
|
||||
.join('[\\s"?!.]+')
|
||||
)
|
||||
.join('|');
|
||||
|
||||
const pattern = `(^|[^\\w])(${inner})(?=[^\\w]|$)`;
|
||||
try {
|
||||
return new RegExp(pattern, 'iu');
|
||||
} catch (_err) {
|
||||
// IE does not support unicode support, so we'll create one without.
|
||||
return new RegExp(pattern, 'i');
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a regular expression detecting `suspectWords` and `bannedWords` phrases.
|
||||
function getPhrasesRegexp(suspectWords, bannedWords) {
|
||||
return generateRegExp([...suspectWords, ...bannedWords]);
|
||||
}
|
||||
|
||||
// Memoized version as arguments rarely change.
|
||||
const getPhrasesRegexpMemoized = memoize(getPhrasesRegexp);
|
||||
|
||||
// markPhrases looks for `supsectWords` and `bannedWords` inside `body` and highlights them by returning
|
||||
// an array of React Elements.
|
||||
function markPhrases(body, suspectWords, bannedWords, keyPrefix) {
|
||||
const regexp = getPhrasesRegexpMemoized(suspectWords, bannedWords);
|
||||
const tokens = body.split(regexp);
|
||||
return tokens.map(
|
||||
(token, i) =>
|
||||
i % 3 === 2 ? <mark key={`${keyPrefix}_${i}`}>{token}</mark> : token
|
||||
);
|
||||
}
|
||||
|
||||
// markLinks looks for links inside `body` and highlights them by returning
|
||||
// an array of React Elements.
|
||||
function markLinks(body) {
|
||||
const matches = matchLinks(body);
|
||||
const content = [];
|
||||
let index = 0;
|
||||
if (matches) {
|
||||
matches.forEach((match, i) => {
|
||||
content.push(body.substring(index, match.index));
|
||||
content.push(<mark key={i}>{match.text}</mark>);
|
||||
index = match.lastIndex;
|
||||
});
|
||||
}
|
||||
content.push(body.substring(index));
|
||||
return content;
|
||||
}
|
||||
|
||||
const CommentFormatter = ({
|
||||
body,
|
||||
suspectWords,
|
||||
bannedWords,
|
||||
className = 'comment',
|
||||
...rest
|
||||
}) => {
|
||||
// Breaking the body by line break
|
||||
const textbreaks = body.split('\n');
|
||||
|
||||
return (
|
||||
<span className={`${className}-text`} {...rest}>
|
||||
{textbreaks.map((line, i) => {
|
||||
const content = markLinks(line).map((element, index) => {
|
||||
// Keep highlighted links.
|
||||
if (typeof element !== 'string') {
|
||||
return element;
|
||||
}
|
||||
|
||||
// Highlight suspect and banned phrase inside this part of text.
|
||||
return markPhrases(element, suspectWords, bannedWords, index);
|
||||
});
|
||||
|
||||
return (
|
||||
<span key={i} className={`${className}-line`}>
|
||||
{content}
|
||||
{i !== textbreaks.length - 1 && (
|
||||
<br className={`${className}-linebreak`} />
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
CommentFormatter.propTypes = {
|
||||
className: PropTypes.string,
|
||||
bannedWords: PropTypes.array,
|
||||
suspectWords: PropTypes.array,
|
||||
body: PropTypes.string,
|
||||
};
|
||||
|
||||
export default CommentFormatter;
|
||||
@@ -43,6 +43,9 @@ const CommentLabels = ({
|
||||
comment,
|
||||
comment: { className, status, actions, hasParent },
|
||||
}) => {
|
||||
const slotPassthrough = {
|
||||
comment,
|
||||
};
|
||||
return (
|
||||
<div className={cn(className, styles.root)}>
|
||||
<div className={styles.coreLabels}>
|
||||
@@ -69,7 +72,7 @@ const CommentLabels = ({
|
||||
<Slot
|
||||
className={styles.slot}
|
||||
fill="adminCommentLabels"
|
||||
queryData={{ comment }}
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -7,12 +7,12 @@ import t from 'coral-framework/services/i18n';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import cn from 'classnames';
|
||||
|
||||
const CoralDrawer = ({ handleLogout, auth = {} }) => (
|
||||
const CoralDrawer = ({ handleLogout, currentUser }) => (
|
||||
<Drawer className={cn('talk-admin-drawer-nav', styles.drawer)}>
|
||||
{auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? (
|
||||
{currentUser && can(currentUser, 'ACCESS_ADMIN') ? (
|
||||
<div>
|
||||
<Navigation className={styles.nav}>
|
||||
{can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
{can(currentUser, 'MODERATE_COMMENTS') && (
|
||||
<IndexLink
|
||||
className={cn('talk-admin-nav-moderate', styles.navLink)}
|
||||
to="/admin/moderate"
|
||||
@@ -35,7 +35,7 @@ const CoralDrawer = ({ handleLogout, auth = {} }) => (
|
||||
>
|
||||
{t('configure.community')}
|
||||
</Link>
|
||||
{can(auth.user, 'UPDATE_CONFIG') && (
|
||||
{can(currentUser, 'UPDATE_CONFIG') && (
|
||||
<Link
|
||||
className={cn('talk-admin-nav-configure', styles.navLink)}
|
||||
to="/admin/configure"
|
||||
@@ -55,7 +55,7 @@ const CoralDrawer = ({ handleLogout, auth = {} }) => (
|
||||
CoralDrawer.propTypes = {
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
restricted: PropTypes.bool, // hide app elements from a logged out user
|
||||
auth: PropTypes.object,
|
||||
currentUser: PropTypes.object,
|
||||
};
|
||||
|
||||
export default CoralDrawer;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.copy {
|
||||
padding: 20px 0;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import styles from './Forbidden.css';
|
||||
|
||||
const Forbidden = () => (
|
||||
<div className={styles.container}>
|
||||
<p className={styles.copy}>
|
||||
This page is for team use only. Please contact an administrator if you
|
||||
want to join this team.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Forbidden;
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
.header, .cta, .success {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.success {
|
||||
cursor: pointer;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
|
||||
.signInLink {
|
||||
color: blue;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.signInLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './ForgotPassword.css';
|
||||
import { Button, TextField, Alert, Success } from 'coral-ui';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class ForgotPassword extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
handleEmailChange = e => this.props.onEmailChange(e.target.value);
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
handleSignInLink = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSignInLink();
|
||||
};
|
||||
|
||||
renderSuccess() {
|
||||
return (
|
||||
<div className={styles.success} onClick={this.handleSignInLink}>
|
||||
{t('password_reset.mail_sent')}{' '}
|
||||
<a
|
||||
className={styles.signInLink}
|
||||
href="#"
|
||||
onClick={this.handleSignInLink}
|
||||
>
|
||||
Sign in
|
||||
</a>
|
||||
<Success />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderForm() {
|
||||
const { email, errorMessage } = this.props;
|
||||
return (
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
label="Email Address"
|
||||
value={email}
|
||||
onChange={this.handleEmailChange}
|
||||
/>
|
||||
<Button type="submit" cStyle="black" full>
|
||||
Reset Password
|
||||
</Button>
|
||||
<p className={styles.cta}>
|
||||
Go back to{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.signInLink}
|
||||
onClick={this.handleSignInLink}
|
||||
>
|
||||
Sign In
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.success ? this.renderSuccess() : this.renderForm();
|
||||
}
|
||||
}
|
||||
|
||||
ForgotPassword.propTypes = {
|
||||
success: PropTypes.bool.isRequired,
|
||||
email: PropTypes.string.isRequired,
|
||||
onEmailChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
onSignInLink: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ForgotPassword;
|
||||
@@ -13,7 +13,7 @@ import CommunityIndicator from '../routes/Community/containers/Indicator';
|
||||
const CoralHeader = ({
|
||||
handleLogout,
|
||||
showShortcuts = () => {},
|
||||
auth,
|
||||
currentUser,
|
||||
root,
|
||||
data,
|
||||
}) => {
|
||||
@@ -22,9 +22,9 @@ const CoralHeader = ({
|
||||
<Header className={styles.header}>
|
||||
<Logo className={styles.logo} />
|
||||
<div>
|
||||
{auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ? (
|
||||
{currentUser && can(currentUser, 'ACCESS_ADMIN') ? (
|
||||
<Navigation className={styles.nav}>
|
||||
{can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
{can(currentUser, 'MODERATE_COMMENTS') && (
|
||||
<IndexLink
|
||||
id="moderateNav"
|
||||
className={cn('talk-admin-nav-moderate', styles.navLink)}
|
||||
@@ -54,7 +54,7 @@ const CoralHeader = ({
|
||||
<CommunityIndicator root={root} data={data} />
|
||||
</Link>
|
||||
|
||||
{can(auth.user, 'UPDATE_CONFIG') && (
|
||||
{can(currentUser, 'UPDATE_CONFIG') && (
|
||||
<Link
|
||||
id="configureNav"
|
||||
className={cn('talk-admin-nav-configure', styles.navLink)}
|
||||
@@ -97,12 +97,14 @@ const CoralHeader = ({
|
||||
Report a bug or give feedback
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={handleLogout}
|
||||
className="talk-admin-header-sign-out"
|
||||
>
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
{currentUser && (
|
||||
<MenuItem
|
||||
onClick={handleLogout}
|
||||
className="talk-admin-header-sign-out"
|
||||
>
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
@@ -116,7 +118,7 @@ const CoralHeader = ({
|
||||
};
|
||||
|
||||
CoralHeader.propTypes = {
|
||||
auth: PropTypes.object,
|
||||
currentUser: PropTypes.object,
|
||||
showShortcuts: PropTypes.func,
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { matchLinks } from '../utils';
|
||||
import matchLinks from 'coral-framework/utils/matchLinks';
|
||||
|
||||
export default ({ text, children }) => {
|
||||
const hasLinks = !!matchLinks(text);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
.layout {
|
||||
margin: 0 auto;
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
height: inherit;
|
||||
min-height: calc(100vh - 58px);
|
||||
}
|
||||
@@ -10,22 +10,26 @@ const Layout = ({
|
||||
handleLogout = () => {},
|
||||
toggleShortcutModal = () => {},
|
||||
restricted = false,
|
||||
auth,
|
||||
currentUser,
|
||||
}) => (
|
||||
<LayoutMDL className={styles.layout} fixedDrawer>
|
||||
<Header
|
||||
handleLogout={handleLogout}
|
||||
showShortcuts={toggleShortcutModal}
|
||||
auth={auth}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
<Drawer
|
||||
handleLogout={handleLogout}
|
||||
restricted={restricted}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
<Drawer handleLogout={handleLogout} restricted={restricted} auth={auth} />
|
||||
<div className={styles.layout}>{children}</div>
|
||||
</LayoutMDL>
|
||||
);
|
||||
|
||||
Layout.propTypes = {
|
||||
children: PropTypes.node,
|
||||
auth: PropTypes.object,
|
||||
currentUser: PropTypes.object,
|
||||
handleLogout: PropTypes.func,
|
||||
toggleShortcutModal: PropTypes.func,
|
||||
restricted: PropTypes.bool, // hide elements from a user that's logged out
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
.layout {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header, .cta {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.layout h1 {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import React, { Component } from 'react';
|
||||
import SignIn from '../containers/SignIn';
|
||||
import ForgotPassword from '../containers/ForgotPassword';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Login.css';
|
||||
import Layout from 'coral-admin/src/components/Layout';
|
||||
import cn from 'classnames';
|
||||
|
||||
class LoginContainer extends Component {
|
||||
renderForm() {
|
||||
return this.props.forgotPassword ? (
|
||||
<ForgotPassword onSignInLink={this.props.onSignInLink} />
|
||||
) : (
|
||||
<SignIn onForgotPasswordLink={this.props.onForgotPasswordLink} />
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Layout fixedDrawer restricted={true}>
|
||||
<div className={cn(styles.layout, 'talk-admin-login')}>
|
||||
<h1 className={styles.header}>Team sign in</h1>
|
||||
<p className={styles.cta}>Sign in to interact with your community.</p>
|
||||
{this.renderForm()}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LoginContainer.propTypes = {
|
||||
forgotPassword: PropTypes.bool.isRequired,
|
||||
onForgotPasswordLink: PropTypes.func.isRequired,
|
||||
onSignInLink: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default LoginContainer;
|
||||
@@ -3,35 +3,3 @@
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loginLayout {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loginHeader, .loginCTA, .forgotPasswordCTA, .passwordRequestSuccess {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.forgotPasswordLink, .signInLink {
|
||||
color: blue;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.forgotPasswordLink:hover, .signInLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.layout h1 {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.loginHeader {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.passwordRequestSuccess {
|
||||
cursor: pointer;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
.forgotPasswordCTA {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.forgotPasswordLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.forgotPasswordLink {
|
||||
color: blue;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.recaptcha {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.signInButton {
|
||||
margin-top: 10px;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './SignIn.css';
|
||||
import { Button, TextField, Alert } from 'coral-ui';
|
||||
import cn from 'classnames';
|
||||
import Recaptcha from 'coral-framework/components/Recaptcha';
|
||||
|
||||
class SignIn extends React.Component {
|
||||
recaptcha = null;
|
||||
|
||||
handleForgotPasswordLink = e => {
|
||||
e.preventDefault();
|
||||
this.props.onForgotPasswordLink();
|
||||
};
|
||||
handleEmailChange = e => this.props.onEmailChange(e.target.value);
|
||||
handlePasswordChange = e => this.props.onPasswordChange(e.target.value);
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit();
|
||||
|
||||
// Reset recaptcha because each response can only
|
||||
// be used once.
|
||||
if (this.recaptcha) {
|
||||
this.recaptcha.reset();
|
||||
}
|
||||
};
|
||||
|
||||
handleRecaptchaRef = ref => {
|
||||
this.recaptcha = ref;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { email, password, errorMessage, requireRecaptcha } = this.props;
|
||||
return (
|
||||
<form className="talk-admin-login-sign-in" onSubmit={this.handleSubmit}>
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
id="email"
|
||||
label="Email Address"
|
||||
value={email}
|
||||
onChange={this.handleEmailChange}
|
||||
/>
|
||||
<TextField
|
||||
id="password"
|
||||
label="Password"
|
||||
value={password}
|
||||
onChange={this.handlePasswordChange}
|
||||
type="password"
|
||||
/>
|
||||
{requireRecaptcha && (
|
||||
<div className={styles.recaptcha}>
|
||||
<Recaptcha
|
||||
ref={this.handleRecaptchaRef}
|
||||
onVerify={this.props.onRecaptchaVerify}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
className={cn(styles.signInButton, 'talk-admin-login-sign-in-button')}
|
||||
type="submit"
|
||||
cStyle="black"
|
||||
full
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
<p className={styles.forgotPasswordCTA}>
|
||||
Forgot your password?{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.forgotPasswordLink}
|
||||
onClick={this.handleForgotPasswordLink}
|
||||
>
|
||||
Request a new one.
|
||||
</a>
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SignIn.propTypes = {
|
||||
email: PropTypes.string.isRequired,
|
||||
password: PropTypes.string.isRequired,
|
||||
onEmailChange: PropTypes.func.isRequired,
|
||||
onPasswordChange: PropTypes.func.isRequired,
|
||||
onForgotPasswordLink: PropTypes.func.isRequired,
|
||||
onRecaptchaVerify: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
requireRecaptcha: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default SignIn;
|
||||
@@ -98,7 +98,6 @@ class UserDetail extends React.Component {
|
||||
|
||||
renderLoaded() {
|
||||
const {
|
||||
data,
|
||||
root,
|
||||
root: { me, user, totalComments, rejectedComments },
|
||||
activeTab,
|
||||
@@ -123,6 +122,11 @@ class UserDetail extends React.Component {
|
||||
const banned = isBanned(user);
|
||||
const suspended = isSuspended(user);
|
||||
|
||||
const slotPassthrough = {
|
||||
root,
|
||||
user,
|
||||
};
|
||||
|
||||
return (
|
||||
<ClickOutside onClickOutside={modal ? null : hideUserDetail}>
|
||||
<Drawer
|
||||
@@ -244,11 +248,7 @@ class UserDetail extends React.Component {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<Slot
|
||||
fill="userProfile"
|
||||
data={this.props.data}
|
||||
queryData={{ root, user }}
|
||||
/>
|
||||
<Slot fill="userProfile" passthrough={slotPassthrough} />
|
||||
|
||||
<hr />
|
||||
|
||||
@@ -301,7 +301,6 @@ class UserDetail extends React.Component {
|
||||
<UserDetailCommentList
|
||||
user={user}
|
||||
root={root}
|
||||
data={data}
|
||||
loadMore={loadMore}
|
||||
toggleSelect={toggleSelect}
|
||||
viewUserDetail={viewUserDetail}
|
||||
@@ -320,7 +319,6 @@ class UserDetail extends React.Component {
|
||||
<UserDetailCommentList
|
||||
user={user}
|
||||
root={root}
|
||||
data={data}
|
||||
loadMore={loadMore}
|
||||
toggleSelect={toggleSelect}
|
||||
viewUserDetail={viewUserDetail}
|
||||
@@ -368,9 +366,7 @@ UserDetail.propTypes = {
|
||||
bulkReject: PropTypes.func.isRequired,
|
||||
toggleSelectAll: PropTypes.func.isRequired,
|
||||
loading: PropTypes.bool.isRequired,
|
||||
data: PropTypes.shape({
|
||||
refetch: PropTypes.func.isRequired,
|
||||
}),
|
||||
data: PropTypes.object,
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
selectedCommentIds: PropTypes.array.isRequired,
|
||||
viewUserDetail: PropTypes.any.isRequired,
|
||||
|
||||
@@ -78,6 +78,12 @@
|
||||
font-weight: 300;
|
||||
font-size: 16px;
|
||||
max-width: 360px;
|
||||
|
||||
blockquote {
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
letter-spacing: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.sideActions {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import { Icon } from 'coral-ui';
|
||||
import CommentDetails from './CommentDetails';
|
||||
import styles from './UserDetailComment.css';
|
||||
import CommentFormatter from 'coral-admin/src/components/CommentFormatter';
|
||||
import AdminCommentContent from 'coral-framework/components/AdminCommentContent';
|
||||
import IfHasLink from 'coral-admin/src/components/IfHasLink';
|
||||
import cn from 'classnames';
|
||||
import CommentAnimatedEdit from './CommentAnimatedEdit';
|
||||
@@ -32,10 +32,17 @@ class UserDetailComment extends React.Component {
|
||||
selected,
|
||||
toggleSelect,
|
||||
className,
|
||||
data,
|
||||
root: { settings: { wordlist: { banned, suspect } } },
|
||||
root: { settings },
|
||||
} = this.props;
|
||||
|
||||
const slotPassthrough = {
|
||||
root,
|
||||
comment,
|
||||
suspectWords: settings.wordlist.suspect,
|
||||
bannedWords: settings.wordlist.banned,
|
||||
body: comment.body,
|
||||
};
|
||||
|
||||
return (
|
||||
<li
|
||||
tabIndex={0}
|
||||
@@ -68,7 +75,8 @@ class UserDetailComment extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.story}>
|
||||
Story: {comment.asset.title}
|
||||
{t('common.story')}:{' '}
|
||||
{comment.asset.title ? comment.asset.title : comment.asset.url}
|
||||
{
|
||||
<Link to={`/admin/moderate/${comment.asset.id}`}>
|
||||
{t('modqueue.moderate')}
|
||||
@@ -78,11 +86,15 @@ class UserDetailComment extends React.Component {
|
||||
<CommentAnimatedEdit body={comment.body}>
|
||||
<div className={styles.bodyContainer}>
|
||||
<div className={styles.body}>
|
||||
<CommentFormatter
|
||||
suspectWords={suspect}
|
||||
bannedWords={banned}
|
||||
body={comment.body}
|
||||
className="talk-admin-user-detail-comment"
|
||||
<Slot
|
||||
fill="userDetailCommentContent"
|
||||
className={cn(
|
||||
styles.commentContent,
|
||||
'talk-admin-user-detail-comment'
|
||||
)}
|
||||
size={1}
|
||||
defaultComponent={AdminCommentContent}
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
<a
|
||||
className={styles.external}
|
||||
@@ -95,6 +107,7 @@ class UserDetailComment extends React.Component {
|
||||
<div className={styles.sideActions}>
|
||||
<IfHasLink text={comment.body}>
|
||||
<span className={styles.hasLinks}>
|
||||
{/* TODO: translate string */}
|
||||
<Icon name="error_outline" /> Contains Link
|
||||
</span>
|
||||
</IfHasLink>
|
||||
@@ -114,7 +127,7 @@ class UserDetailComment extends React.Component {
|
||||
</div>
|
||||
</CommentAnimatedEdit>
|
||||
</div>
|
||||
<CommentDetails data={data} root={root} comment={comment} />
|
||||
<CommentDetails root={root} comment={comment} />
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -122,7 +135,6 @@ class UserDetailComment extends React.Component {
|
||||
|
||||
UserDetailComment.propTypes = {
|
||||
selected: PropTypes.bool,
|
||||
data: PropTypes.object,
|
||||
user: PropTypes.object.isRequired,
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
|
||||
@@ -9,7 +9,6 @@ import ApproveButton from './ApproveButton';
|
||||
|
||||
const UserDetailCommentList = props => {
|
||||
const {
|
||||
data,
|
||||
root,
|
||||
root: { user, comments: { nodes, hasNextPage } },
|
||||
acceptComment,
|
||||
@@ -70,7 +69,6 @@ const UserDetailCommentList = props => {
|
||||
key={comment.id}
|
||||
user={user}
|
||||
root={root}
|
||||
data={data}
|
||||
comment={comment}
|
||||
acceptComment={acceptComment}
|
||||
rejectComment={rejectComment}
|
||||
@@ -93,7 +91,6 @@ UserDetailCommentList.propTypes = {
|
||||
root: PropTypes.object.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
selectedCommentIds: PropTypes.array.isRequired,
|
||||
viewUserDetail: PropTypes.any.isRequired,
|
||||
loadMore: PropTypes.any.isRequired,
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
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;
|
||||
right: 0px;
|
||||
width: 140px;
|
||||
left: -100px;
|
||||
width: 200px;
|
||||
text-align: left;
|
||||
color: #616161;
|
||||
}
|
||||
@@ -39,7 +39,7 @@
|
||||
border: 10px solid transparent;
|
||||
border-top-color: #999;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
left: 96px;
|
||||
top: -20px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
@@ -49,7 +49,7 @@
|
||||
border: 10px solid transparent;
|
||||
border-top-color: white;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
left: 96px;
|
||||
top: -19px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ class UserInfoTooltip extends React.Component {
|
||||
new Date(
|
||||
this.getLastHistoryItem(user, 'banned').created_at
|
||||
)
|
||||
).format('MMMM Do YYYY, h:mm:ss a')}
|
||||
).format('MMM Do YYYY, h:mm:ss a')}
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
@@ -139,7 +139,7 @@ class UserInfoTooltip extends React.Component {
|
||||
'suspension'
|
||||
).created_at
|
||||
)
|
||||
).format('MMMM Do YYYY, h:mm:ss a')}
|
||||
).format('MMM Do YYYY, h:mm:ss a')}
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
@@ -154,7 +154,7 @@ class UserInfoTooltip extends React.Component {
|
||||
new Date(
|
||||
this.getLastHistoryItem(user, 'suspension').until
|
||||
)
|
||||
).format('MMMM Do YYYY, h:mm:ss a')}
|
||||
).format('MMM Do YYYY, h:mm:ss a')}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -2,4 +2,6 @@ const prefix = 'TALK_ADMIN_CONFIGURE';
|
||||
|
||||
export const UPDATE_PENDING = `${prefix}_UPDATE_PENDING`;
|
||||
export const CLEAR_PENDING = `${prefix}_CLEAR_PENDING`;
|
||||
export const SET_ACTIVE_SECTION = `${prefix}_SET_ACTIVE_SECTION`;
|
||||
|
||||
export const SHOW_SAVE_DIALOG = `${prefix}_SHOW_SAVE_DIALOG`;
|
||||
export const HIDE_SAVE_DIALOG = `${prefix}_HIDE_SAVE_DIALOG`;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withForgotPassword } from 'coral-framework/hocs';
|
||||
import { compose } from 'recompose';
|
||||
import ForgotPassword from '../components/ForgotPassword';
|
||||
|
||||
class ForgotPasswordContainer extends Component {
|
||||
state = {
|
||||
email: '',
|
||||
};
|
||||
|
||||
handleSubmit = () => {
|
||||
this.props.forgotPassword(this.state.email);
|
||||
};
|
||||
|
||||
handleEmailChange = email => {
|
||||
this.setState({ email });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ForgotPassword
|
||||
onSubmit={this.handleSubmit}
|
||||
onEmailChange={this.handleEmailChange}
|
||||
email={this.state.email}
|
||||
errorMessage={this.props.errorMessage}
|
||||
success={this.props.success}
|
||||
onSignInLink={this.props.onSignInLink}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ForgotPasswordContainer.propTypes = {
|
||||
success: PropTypes.bool.isRequired,
|
||||
forgotPassword: PropTypes.func.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
onSignInLink: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default compose(withForgotPassword)(ForgotPasswordContainer);
|
||||
@@ -2,86 +2,56 @@ import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import Layout from '../components/Layout';
|
||||
import { fetchConfig } from '../actions/config';
|
||||
import AdminLogin from '../components/AdminLogin';
|
||||
import Login from '../containers/Login';
|
||||
import { FullLoading } from '../components/FullLoading';
|
||||
import BanUserDialog from './BanUserDialog';
|
||||
import SuspendUserDialog from './SuspendUserDialog';
|
||||
import { toggleModal as toggleShortcutModal } from '../actions/moderation';
|
||||
import {
|
||||
checkLogin,
|
||||
handleLogin,
|
||||
requestPasswordReset,
|
||||
logout,
|
||||
} from '../actions/auth';
|
||||
import { logout } from 'coral-framework/actions/auth';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import UserDetail from 'coral-admin/src/containers/UserDetail';
|
||||
import PropTypes from 'prop-types';
|
||||
import Forbidden from '../components/Forbidden';
|
||||
|
||||
class LayoutContainer extends React.Component {
|
||||
componentWillMount() {
|
||||
const { checkLogin, fetchConfig } = this.props;
|
||||
|
||||
checkLogin();
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
user,
|
||||
loggedIn,
|
||||
loadingUser,
|
||||
loginError,
|
||||
loginMaxExceeded,
|
||||
passwordRequestSuccess,
|
||||
} = this.props.auth;
|
||||
|
||||
const {
|
||||
currentUser,
|
||||
checkedInitialLogin,
|
||||
children,
|
||||
logout,
|
||||
toggleShortcutModal,
|
||||
TALK_RECAPTCHA_PUBLIC,
|
||||
} = this.props;
|
||||
|
||||
if (loadingUser) {
|
||||
if (!checkedInitialLogin) {
|
||||
return <FullLoading />;
|
||||
}
|
||||
|
||||
if (!loggedIn) {
|
||||
return (
|
||||
<AdminLogin
|
||||
loginMaxExceeded={loginMaxExceeded}
|
||||
handleLogin={this.props.handleLogin}
|
||||
requestPasswordReset={this.props.requestPasswordReset}
|
||||
passwordRequestSuccess={passwordRequestSuccess}
|
||||
recaptchaPublic={TALK_RECAPTCHA_PUBLIC}
|
||||
errorMessage={loginError}
|
||||
/>
|
||||
);
|
||||
if (!currentUser) {
|
||||
return <Login />;
|
||||
}
|
||||
|
||||
if (can(user, 'ACCESS_ADMIN') && loggedIn) {
|
||||
return (
|
||||
<Layout
|
||||
handleLogout={logout}
|
||||
toggleShortcutModal={toggleShortcutModal}
|
||||
auth={this.props.auth}
|
||||
>
|
||||
<BanUserDialog />
|
||||
<SuspendUserDialog />
|
||||
<UserDetail />
|
||||
{children}
|
||||
</Layout>
|
||||
);
|
||||
} else if (loggedIn) {
|
||||
return (
|
||||
<Layout handleLogout={logout} {...this.props}>
|
||||
<p>
|
||||
This page is for team use only. Please contact an administrator if
|
||||
you want to join this team.
|
||||
</p>
|
||||
</Layout>
|
||||
);
|
||||
if (currentUser) {
|
||||
if (can(currentUser, 'ACCESS_ADMIN')) {
|
||||
return (
|
||||
<Layout
|
||||
handleLogout={logout}
|
||||
toggleShortcutModal={toggleShortcutModal}
|
||||
currentUser={this.props.currentUser}
|
||||
>
|
||||
<BanUserDialog />
|
||||
<SuspendUserDialog />
|
||||
<UserDetail />
|
||||
{children}
|
||||
</Layout>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Layout {...this.props} handleLogout={logout}>
|
||||
<Forbidden />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
return <FullLoading />;
|
||||
}
|
||||
@@ -89,29 +59,20 @@ class LayoutContainer extends React.Component {
|
||||
|
||||
LayoutContainer.propTypes = {
|
||||
children: PropTypes.node,
|
||||
requestPasswordReset: PropTypes.func,
|
||||
handleLogin: PropTypes.func,
|
||||
auth: PropTypes.object,
|
||||
handleLogout: PropTypes.func,
|
||||
currentUser: PropTypes.object,
|
||||
checkedInitialLogin: PropTypes.bool,
|
||||
logout: PropTypes.func,
|
||||
toggleShortcutModal: PropTypes.func,
|
||||
TALK_RECAPTCHA_PUBLIC: PropTypes.string,
|
||||
checkLogin: PropTypes.func,
|
||||
fetchConfig: PropTypes.func,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
auth: state.auth,
|
||||
TALK_RECAPTCHA_PUBLIC: state.config.data.TALK_RECAPTCHA_PUBLIC,
|
||||
currentUser: state.auth.user,
|
||||
checkedInitialLogin: state.auth.checkedInitialLogin,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
checkLogin,
|
||||
fetchConfig,
|
||||
handleLogin,
|
||||
requestPasswordReset,
|
||||
toggleShortcutModal,
|
||||
logout,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import React, { Component } from 'react';
|
||||
import Login from '../components/Login';
|
||||
|
||||
class LoginContainer extends Component {
|
||||
state = {
|
||||
forgotPassword: false,
|
||||
};
|
||||
|
||||
switchToForgotPassword = () => {
|
||||
this.setState({ forgotPassword: true });
|
||||
};
|
||||
|
||||
switchToSignIn = () => {
|
||||
this.setState({ forgotPassword: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Login
|
||||
forgotPassword={this.state.forgotPassword}
|
||||
onForgotPasswordLink={this.switchToForgotPassword}
|
||||
onSignInLink={this.switchToSignIn}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LoginContainer.propTypes = {};
|
||||
|
||||
export default LoginContainer;
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withSignIn } from 'coral-framework/hocs';
|
||||
import { compose } from 'recompose';
|
||||
import SignIn from '../components/SignIn';
|
||||
|
||||
class SignInContainer extends Component {
|
||||
state = {
|
||||
email: '',
|
||||
password: '',
|
||||
recaptchaResponse: null,
|
||||
};
|
||||
|
||||
handleSubmit = () => {
|
||||
this.props.signIn(
|
||||
this.state.email,
|
||||
this.state.password,
|
||||
this.state.recaptchaResponse
|
||||
);
|
||||
};
|
||||
|
||||
handleEmailChange = email => {
|
||||
this.setState({ email });
|
||||
};
|
||||
|
||||
handlePasswordChange = password => {
|
||||
this.setState({ password });
|
||||
};
|
||||
|
||||
handleRecaptchaVerify = recaptchaResponse => {
|
||||
this.setState({ recaptchaResponse });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SignIn
|
||||
onSubmit={this.handleSubmit}
|
||||
onEmailChange={this.handleEmailChange}
|
||||
onPasswordChange={this.handlePasswordChange}
|
||||
email={this.state.email}
|
||||
password={this.state.password}
|
||||
errorMessage={this.props.errorMessage}
|
||||
onForgotPasswordLink={this.props.onForgotPasswordLink}
|
||||
onRecaptchaVerify={this.handleRecaptchaVerify}
|
||||
requireRecaptcha={this.props.requireRecaptcha}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SignInContainer.propTypes = {
|
||||
signIn: PropTypes.func.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
onForgotPasswordLink: PropTypes.func.isRequired,
|
||||
requireRecaptcha: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default compose(withSignIn)(SignInContainer);
|
||||
@@ -4,6 +4,9 @@ import withFragments from 'coral-framework/hocs/withFragments';
|
||||
import { getDefinitionName } from 'coral-framework/utils';
|
||||
import CommentLabels from './CommentLabels';
|
||||
import CommentDetails from './CommentDetails';
|
||||
import { getSlotFragmentSpreads } from 'coral-framework/utils';
|
||||
|
||||
const slots = ['userDetailCommentContent'];
|
||||
|
||||
export default withFragments({
|
||||
root: gql`
|
||||
@@ -14,6 +17,7 @@ export default withFragments({
|
||||
suspect
|
||||
}
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
...${getDefinitionName(CommentLabels.fragments.root)}
|
||||
...${getDefinitionName(CommentDetails.fragments.root)}
|
||||
}
|
||||
@@ -38,6 +42,7 @@ export default withFragments({
|
||||
status_history {
|
||||
type
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'comment')}
|
||||
...${getDefinitionName(CommentLabels.fragments.comment)}
|
||||
...${getDefinitionName(CommentDetails.fragments.comment)}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import * as actions from '../constants/auth';
|
||||
|
||||
const initialState = {
|
||||
loggedIn: false,
|
||||
user: null,
|
||||
loginError: null,
|
||||
loginMaxExceeded: false,
|
||||
passwordRequestSuccess: null,
|
||||
};
|
||||
|
||||
export default function auth(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.CHECK_LOGIN_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
loadingUser: true,
|
||||
};
|
||||
case actions.CHECK_LOGIN_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
loggedIn: false,
|
||||
loadingUser: false,
|
||||
user: null,
|
||||
};
|
||||
case actions.CHECK_LOGIN_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
loggedIn: true,
|
||||
loadingUser: false,
|
||||
user: action.user,
|
||||
};
|
||||
case actions.LOGOUT:
|
||||
return initialState;
|
||||
case actions.LOGIN_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
loginMaxExceeded: false,
|
||||
loginError: null,
|
||||
};
|
||||
case actions.LOGIN_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
loginError: action.message,
|
||||
};
|
||||
case actions.FETCH_FORGOT_PASSWORD_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
passwordRequestSuccess: null,
|
||||
};
|
||||
case actions.FETCH_FORGOT_PASSWORD_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
passwordRequestSuccess:
|
||||
'If you have a registered account, a password reset link was sent to that email.',
|
||||
};
|
||||
case actions.LOGIN_MAXIMUM_EXCEEDED:
|
||||
return {
|
||||
...state,
|
||||
loginMaxExceeded: true,
|
||||
loginError: action.message,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import * as actions from '../actions/config';
|
||||
|
||||
const initialState = {
|
||||
data: {},
|
||||
};
|
||||
|
||||
export default function config(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.CONFIG_UPDATED:
|
||||
return {
|
||||
...state,
|
||||
data: action.data,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,23 @@ const initialState = {
|
||||
canSave: false,
|
||||
pending: {},
|
||||
errors: {},
|
||||
activeSection: 'stream',
|
||||
saveDialog: false,
|
||||
};
|
||||
|
||||
export default function configure(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.SHOW_SAVE_DIALOG: {
|
||||
return {
|
||||
...state,
|
||||
saveDialog: true,
|
||||
};
|
||||
}
|
||||
case actions.HIDE_SAVE_DIALOG: {
|
||||
return {
|
||||
...state,
|
||||
saveDialog: false,
|
||||
};
|
||||
}
|
||||
case actions.UPDATE_PENDING: {
|
||||
let next = state;
|
||||
if (action.updater) {
|
||||
@@ -40,11 +52,8 @@ export default function configure(state = initialState, action) {
|
||||
pending: {},
|
||||
canSave: false,
|
||||
};
|
||||
case actions.SET_ACTIVE_SECTION:
|
||||
return {
|
||||
...state,
|
||||
activeSection: action.section,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import auth from './auth';
|
||||
import stories from './stories';
|
||||
import configure from './configure';
|
||||
import community from './community';
|
||||
import moderation from './moderation';
|
||||
import install from './install';
|
||||
import config from './config';
|
||||
import banUserDialog from './banUserDialog';
|
||||
import suspendUserDialog from './suspendUserDialog';
|
||||
import userDetail from './userDetail';
|
||||
import ui from './ui';
|
||||
|
||||
export default {
|
||||
auth,
|
||||
banUserDialog,
|
||||
configure,
|
||||
suspendUserDialog,
|
||||
@@ -20,6 +17,5 @@ export default {
|
||||
community,
|
||||
moderation,
|
||||
install,
|
||||
config,
|
||||
ui,
|
||||
};
|
||||
|
||||
@@ -130,3 +130,12 @@ th.header:nth-child(2), th.header:nth-child(3) {
|
||||
.loadMore {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.roleDropdown {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.roleOption {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
|
||||
@@ -200,24 +200,31 @@ class People extends React.Component {
|
||||
</td>
|
||||
<td className="mdl-data-table__cell--non-numeric">
|
||||
<Dropdown
|
||||
containerClassName="talk-admin-community-people-dd-role"
|
||||
className={cn(
|
||||
'talk-admin-community-people-dd-role',
|
||||
styles.roleDropdown
|
||||
)}
|
||||
value={user.role}
|
||||
placeholder={t('community.role')}
|
||||
onChange={role => setUserRole(user.id, role)}
|
||||
>
|
||||
<Option
|
||||
className={styles.roleOption}
|
||||
value={COMMENTER}
|
||||
label={t('community.commenter')}
|
||||
/>
|
||||
<Option
|
||||
className={styles.roleOption}
|
||||
value={STAFF}
|
||||
label={t('community.staff')}
|
||||
/>
|
||||
<Option
|
||||
className={styles.roleOption}
|
||||
value={MODERATOR}
|
||||
label={t('community.moderator')}
|
||||
/>
|
||||
<Option
|
||||
className={styles.roleOption}
|
||||
value={ADMIN}
|
||||
label={t('community.admin')}
|
||||
/>
|
||||
|
||||
@@ -1,50 +1,37 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { Button, List, Item } from 'coral-ui';
|
||||
import styles from './Configure.css';
|
||||
import StreamSettings from '../containers/StreamSettings';
|
||||
import ModerationSettings from '../containers/ModerationSettings';
|
||||
import TechSettings from '../containers/TechSettings';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { Button, List, Item } from 'coral-ui';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import styles from './Configure.css';
|
||||
import SaveChangesDialog from './SaveChangesDialog';
|
||||
|
||||
export default class Configure extends Component {
|
||||
getSectionComponent(section) {
|
||||
switch (section) {
|
||||
case 'stream':
|
||||
return StreamSettings;
|
||||
case 'moderation':
|
||||
return ModerationSettings;
|
||||
case 'tech':
|
||||
return TechSettings;
|
||||
}
|
||||
throw new Error(`Unknown section ${section}`);
|
||||
}
|
||||
|
||||
class Configure extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
auth: { user },
|
||||
canSave,
|
||||
savePending,
|
||||
setActiveSection,
|
||||
activeSection,
|
||||
} = this.props;
|
||||
const SectionComponent = this.getSectionComponent(activeSection);
|
||||
const { canSave, currentUser, root, savePending, settings } = this.props;
|
||||
|
||||
if (!can(user, 'UPDATE_CONFIG')) {
|
||||
return (
|
||||
<p>
|
||||
You must be an administrator to access config settings. Please find
|
||||
the nearest Admin and ask them to level you up!
|
||||
</p>
|
||||
);
|
||||
if (!can(currentUser, 'UPDATE_CONFIG')) {
|
||||
return <p>{t('configure.access_message')}</p>;
|
||||
}
|
||||
|
||||
const passProps = {
|
||||
root,
|
||||
settings,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<SaveChangesDialog
|
||||
saveDialog={this.props.saveDialog}
|
||||
hideSaveDialog={this.props.hideSaveDialog}
|
||||
saveChanges={this.props.saveChanges}
|
||||
discardChanges={this.props.discardChanges}
|
||||
/>
|
||||
<div className={styles.leftColumn}>
|
||||
<List onChange={setActiveSection} activeItem={activeSection}>
|
||||
<List
|
||||
onChange={this.props.handleSectionChange}
|
||||
activeItem={this.props.activeSection}
|
||||
>
|
||||
<Item itemId="stream" icon="speaker_notes">
|
||||
{t('configure.stream_settings')}
|
||||
</Item>
|
||||
@@ -74,11 +61,7 @@ export default class Configure extends Component {
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.mainContent}>
|
||||
<SectionComponent
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
settings={this.props.settings}
|
||||
/>
|
||||
{React.cloneElement(this.props.children, passProps)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -87,11 +70,17 @@ export default class Configure extends Component {
|
||||
|
||||
Configure.propTypes = {
|
||||
savePending: PropTypes.func.isRequired,
|
||||
auth: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
saveChanges: PropTypes.func.isRequired,
|
||||
discardChanges: PropTypes.func.isRequired,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
canSave: PropTypes.bool.isRequired,
|
||||
setActiveSection: PropTypes.func.isRequired,
|
||||
handleSectionChange: PropTypes.func.isRequired,
|
||||
activeSection: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
saveDialog: PropTypes.bool,
|
||||
hideSaveDialog: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Configure;
|
||||
|
||||
@@ -52,7 +52,7 @@ class ModerationSettings extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { settings, data, root, updatePending, errors } = this.props;
|
||||
const { settings, slotPassthrough } = this.props;
|
||||
|
||||
return (
|
||||
<ConfigurePage title={t('configure.moderation_settings')}>
|
||||
@@ -82,13 +82,7 @@ class ModerationSettings extends React.Component {
|
||||
suspectWords={settings.wordlist.suspect}
|
||||
onChangeWordlist={this.updateWordlist}
|
||||
/>
|
||||
<Slot
|
||||
fill="adminModerationSettings"
|
||||
data={data}
|
||||
queryData={{ root, settings }}
|
||||
updatePending={updatePending}
|
||||
errors={errors}
|
||||
/>
|
||||
<Slot fill="adminModerationSettings" passthrough={slotPassthrough} />
|
||||
</ConfigurePage>
|
||||
);
|
||||
}
|
||||
@@ -97,9 +91,8 @@ class ModerationSettings extends React.Component {
|
||||
ModerationSettings.propTypes = {
|
||||
updatePending: PropTypes.func.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
slotPassthrough: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default ModerationSettings;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
.buttonActions {
|
||||
padding-top: 15px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
padding: 25px;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.close {
|
||||
font-size: 20px;
|
||||
line-height: 14px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
position: absolute;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #363636;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
color: #363636;
|
||||
margin-right: 15px;
|
||||
display: inline-block;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import { Button, Dialog } from 'coral-ui';
|
||||
import styles from './SaveChangesDialog.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const SaveChangesDialog = ({
|
||||
saveDialog,
|
||||
hideSaveDialog,
|
||||
saveChanges,
|
||||
discardChanges,
|
||||
}) => (
|
||||
<Dialog
|
||||
className={cn(styles.dialog, 'talk-admin-configure-save-dialog')}
|
||||
id="saveDialog"
|
||||
open={saveDialog}
|
||||
onCancel={hideSaveDialog}
|
||||
>
|
||||
<span className={styles.close} onClick={hideSaveDialog}>
|
||||
×
|
||||
</span>
|
||||
<div className={styles.title}>
|
||||
{t('configure.save_changes_dialog.unsaved_changes')}
|
||||
</div>
|
||||
{t('configure.save_changes_dialog.copy')}
|
||||
<div
|
||||
className={cn(
|
||||
styles.buttonActions,
|
||||
'talk-admin-configure-save-dialog-button-actions'
|
||||
)}
|
||||
>
|
||||
<a className={styles.cancel} onClick={hideSaveDialog}>
|
||||
Cancel
|
||||
</a>
|
||||
<Button onClick={discardChanges} className={styles.button}>
|
||||
{t('configure.save_changes_dialog.discard')}
|
||||
</Button>
|
||||
<Button onClick={saveChanges} cStyle="green" className={styles.button}>
|
||||
{t('configure.save_changes_dialog.save_settings')}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
SaveChangesDialog.propTypes = {
|
||||
saveDialog: PropTypes.bool.isRequired,
|
||||
hideSaveDialog: PropTypes.func.isRequired,
|
||||
saveChanges: PropTypes.func.isRequired,
|
||||
discardChanges: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default SaveChangesDialog;
|
||||
@@ -107,7 +107,7 @@ class StreamSettings extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { settings, data, root, errors, updatePending } = this.props;
|
||||
const { settings, slotPassthrough, errors } = this.props;
|
||||
|
||||
return (
|
||||
<ConfigurePage title={t('configure.stream_settings')}>
|
||||
@@ -220,13 +220,7 @@ class StreamSettings extends React.Component {
|
||||
</div>
|
||||
</ConfigureCard>
|
||||
{/* the above card should be the last one if at all possible because of z-index issues with the selects */}
|
||||
<Slot
|
||||
fill="adminStreamSettings"
|
||||
data={data}
|
||||
queryData={{ root, settings }}
|
||||
updatePending={updatePending}
|
||||
errors={errors}
|
||||
/>
|
||||
<Slot fill="adminStreamSettings" passthrough={slotPassthrough} />
|
||||
</ConfigurePage>
|
||||
);
|
||||
}
|
||||
@@ -235,9 +229,8 @@ class StreamSettings extends React.Component {
|
||||
StreamSettings.propTypes = {
|
||||
updatePending: PropTypes.func.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
slotPassthrough: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default StreamSettings;
|
||||
|
||||
@@ -34,7 +34,7 @@ class TechSettings extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { settings, data, root, errors, updatePending } = this.props;
|
||||
const { settings, slotPassthrough } = this.props;
|
||||
return (
|
||||
<ConfigurePage title={t('configure.tech_settings')}>
|
||||
<Domainlist
|
||||
@@ -50,13 +50,7 @@ class TechSettings extends React.Component {
|
||||
onChange={this.updateCustomCssUrl}
|
||||
/>
|
||||
</ConfigureCard>
|
||||
<Slot
|
||||
fill="adminTechSettings"
|
||||
data={data}
|
||||
queryData={{ root, settings }}
|
||||
updatePending={updatePending}
|
||||
errors={errors}
|
||||
/>
|
||||
<Slot fill="adminTechSettings" passthrough={slotPassthrough} />
|
||||
</ConfigurePage>
|
||||
);
|
||||
}
|
||||
@@ -64,10 +58,9 @@ class TechSettings extends React.Component {
|
||||
|
||||
TechSettings.propTypes = {
|
||||
updatePending: PropTypes.func.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
slotPassthrough: PropTypes.object.isRequired,
|
||||
errors: PropTypes.object,
|
||||
};
|
||||
|
||||
export default TechSettings;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
@@ -10,15 +10,70 @@ import { getDefinitionName } from 'coral-framework/utils';
|
||||
import StreamSettings from './StreamSettings';
|
||||
import TechSettings from './TechSettings';
|
||||
import ModerationSettings from './ModerationSettings';
|
||||
import { clearPending, setActiveSection } from '../../../actions/configure';
|
||||
import {
|
||||
clearPending,
|
||||
showSaveDialog,
|
||||
hideSaveDialog,
|
||||
} from '../../../actions/configure';
|
||||
import Configure from '../components/Configure';
|
||||
import { withRouter } from 'react-router';
|
||||
|
||||
class ConfigureContainer extends React.Component {
|
||||
state = { nextRoute: '' };
|
||||
|
||||
class ConfigureContainer extends Component {
|
||||
savePending = async () => {
|
||||
await this.props.updateSettings(this.props.pending);
|
||||
this.props.clearPending();
|
||||
};
|
||||
|
||||
saveChanges = async () => {
|
||||
await this.savePending();
|
||||
this.props.hideSaveDialog();
|
||||
this.gotoNextRoute();
|
||||
};
|
||||
|
||||
discardChanges = async () => {
|
||||
await this.props.clearPending();
|
||||
this.props.hideSaveDialog();
|
||||
this.gotoNextRoute();
|
||||
};
|
||||
|
||||
gotoNextRoute = () => {
|
||||
const { nextRoute } = this.state;
|
||||
if (nextRoute) {
|
||||
this.props.router.push(nextRoute);
|
||||
this.setState({ nextRoute: '' });
|
||||
}
|
||||
};
|
||||
|
||||
handleSectionChange = async section => {
|
||||
const nextRoute = `/admin/configure/${section}`;
|
||||
|
||||
if (this.shouldShowSaveDialog()) {
|
||||
await this.setState({ nextRoute });
|
||||
this.props.showSaveDialog();
|
||||
} else {
|
||||
// Just go to the section
|
||||
this.props.router.push(nextRoute);
|
||||
}
|
||||
};
|
||||
|
||||
shouldShowSaveDialog = () => {
|
||||
return !!Object.keys(this.props.pending).length;
|
||||
};
|
||||
|
||||
routeLeave = ({ pathname }) => {
|
||||
if (this.shouldShowSaveDialog()) {
|
||||
this.setState({ nextRoute: pathname });
|
||||
this.props.showSaveDialog();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.router.setRouteLeaveHook(this.props.route, this.routeLeave);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.data.error) {
|
||||
return <div>{this.props.data.error.message}</div>;
|
||||
@@ -30,15 +85,20 @@ class ConfigureContainer extends Component {
|
||||
|
||||
return (
|
||||
<Configure
|
||||
auth={this.props.auth}
|
||||
data={this.props.data}
|
||||
saveChanges={this.saveChanges}
|
||||
discardChanges={this.discardChanges}
|
||||
saveDialog={this.props.saveDialog}
|
||||
activeSection={this.props.routes[3].path}
|
||||
hideSaveDialog={this.props.hideSaveDialog}
|
||||
canSave={this.props.canSave}
|
||||
currentUser={this.props.currentUser}
|
||||
root={this.props.root}
|
||||
settings={this.props.mergedSettings}
|
||||
canSave={this.props.canSave}
|
||||
handleSectionChange={this.handleSectionChange}
|
||||
savePending={this.savePending}
|
||||
setActiveSection={this.props.setActiveSection}
|
||||
activeSection={this.props.activeSection}
|
||||
/>
|
||||
>
|
||||
{this.props.children}
|
||||
</Configure>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -71,22 +131,25 @@ const withConfigureQuery = withQuery(
|
||||
);
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
auth: state.auth,
|
||||
currentUser: state.auth.user,
|
||||
pending: state.configure.pending,
|
||||
canSave: state.configure.canSave,
|
||||
activeSection: state.configure.activeSection,
|
||||
saveDialog: state.configure.saveDialog,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
clearPending,
|
||||
setActiveSection,
|
||||
showSaveDialog,
|
||||
hideSaveDialog,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withUpdateSettings,
|
||||
withConfigureQuery,
|
||||
@@ -94,14 +157,20 @@ export default compose(
|
||||
)(ConfigureContainer);
|
||||
|
||||
ConfigureContainer.propTypes = {
|
||||
activeSection: PropTypes.string,
|
||||
updateSettings: PropTypes.func.isRequired,
|
||||
clearPending: PropTypes.func.isRequired,
|
||||
setActiveSection: PropTypes.func.isRequired,
|
||||
auth: PropTypes.object.isRequired,
|
||||
showSaveDialog: PropTypes.func.isRequired,
|
||||
hideSaveDialog: PropTypes.func.isRequired,
|
||||
saveDialog: PropTypes.bool.isRequired,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
canSave: PropTypes.bool.isRequired,
|
||||
pending: PropTypes.object.isRequired,
|
||||
mergedSettings: PropTypes.object.isRequired,
|
||||
activeSection: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
router: PropTypes.object,
|
||||
route: PropTypes.object,
|
||||
routes: PropTypes.array,
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import ModerationSettings from '../components/ModerationSettings';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
import { getSlotFragmentSpreads } from 'coral-framework/utils';
|
||||
import { updatePending } from '../../../actions/configure';
|
||||
import { mapProps } from 'recompose';
|
||||
|
||||
const slots = ['adminModerationSettings'];
|
||||
|
||||
@@ -41,5 +42,17 @@ export default compose(
|
||||
}
|
||||
`,
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
|
||||
slotPassthrough: {
|
||||
root,
|
||||
settings,
|
||||
updatePending,
|
||||
errors,
|
||||
},
|
||||
updatePending,
|
||||
settings,
|
||||
errors,
|
||||
...rest,
|
||||
}))
|
||||
)(ModerationSettings);
|
||||
|
||||
@@ -5,6 +5,7 @@ import StreamSettings from '../components/StreamSettings';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
import { getSlotFragmentSpreads } from 'coral-framework/utils';
|
||||
import { updatePending } from '../../../actions/configure';
|
||||
import { mapProps } from 'recompose';
|
||||
|
||||
const slots = ['adminStreamSettings'];
|
||||
|
||||
@@ -42,5 +43,17 @@ export default compose(
|
||||
}
|
||||
`,
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
|
||||
slotPassthrough: {
|
||||
root,
|
||||
settings,
|
||||
updatePending,
|
||||
errors,
|
||||
},
|
||||
updatePending,
|
||||
settings,
|
||||
errors,
|
||||
...rest,
|
||||
}))
|
||||
)(StreamSettings);
|
||||
|
||||
@@ -5,6 +5,7 @@ import TechSettings from '../components/TechSettings';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
import { getSlotFragmentSpreads } from 'coral-framework/utils';
|
||||
import { updatePending } from '../../../actions/configure';
|
||||
import { mapProps } from 'recompose';
|
||||
|
||||
const slots = ['adminTechSettings'];
|
||||
|
||||
@@ -38,5 +39,17 @@ export default compose(
|
||||
}
|
||||
`,
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
|
||||
slotPassthrough: {
|
||||
root,
|
||||
settings,
|
||||
updatePending,
|
||||
errors,
|
||||
},
|
||||
updatePending,
|
||||
settings,
|
||||
errors,
|
||||
...rest,
|
||||
}))
|
||||
)(TechSettings);
|
||||
|
||||
@@ -10,12 +10,14 @@ const InitialStep = () => {
|
||||
return (
|
||||
<div className={cn(styles.step, styles.finalStep, 'talk-install-step-5')}>
|
||||
<p>{t('install.final.description')}</p>
|
||||
<Button raised>
|
||||
<Link to="/admin">{t('install.final.launch')}</Link>
|
||||
</Button>
|
||||
<Button cStyle="black" raised>
|
||||
<a href="http://coralproject.net">{t('install.final.close')}</a>
|
||||
</Button>
|
||||
<Link to="/admin">
|
||||
<Button raised>{t('install.final.launch')}</Button>
|
||||
</Link>
|
||||
<a href="http://coralproject.net">
|
||||
<Button cStyle="black" raised>
|
||||
{t('install.final.close')}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -72,9 +72,9 @@
|
||||
flex: 1;
|
||||
color: black;
|
||||
max-width: 500px;
|
||||
word-wrap: break-word;
|
||||
font-weight: 300;
|
||||
font-size: 16px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.created {
|
||||
@@ -133,7 +133,6 @@
|
||||
color: #063b9a;
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
margin-left: 10px;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
@@ -143,7 +142,6 @@
|
||||
|
||||
i {
|
||||
font-size: 12px;
|
||||
top: 2px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
@@ -185,3 +183,17 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.commentContent {
|
||||
display: flex;
|
||||
|
||||
blockquote {
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
letter-spacing: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.commentContentFooter {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import styles from './Comment.css';
|
||||
import CommentLabels from 'coral-admin/src/components/CommentLabels';
|
||||
import CommentAnimatedEdit from 'coral-admin/src/components/CommentAnimatedEdit';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import CommentFormatter from 'coral-admin/src/components/CommentFormatter';
|
||||
import AdminCommentContent from 'coral-framework/components/AdminCommentContent';
|
||||
import IfHasLink from 'coral-admin/src/components/IfHasLink';
|
||||
import cn from 'classnames';
|
||||
import ApproveButton from 'coral-admin/src/components/ApproveButton';
|
||||
@@ -53,7 +53,6 @@ class Comment extends React.Component {
|
||||
comment,
|
||||
selected,
|
||||
className,
|
||||
data,
|
||||
root,
|
||||
root: { settings },
|
||||
currentAsset,
|
||||
@@ -62,7 +61,19 @@ class Comment extends React.Component {
|
||||
} = this.props;
|
||||
|
||||
const selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
|
||||
const queryData = { root, comment, asset: comment.asset };
|
||||
|
||||
const formatterSettings = {
|
||||
suspectWords: settings.wordlist.suspect,
|
||||
bannedWords: settings.wordlist.banned,
|
||||
body: comment.body,
|
||||
};
|
||||
|
||||
const slotPassthrough = {
|
||||
clearHeightCache,
|
||||
root,
|
||||
comment,
|
||||
asset: comment.asset,
|
||||
};
|
||||
|
||||
return (
|
||||
<li
|
||||
@@ -107,16 +118,15 @@ class Comment extends React.Component {
|
||||
<CommentLabels comment={comment} />
|
||||
<Slot
|
||||
fill="adminCommentInfoBar"
|
||||
data={data}
|
||||
clearHeightCache={clearHeightCache}
|
||||
queryData={queryData}
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.moderateArticle}>
|
||||
Story: {comment.asset.title}
|
||||
{t('common.story')}:{' '}
|
||||
{comment.asset.title ? comment.asset.title : comment.asset.url}
|
||||
{!currentAsset && (
|
||||
<Link to={`/admin/moderate/${comment.asset.id}`}>
|
||||
{t('modqueue.moderate')}
|
||||
@@ -126,29 +136,28 @@ class Comment extends React.Component {
|
||||
<CommentAnimatedEdit body={comment.body}>
|
||||
<div className={styles.itemBody}>
|
||||
<div className={styles.body}>
|
||||
<CommentFormatter
|
||||
suspectWords={settings.wordlist.suspect}
|
||||
bannedWords={settings.wordlist.banned}
|
||||
className="talk-admin-comment"
|
||||
body={comment.body}
|
||||
<Slot
|
||||
fill="adminCommentContent"
|
||||
className={cn(styles.commentContent, 'talk-admin-comment')}
|
||||
size={1}
|
||||
defaultComponent={AdminCommentContent}
|
||||
passthrough={{ ...slotPassthrough, ...formatterSettings }}
|
||||
/>
|
||||
<a
|
||||
className={styles.external}
|
||||
href={`${comment.asset.url}?commentId=${comment.id}`}
|
||||
target="_blank"
|
||||
>
|
||||
<Icon name="open_in_new" /> {t('comment.view_context')}
|
||||
</a>
|
||||
<div className={styles.commentContentFooter}>
|
||||
<a
|
||||
className={styles.external}
|
||||
href={`${comment.asset.url}?commentId=${comment.id}`}
|
||||
target="_blank"
|
||||
>
|
||||
<Icon name="open_in_new" /> {t('comment.view_context')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<Slot
|
||||
fill="adminCommentContent"
|
||||
data={data}
|
||||
clearHeightCache={clearHeightCache}
|
||||
queryData={queryData}
|
||||
/>
|
||||
|
||||
<div className={styles.sideActions}>
|
||||
<IfHasLink text={comment.body}>
|
||||
<span className={styles.hasLinks}>
|
||||
{/* TODO: translate string */}
|
||||
<Icon name="error_outline" /> Contains Link
|
||||
</span>
|
||||
</IfHasLink>
|
||||
@@ -162,18 +171,12 @@ class Comment extends React.Component {
|
||||
onClick={this.reject}
|
||||
/>
|
||||
</div>
|
||||
<Slot
|
||||
fill="adminSideActions"
|
||||
data={data}
|
||||
clearHeightCache={clearHeightCache}
|
||||
queryData={queryData}
|
||||
/>
|
||||
<Slot fill="adminSideActions" passthrough={slotPassthrough} />
|
||||
</div>
|
||||
</div>
|
||||
</CommentAnimatedEdit>
|
||||
</div>
|
||||
<CommentDetails
|
||||
data={data}
|
||||
root={root}
|
||||
comment={comment}
|
||||
clearHeightCache={clearHeightCache}
|
||||
@@ -211,7 +214,6 @@ Comment.propTypes = {
|
||||
id: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
selected: PropTypes.bool,
|
||||
};
|
||||
|
||||
@@ -126,7 +126,6 @@ class Moderation extends Component {
|
||||
render() {
|
||||
const {
|
||||
root,
|
||||
data,
|
||||
moderation,
|
||||
viewUserDetail,
|
||||
activeTab,
|
||||
@@ -148,6 +147,13 @@ class Moderation extends Component {
|
||||
count: root[`${queue}Count`],
|
||||
}));
|
||||
|
||||
const slotPassthrough = {
|
||||
root,
|
||||
asset,
|
||||
activeTab,
|
||||
handleCommentChange,
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ModerationHeader
|
||||
@@ -171,7 +177,6 @@ class Moderation extends Component {
|
||||
/>
|
||||
<ModerationQueue
|
||||
key={`${activeTab}_${this.props.moderation.sortOrder}`}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
currentAsset={asset}
|
||||
comments={comments.nodes}
|
||||
@@ -185,7 +190,7 @@ class Moderation extends Component {
|
||||
commentBelongToQueue={this.props.commentBelongToQueue}
|
||||
isLoadingMore={this.state.isLoadingMore}
|
||||
commentCount={activeTabCount}
|
||||
currentUserId={this.props.auth.user.id}
|
||||
currentUserId={this.props.currentUser.id}
|
||||
viewUserDetail={viewUserDetail}
|
||||
selectCommentId={props.selectCommentId}
|
||||
cleanUpQueue={props.cleanUpQueue}
|
||||
@@ -204,13 +209,7 @@ class Moderation extends Component {
|
||||
closeSearch={this.closeSearch}
|
||||
storySearchChange={this.props.storySearchChange}
|
||||
/>
|
||||
<Slot
|
||||
data={data}
|
||||
queryData={{ root, asset }}
|
||||
activeTab={activeTab}
|
||||
handleCommentChange={handleCommentChange}
|
||||
fill="adminModeration"
|
||||
/>
|
||||
<Slot fill="adminModeration" passthrough={slotPassthrough} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -225,7 +224,7 @@ Moderation.propTypes = {
|
||||
cleanUpQueue: PropTypes.func.isRequired,
|
||||
storySearchChange: PropTypes.func.isRequired,
|
||||
moderation: PropTypes.object.isRequired,
|
||||
auth: PropTypes.object.isRequired,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
queueConfig: PropTypes.object.isRequired,
|
||||
commentBelongToQueue: PropTypes.func.isRequired,
|
||||
handleCommentChange: PropTypes.func.isRequired,
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
:global(html) {
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.list {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -344,7 +344,6 @@ class ModerationQueue extends React.Component {
|
||||
child = (
|
||||
<div style={style}>
|
||||
<Comment
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
comment={comment}
|
||||
dangling={
|
||||
@@ -408,7 +407,6 @@ class ModerationQueue extends React.Component {
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<Comment
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
key={comment.id}
|
||||
comment={comment}
|
||||
@@ -476,7 +474,6 @@ ModerationQueue.propTypes = {
|
||||
hasNextPage: PropTypes.bool,
|
||||
comments: PropTypes.array,
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
@@ -111,14 +111,14 @@ class IndicatorContainer extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
const slotPassthrough = {
|
||||
handleCommentChange: this.handleCommentChange,
|
||||
};
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Indicator />
|
||||
<Slot
|
||||
data={this.props.data}
|
||||
handleCommentChange={this.handleCommentChange}
|
||||
fill="adminModerationIndicator"
|
||||
/>
|
||||
<Slot fill="adminModerationIndicator" passthrough={slotPassthrough} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ class ModerationContainer extends Component {
|
||||
comment.status_history[comment.status_history.length - 1]
|
||||
.assigned_by;
|
||||
const notifyText =
|
||||
this.props.auth.user.id === user.id
|
||||
this.props.currentUser.id === user.id
|
||||
? ''
|
||||
: t(
|
||||
'modqueue.notify_accepted',
|
||||
@@ -131,7 +131,7 @@ class ModerationContainer extends Component {
|
||||
comment.status_history[comment.status_history.length - 1]
|
||||
.assigned_by;
|
||||
const notifyText =
|
||||
this.props.auth.user.id === user.id
|
||||
this.props.currentUser.id === user.id
|
||||
? ''
|
||||
: t(
|
||||
'modqueue.notify_rejected',
|
||||
@@ -152,7 +152,7 @@ class ModerationContainer extends Component {
|
||||
comment.status_history[comment.status_history.length - 1]
|
||||
.assigned_by;
|
||||
const notifyText =
|
||||
this.props.auth.user.id === user.id
|
||||
this.props.currentUser.id === user.id
|
||||
? ''
|
||||
: t(
|
||||
'modqueue.notify_reset',
|
||||
@@ -515,7 +515,7 @@ const withModQueueQuery = withQuery(
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
moderation: state.moderation,
|
||||
auth: state.auth,
|
||||
currentUser: state.auth.user,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
@@ -89,3 +89,12 @@
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.statusDropdown {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.statusDropdownOption {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,11 +22,20 @@ class Stories extends Component {
|
||||
const closed = !!(closedAt && new Date(closedAt).getTime() < Date.now());
|
||||
return (
|
||||
<Dropdown
|
||||
className={styles.statusDropdown}
|
||||
value={closed}
|
||||
onChange={value => this.props.onStatusChange(value, id)}
|
||||
>
|
||||
<Option value={false} label={t('streams.open')} />
|
||||
<Option value={true} label={t('streams.closed')} />
|
||||
<Option
|
||||
value={false}
|
||||
label={t('streams.open')}
|
||||
className={styles.statusDropdownOption}
|
||||
/>
|
||||
<Option
|
||||
value={true}
|
||||
label={t('streams.closed')}
|
||||
className={styles.statusDropdownOption}
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
import LinkifyIt from 'linkify-it';
|
||||
import tlds from 'tlds';
|
||||
const linkify = new LinkifyIt();
|
||||
linkify.tlds(tlds);
|
||||
|
||||
export function matchLinks(text) {
|
||||
return linkify.match(text);
|
||||
}
|
||||
|
||||
export const isPremod = mod => mod === 'PRE';
|
||||
|
||||
export const getModPath = (type = 'all', assetId) =>
|
||||
|
||||
@@ -1,14 +1,34 @@
|
||||
import { HANDLE_SUCCESSFUL_LOGIN } from 'coral-framework/constants/auth';
|
||||
import { getStaticConfiguration } from 'coral-framework/services/staticConfiguration';
|
||||
import { createPostMessage } from 'coral-framework/services/postMessage';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Get the auth element and parse it as JSON by decoding it.
|
||||
const auth = document.getElementById('auth');
|
||||
const doc = document.implementation.createHTMLDocument('');
|
||||
doc.body.innerHTML = auth.innerText;
|
||||
try {
|
||||
const staticConfig = getStaticConfiguration();
|
||||
const { STATIC_ORIGIN: origin } = staticConfig;
|
||||
const postMessage = createPostMessage(origin);
|
||||
|
||||
// Set the item in localStorage.
|
||||
localStorage.setItem('auth', doc.body.textContent);
|
||||
// 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;
|
||||
|
||||
// Close the window.
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
}, 50);
|
||||
// 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);
|
||||
} 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 });
|
||||
}
|
||||
} finally {
|
||||
// Always close the window.
|
||||
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());
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Router, Route } from 'react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Embed from './containers/Embed';
|
||||
import { LoginContainer } from './containers/LoginContainer';
|
||||
|
||||
const routes = (
|
||||
<div>
|
||||
<Route exact path="/embed/stream/login" component={LoginContainer} />
|
||||
<Route path="*" component={Embed} />
|
||||
</div>
|
||||
);
|
||||
|
||||
class AppRouter extends React.Component {
|
||||
static contextTypes = {
|
||||
history: PropTypes.object,
|
||||
};
|
||||
|
||||
render() {
|
||||
return <Router history={this.context.history} routes={routes} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default AppRouter;
|
||||
@@ -1,75 +0,0 @@
|
||||
import * as actions from '../constants/asset';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const fetchAssetRequest = () => ({ type: actions.FETCH_ASSET_REQUEST });
|
||||
export const fetchAssetSuccess = asset => ({
|
||||
type: actions.FETCH_ASSET_SUCCESS,
|
||||
asset,
|
||||
});
|
||||
export const fetchAssetFailure = error => ({
|
||||
type: actions.FETCH_ASSET_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
const updateAssetSettingsRequest = () => ({
|
||||
type: actions.UPDATE_ASSET_SETTINGS_REQUEST,
|
||||
});
|
||||
const updateAssetSettingsSuccess = settings => ({
|
||||
type: actions.UPDATE_ASSET_SETTINGS_SUCCESS,
|
||||
settings,
|
||||
});
|
||||
const updateAssetSettingsFailure = error => ({
|
||||
type: actions.UPDATE_ASSET_SETTINGS_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const updateConfiguration = newConfig => (
|
||||
dispatch,
|
||||
getState,
|
||||
{ rest }
|
||||
) => {
|
||||
const assetId = getState().asset.id;
|
||||
dispatch(updateAssetSettingsRequest());
|
||||
rest(`/assets/${assetId}/settings`, { method: 'PUT', body: newConfig })
|
||||
.then(() => {
|
||||
dispatch(notify('success', t('framework.success_update_settings')));
|
||||
dispatch(updateAssetSettingsSuccess(newConfig));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
dispatch(updateAssetSettingsFailure(error));
|
||||
});
|
||||
};
|
||||
|
||||
export const updateOpenStream = closedBody => (
|
||||
dispatch,
|
||||
getState,
|
||||
{ rest }
|
||||
) => {
|
||||
const assetId = getState().asset.id;
|
||||
dispatch(fetchAssetRequest());
|
||||
rest(`/assets/${assetId}/status`, { method: 'PUT', body: closedBody })
|
||||
.then(() => {
|
||||
dispatch(notify('success', t('framework.success_update_settings')));
|
||||
dispatch(fetchAssetSuccess(closedBody));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
dispatch(fetchAssetFailure(error));
|
||||
});
|
||||
};
|
||||
|
||||
const openStream = () => ({ type: actions.OPEN_COMMENTS });
|
||||
const closeStream = () => ({ type: actions.CLOSE_COMMENTS });
|
||||
|
||||
export const updateOpenStatus = status => dispatch => {
|
||||
if (status === 'open') {
|
||||
dispatch(openStream());
|
||||
dispatch(updateOpenStream({ closedAt: null }));
|
||||
} else {
|
||||
dispatch(closeStream());
|
||||
dispatch(updateOpenStream({ closedAt: new Date().getTime() }));
|
||||
}
|
||||
};
|
||||
@@ -1,415 +0,0 @@
|
||||
import jwtDecode from 'jwt-decode';
|
||||
import bowser from 'bowser';
|
||||
import * as actions from '../constants/auth';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import get from 'lodash/get';
|
||||
|
||||
export const updateStatus = status => ({
|
||||
type: actions.UPDATE_STATUS,
|
||||
status,
|
||||
});
|
||||
|
||||
export const showSignInDialog = () => ({
|
||||
type: actions.SHOW_SIGNIN_DIALOG,
|
||||
});
|
||||
|
||||
export const hideSignInDialog = () => dispatch => {
|
||||
if (window.opener && window.opener !== window) {
|
||||
// TODO: We need to address this when we refactor the
|
||||
// login popup out of the embed.
|
||||
|
||||
// we are in a popup
|
||||
window.close();
|
||||
} else {
|
||||
dispatch(checkLogin());
|
||||
}
|
||||
dispatch({ type: actions.HIDE_SIGNIN_DIALOG });
|
||||
};
|
||||
|
||||
export const resetSignInDialog = () => dispatch => {
|
||||
dispatch({ type: actions.HIDE_SIGNIN_DIALOG });
|
||||
};
|
||||
|
||||
export const focusSignInDialog = () => ({
|
||||
type: actions.FOCUS_SIGNIN_DIALOG,
|
||||
});
|
||||
|
||||
export const blurSignInDialog = () => ({
|
||||
type: actions.BLUR_SIGNIN_DIALOG,
|
||||
});
|
||||
|
||||
export const showCreateUsernameDialog = () => ({
|
||||
type: actions.SHOW_CREATEUSERNAME_DIALOG,
|
||||
});
|
||||
|
||||
export const hideCreateUsernameDialog = () => ({
|
||||
type: actions.HIDE_CREATEUSERNAME_DIALOG,
|
||||
});
|
||||
|
||||
export const updateUsername = username => ({
|
||||
type: actions.UPDATE_USERNAME,
|
||||
username,
|
||||
});
|
||||
|
||||
export const changeView = view => dispatch => {
|
||||
dispatch({
|
||||
type: actions.CHANGE_VIEW,
|
||||
view,
|
||||
});
|
||||
|
||||
switch (view) {
|
||||
case 'SIGNUP':
|
||||
window.resizeTo(500, 800);
|
||||
break;
|
||||
case 'FORGOT':
|
||||
window.resizeTo(500, 400);
|
||||
break;
|
||||
default:
|
||||
window.resizeTo(500, 550);
|
||||
}
|
||||
};
|
||||
|
||||
export const cleanState = () => ({
|
||||
type: actions.CLEAN_STATE,
|
||||
});
|
||||
|
||||
// Sign In Actions
|
||||
|
||||
const signInRequest = email => ({
|
||||
type: actions.FETCH_SIGNIN_REQUEST,
|
||||
email,
|
||||
});
|
||||
|
||||
const signInFailure = error => ({
|
||||
type: actions.FETCH_SIGNIN_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
// AUTH TOKEN
|
||||
//==============================================================================
|
||||
|
||||
export const handleAuthToken = token => (dispatch, _, { localStorage }) => {
|
||||
if (localStorage) {
|
||||
localStorage.setItem('exp', jwtDecode(token).exp);
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
|
||||
dispatch({ type: 'HANDLE_AUTH_TOKEN' });
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// SIGN IN
|
||||
//==============================================================================
|
||||
|
||||
export const fetchSignIn = formData => {
|
||||
return (dispatch, _, { rest }) => {
|
||||
dispatch(signInRequest(formData.email));
|
||||
|
||||
return rest('/auth/local', { method: 'POST', body: formData })
|
||||
.then(({ token }) => {
|
||||
if (!bowser.safari && !bowser.ios) {
|
||||
dispatch(handleAuthToken(token));
|
||||
}
|
||||
dispatch(hideSignInDialog());
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
if (error.metadata) {
|
||||
// the user might not have a valid email. prompt the user user re-request the confirmation email
|
||||
dispatch(
|
||||
signInFailure(t('error.email_not_verified', error.metadata))
|
||||
);
|
||||
} else if (error.translation_key === 'NOT_AUTHORIZED') {
|
||||
// invalid credentials
|
||||
dispatch(signInFailure(t('error.email_password'), error.metadata));
|
||||
} else {
|
||||
dispatch(signInFailure(error));
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// SIGN IN - FACEBOOK
|
||||
//==============================================================================
|
||||
|
||||
const signInFacebookRequest = () => ({
|
||||
type: actions.FETCH_SIGNIN_FACEBOOK_REQUEST,
|
||||
});
|
||||
|
||||
const signInFacebookSuccess = user => ({
|
||||
type: actions.FETCH_SIGNIN_FACEBOOK_SUCCESS,
|
||||
user,
|
||||
});
|
||||
|
||||
const signInFacebookFailure = error => ({
|
||||
type: actions.FETCH_SIGNIN_FACEBOOK_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchSignInFacebook = () => (dispatch, _, { rest }) => {
|
||||
dispatch(signInFacebookRequest());
|
||||
window.open(
|
||||
`${rest.uri}/auth/facebook`,
|
||||
'Continue with Facebook',
|
||||
'menubar=0,resizable=0,width=500,height=500,top=200,left=500'
|
||||
);
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// SIGN UP - FACEBOOK
|
||||
//==============================================================================
|
||||
|
||||
const signUpFacebookRequest = () => ({
|
||||
type: actions.FETCH_SIGNUP_FACEBOOK_REQUEST,
|
||||
});
|
||||
|
||||
export const fetchSignUpFacebook = () => (dispatch, _, { rest }) => {
|
||||
dispatch(signUpFacebookRequest());
|
||||
window.open(
|
||||
`${rest.uri}/auth/facebook`,
|
||||
'Continue with Facebook',
|
||||
'menubar=0,resizable=0,width=500,height=500,top=200,left=500'
|
||||
);
|
||||
};
|
||||
|
||||
export const facebookCallback = (err, data) => dispatch => {
|
||||
if (err) {
|
||||
dispatch(signInFacebookFailure(err));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
dispatch(handleAuthToken(data.token));
|
||||
dispatch(signInFacebookSuccess(data.user));
|
||||
dispatch(hideSignInDialog());
|
||||
} catch (err) {
|
||||
dispatch(signInFacebookFailure(err));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// SIGN UP
|
||||
//==============================================================================
|
||||
|
||||
const signUpRequest = () => ({ type: actions.FETCH_SIGNUP_REQUEST });
|
||||
const signUpSuccess = user => ({ type: actions.FETCH_SIGNUP_SUCCESS, user });
|
||||
const signUpFailure = error => ({ type: actions.FETCH_SIGNUP_FAILURE, error });
|
||||
|
||||
export const fetchSignUp = formData => (dispatch, getState, { rest }) => {
|
||||
const redirectUri = getState().auth.redirectUri;
|
||||
dispatch(signUpRequest());
|
||||
|
||||
rest('/users', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: { 'X-Pym-Url': redirectUri },
|
||||
})
|
||||
.then(({ user }) => {
|
||||
dispatch(signUpSuccess(user));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch(signUpFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// FORGOT PASSWORD
|
||||
//==============================================================================
|
||||
|
||||
const forgotPasswordRequest = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_REQUEST,
|
||||
});
|
||||
|
||||
const forgotPasswordSuccess = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS,
|
||||
});
|
||||
|
||||
const forgotPasswordFailure = error => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchForgotPassword = email => (dispatch, getState, { rest }) => {
|
||||
dispatch(forgotPasswordRequest(email));
|
||||
const redirectUri = getState().auth.redirectUri;
|
||||
rest('/account/password/reset', {
|
||||
method: 'POST',
|
||||
body: { email, loc: redirectUri },
|
||||
})
|
||||
.then(() => dispatch(forgotPasswordSuccess()))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch(forgotPasswordFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// LOGOUT
|
||||
//==============================================================================
|
||||
|
||||
export const logout = () => async (
|
||||
dispatch,
|
||||
_,
|
||||
{ rest, client, pym, localStorage }
|
||||
) => {
|
||||
await rest('/auth', { method: 'DELETE' });
|
||||
|
||||
if (localStorage) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('exp');
|
||||
}
|
||||
|
||||
// Reset the websocket.
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch({ type: actions.LOGOUT });
|
||||
pym.sendMessage('coral-auth-changed');
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// CHECK LOGIN
|
||||
//==============================================================================
|
||||
|
||||
const checkLoginRequest = () => ({ type: actions.CHECK_LOGIN_REQUEST });
|
||||
const checkLoginFailure = error => ({
|
||||
type: actions.CHECK_LOGIN_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
const checkLoginSuccess = (user, isAdmin) => ({
|
||||
type: actions.CHECK_LOGIN_SUCCESS,
|
||||
user,
|
||||
isAdmin,
|
||||
});
|
||||
|
||||
const ErrNotLoggedIn = new Error('Not logged in');
|
||||
|
||||
export const checkLogin = () => (
|
||||
dispatch,
|
||||
_,
|
||||
{ rest, client, pym, localStorage }
|
||||
) => {
|
||||
dispatch(checkLoginRequest());
|
||||
rest('/auth')
|
||||
.then(result => {
|
||||
if (!result.user) {
|
||||
if (localStorage) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('exp');
|
||||
}
|
||||
throw ErrNotLoggedIn;
|
||||
}
|
||||
|
||||
// Reset the websocket.
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch(checkLoginSuccess(result.user));
|
||||
pym.sendMessage('coral-auth-changed', JSON.stringify(result.user));
|
||||
|
||||
// This is for login via social. Usernames should be set.
|
||||
if (
|
||||
get(result.user, 'status.username.status') === 'UNSET' &&
|
||||
!get(result.user, 'status.banned.status')
|
||||
) {
|
||||
dispatch(showCreateUsernameDialog());
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error !== ErrNotLoggedIn) {
|
||||
console.error(error);
|
||||
}
|
||||
if (error.status && error.status === 401 && localStorage) {
|
||||
// Unauthorized.
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('exp');
|
||||
}
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch(checkLoginFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
export const validForm = () => ({ type: actions.VALID_FORM });
|
||||
export const invalidForm = error => ({ type: actions.INVALID_FORM, error });
|
||||
|
||||
//==============================================================================
|
||||
// VERIFY EMAIL
|
||||
//==============================================================================
|
||||
|
||||
const verifyEmailRequest = () => ({
|
||||
type: actions.VERIFY_EMAIL_REQUEST,
|
||||
});
|
||||
|
||||
const verifyEmailSuccess = () => ({
|
||||
type: actions.VERIFY_EMAIL_SUCCESS,
|
||||
});
|
||||
|
||||
const verifyEmailFailure = error => ({
|
||||
type: actions.VERIFY_EMAIL_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const requestConfirmEmail = email => (dispatch, getState, { rest }) => {
|
||||
const redirectUri = getState().auth.redirectUri;
|
||||
dispatch(verifyEmailRequest());
|
||||
return rest('/users/resend-verify', {
|
||||
method: 'POST',
|
||||
body: { email },
|
||||
headers: { 'X-Pym-Url': redirectUri },
|
||||
})
|
||||
.then(() => {
|
||||
dispatch(verifyEmailSuccess());
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
dispatch(verifyEmailFailure(error));
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
// Login Popup actions.
|
||||
export const setRequireEmailVerification = required => ({
|
||||
type: actions.SET_REQUIRE_EMAIL_VERIFICATION,
|
||||
required,
|
||||
});
|
||||
|
||||
export const setRedirectUri = uri => ({
|
||||
type: actions.SET_REDIRECT_URI,
|
||||
uri,
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
// Edit Username
|
||||
//==============================================================================
|
||||
|
||||
const editUsernameFailure = error => ({
|
||||
type: actions.EDIT_USERNAME_FAILURE,
|
||||
error,
|
||||
});
|
||||
const editUsernameSuccess = () => ({ type: actions.EDIT_USERNAME_SUCCESS });
|
||||
|
||||
export const editName = username => (dispatch, _, { rest }) => {
|
||||
return rest('/account/username', { method: 'PUT', body: { username } })
|
||||
.then(() => {
|
||||
dispatch(editUsernameSuccess());
|
||||
dispatch(notify('success', t('framework.success_name_update')));
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key
|
||||
? t(`error.${error.translation_key}`)
|
||||
: error.toString();
|
||||
dispatch(editUsernameFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import { ADD_EXTERNAL_CONFIG } from '../constants/config';
|
||||
|
||||
export const addExternalConfig = config => ({
|
||||
type: ADD_EXTERNAL_CONFIG,
|
||||
config,
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import * as actions from '../constants/login';
|
||||
|
||||
export const showSignInDialog = () => ({
|
||||
type: actions.SHOW_SIGNIN_DIALOG,
|
||||
});
|
||||
|
||||
export const hideSignInDialog = () => ({ type: actions.HIDE_SIGNIN_DIALOG });
|
||||
|
||||
export const focusSignInDialog = () => ({
|
||||
type: actions.FOCUS_SIGNIN_DIALOG,
|
||||
});
|
||||
|
||||
export const blurSignInDialog = () => ({
|
||||
type: actions.BLUR_SIGNIN_DIALOG,
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import * as actions from '../constants/profile';
|
||||
|
||||
export const setActiveTab = tab => ({ type: actions.SET_ACTIVE_TAB, tab });
|
||||
@@ -71,16 +71,19 @@ export const setActiveTab = tab => dispatch => {
|
||||
dispatch({ type: actions.SET_ACTIVE_TAB, tab });
|
||||
};
|
||||
|
||||
// @Deprecated
|
||||
export const addCommentBoxTag = tag => ({
|
||||
type: actions.ADD_COMMENT_BOX_TAG,
|
||||
tag,
|
||||
});
|
||||
|
||||
// @Deprecated
|
||||
export const removeCommentBoxTag = idx => ({
|
||||
type: actions.REMOVE_COMMENT_BOX_TAG,
|
||||
idx,
|
||||
});
|
||||
|
||||
// @Deprecated
|
||||
export const clearCommentBoxTags = () => ({
|
||||
type: actions.CLEAR_COMMENT_BOX_TAGS,
|
||||
});
|
||||
|
||||
@@ -9,24 +9,13 @@ import AutomaticAssetClosure from '../containers/AutomaticAssetClosure';
|
||||
|
||||
import ExtendableTabPanel from '../containers/ExtendableTabPanel';
|
||||
import { Tab, TabPane } from 'coral-ui';
|
||||
import ProfileContainer from '../tabs/profile/containers/ProfileContainer';
|
||||
import Profile from '../tabs/profile/containers/Profile';
|
||||
import Popup from 'coral-framework/components/Popup';
|
||||
import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty';
|
||||
import cn from 'classnames';
|
||||
|
||||
export default class Embed extends React.Component {
|
||||
changeTab = tab => {
|
||||
// TODO: move data fetching to appropiate containers.
|
||||
switch (tab) {
|
||||
case 'profile':
|
||||
this.props.data.refetch();
|
||||
break;
|
||||
}
|
||||
this.props.setActiveTab(tab);
|
||||
};
|
||||
|
||||
getTabs() {
|
||||
const { user } = this.props.auth;
|
||||
const tabs = [
|
||||
<Tab
|
||||
key="stream"
|
||||
@@ -43,7 +32,7 @@ export default class Embed extends React.Component {
|
||||
{t('framework.my_profile')}
|
||||
</Tab>,
|
||||
];
|
||||
if (can(user, 'UPDATE_ASSET_CONFIG')) {
|
||||
if (can(this.props.currentUser, 'UPDATE_ASSET_CONFIG')) {
|
||||
tabs.push(
|
||||
<Tab
|
||||
key="config"
|
||||
@@ -60,17 +49,21 @@ export default class Embed extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
commentId,
|
||||
root,
|
||||
root: { asset },
|
||||
data,
|
||||
auth: { showSignInDialog, signInDialogFocus },
|
||||
showSignInDialog,
|
||||
signInDialogFocus,
|
||||
blurSignInDialog,
|
||||
focusSignInDialog,
|
||||
hideSignInDialog,
|
||||
router: { location: { query: { parentUrl } } },
|
||||
parentUrl,
|
||||
} = this.props;
|
||||
const hasHighlightedComment = !!commentId;
|
||||
const popupUrl = `login?parentUrl=${encodeURIComponent(parentUrl)}`;
|
||||
const slotPassthrough = { root };
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -81,9 +74,7 @@ export default class Embed extends React.Component {
|
||||
<AutomaticAssetClosure asset={asset} />
|
||||
<IfSlotIsNotEmpty slot="login">
|
||||
<Popup
|
||||
href={`embed/stream/login?parentUrl=${encodeURIComponent(
|
||||
parentUrl
|
||||
)}`}
|
||||
href={popupUrl}
|
||||
title="Login"
|
||||
features="menubar=0,resizable=0,width=500,height=550,top=200,left=500"
|
||||
open={showSignInDialog}
|
||||
@@ -94,18 +85,17 @@ export default class Embed extends React.Component {
|
||||
/>
|
||||
</IfSlotIsNotEmpty>
|
||||
|
||||
<Slot data={data} queryData={{ root }} fill="embed" />
|
||||
<Slot passthrough={slotPassthrough} fill="embed" />
|
||||
|
||||
<ExtendableTabPanel
|
||||
className="talk-embed-stream-tab-bar"
|
||||
activeTab={activeTab}
|
||||
setActiveTab={this.changeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
fallbackTab="stream"
|
||||
tabSlot="embedStreamTabs"
|
||||
tabSlotPrepend="embedStreamTabsPrepend"
|
||||
tabPaneSlot="embedStreamTabPanes"
|
||||
slotProps={{ data }}
|
||||
queryData={{ root }}
|
||||
slotPassthrough={slotPassthrough}
|
||||
tabs={this.getTabs()}
|
||||
tabPanes={[
|
||||
<TabPane
|
||||
@@ -120,7 +110,7 @@ export default class Embed extends React.Component {
|
||||
tabId="profile"
|
||||
className="talk-embed-stream-profile-tab-pane"
|
||||
>
|
||||
<ProfileContainer />
|
||||
<Profile />
|
||||
</TabPane>,
|
||||
<TabPane
|
||||
key="config"
|
||||
@@ -138,17 +128,15 @@ export default class Embed extends React.Component {
|
||||
|
||||
Embed.propTypes = {
|
||||
setActiveTab: PropTypes.func,
|
||||
auth: PropTypes.object,
|
||||
currentUser: PropTypes.object,
|
||||
showSignInDialog: PropTypes.bool,
|
||||
signInDialogFocus: PropTypes.bool,
|
||||
blurSignInDialog: PropTypes.func,
|
||||
focusSignInDialog: PropTypes.func,
|
||||
hideSignInDialog: PropTypes.func,
|
||||
router: PropTypes.object,
|
||||
parentUrl: PropTypes.string,
|
||||
commentId: PropTypes.string,
|
||||
root: PropTypes.object,
|
||||
activeTab: PropTypes.string,
|
||||
data: PropTypes.shape({
|
||||
loading: PropTypes.bool,
|
||||
error: PropTypes.object,
|
||||
refetch: PropTypes.func,
|
||||
}).isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
export const FETCH_ASSET_REQUEST = 'FETCH_ASSET_REQUEST';
|
||||
export const FETCH_ASSET_FAILURE = 'FETCH_ASSET_FAILURE';
|
||||
export const FETCH_ASSET_SUCCESS = 'FETCH_ASSET_SUCCESS';
|
||||
|
||||
export const UPDATE_ASSET_SETTINGS_REQUEST = 'UPDATE_ASSET_SETTINGS_REQUEST';
|
||||
export const UPDATE_ASSET_SETTINGS_SUCCESS = 'UPDATE_ASSET_SETTINGS_SUCCESS';
|
||||
export const UPDATE_ASSET_SETTINGS_FAILURE = 'UPDATE_ASSET_SETTINGS_FAILURE';
|
||||
|
||||
export const OPEN_COMMENTS = 'OPEN_COMMENTS';
|
||||
export const CLOSE_COMMENTS = 'CLOSE_COMMENTS';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user