mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f83aea0431 | ||
|
|
c2133eb009 | ||
|
|
20187ad365 | ||
|
|
02326cd3a7 | ||
|
|
6fe453415d | ||
|
|
05b29f2f06 | ||
|
|
28afcdd007 | ||
|
|
a128c7cb76 | ||
|
|
cf61ca0485 | ||
|
|
7c9fd2acb3 | ||
|
|
7e5ff7bf96 | ||
|
|
34e3266a6f | ||
|
|
bc11eaa9c8 | ||
|
|
969d2a1588 | ||
|
|
cd412056ad | ||
|
|
0865c4d5ad | ||
|
|
dca9b19240 | ||
|
|
5b9d24a029 | ||
|
|
b646320b92 | ||
|
|
864012e5ab | ||
|
|
e86cfaaa5b | ||
|
|
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 | ||
|
|
82d520da0c | ||
|
|
bba52b4bbd | ||
|
|
896b12a7b7 | ||
|
|
3df1bed645 | ||
|
|
6ca7f3b53e | ||
|
|
beb301cf22 | ||
|
|
999a0710f5 | ||
|
|
3a92aa33a9 | ||
|
|
7a505ac5fc | ||
|
|
be75980edb | ||
|
|
8a5118728b | ||
|
|
e39e0b7875 | ||
|
|
d5df9ba8e4 | ||
|
|
e67aea48af | ||
|
|
935ba6e9fe | ||
|
|
4881c33942 | ||
|
|
e89b5da864 | ||
|
|
d80a2d84c5 | ||
|
|
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 | ||
|
|
a7e9936671 | ||
|
|
9617948493 | ||
|
|
284009df55 | ||
|
|
34922a6d42 | ||
|
|
8b4b1728bc | ||
|
|
233c514251 | ||
|
|
a9e8f390b8 | ||
|
|
ee04d9a848 | ||
|
|
0ca8b978a1 | ||
|
|
3deb56750f | ||
|
|
12497a1331 | ||
|
|
45dde4636d | ||
|
|
8c2070e345 | ||
|
|
43d4db085b | ||
|
|
b600172380 | ||
|
|
0b547b173a | ||
|
|
499a7fe686 | ||
|
|
91aeb9dbae | ||
|
|
9a89dfaa29 | ||
|
|
634da8012f | ||
|
|
4c06d5659d | ||
|
|
181d258b5e | ||
|
|
36e855237f | ||
|
|
376ffd8486 | ||
|
|
214d9e5047 | ||
|
|
a867b27f53 | ||
|
|
28a348f2cc | ||
|
|
6c1667d80b | ||
|
|
5d1eff0ddb | ||
|
|
5221ebbe9a | ||
|
|
79cb8b0fb2 | ||
|
|
90290cfa2d | ||
|
|
8b669a31c5 | ||
|
|
aaa1e0dbc3 | ||
|
|
eab1e6ca59 | ||
|
|
b758dc91cb | ||
|
|
cdf3c44083 | ||
|
|
26cba9af63 | ||
|
|
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
|
||||
-->
|
||||
|
||||
+35
-21
@@ -14,6 +14,7 @@ client/coral-framework/graphql/introspection.json
|
||||
*.swp
|
||||
*.DS_STORE
|
||||
.prettierrc.json
|
||||
.vscode
|
||||
|
||||
coverage/
|
||||
test/e2e/reports/
|
||||
@@ -22,36 +23,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
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"exec": "npm-run-all --parallel generate-introspection start:development",
|
||||
"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
|
||||
|
||||
|
||||
@@ -18,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,7 +1,6 @@
|
||||
const express = require('express');
|
||||
const morgan = require('morgan');
|
||||
const path = require('path');
|
||||
const uuid = require('uuid');
|
||||
const merge = require('lodash/merge');
|
||||
const helmet = require('helmet');
|
||||
const plugins = require('./services/plugins');
|
||||
@@ -13,13 +12,6 @@ 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();
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
// PLUGIN PRE APPLICATION MIDDLEWARE
|
||||
//==============================================================================
|
||||
|
||||
+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);
|
||||
});
|
||||
|
||||
+3
-1
@@ -15,6 +15,7 @@ const SetupService = require('../services/setup');
|
||||
const UsersService = require('../services/users');
|
||||
const MigrationService = require('../services/migration');
|
||||
const errors = require('../errors');
|
||||
const Context = require('../graph/context');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
@@ -184,7 +185,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
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -11,6 +11,7 @@ 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 {
|
||||
render() {
|
||||
@@ -47,10 +48,7 @@ class LayoutContainer extends React.Component {
|
||||
} else {
|
||||
return (
|
||||
<Layout {...this.props} handleLogout={logout}>
|
||||
<p>
|
||||
This page is for team use only. Please contact an administrator if
|
||||
you want to join this team.
|
||||
</p>
|
||||
<Forbidden />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
}
|
||||
|
||||
@@ -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')}
|
||||
/>
|
||||
|
||||
@@ -75,7 +75,6 @@ export default class Configure extends Component {
|
||||
</div>
|
||||
<div className={styles.mainContent}>
|
||||
<SectionComponent
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
settings={this.props.settings}
|
||||
/>
|
||||
@@ -88,7 +87,6 @@ export default class Configure extends Component {
|
||||
Configure.propTypes = {
|
||||
savePending: PropTypes.func.isRequired,
|
||||
currentUser: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
canSave: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -31,7 +31,6 @@ class ConfigureContainer extends Component {
|
||||
return (
|
||||
<Configure
|
||||
currentUser={this.props.currentUser}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
settings={this.props.mergedSettings}
|
||||
canSave={this.props.canSave}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
@@ -184,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}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,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,6 +0,0 @@
|
||||
import { ADD_EXTERNAL_CONFIG } from '../constants/config';
|
||||
|
||||
export const addExternalConfig = config => ({
|
||||
type: ADD_EXTERNAL_CONFIG,
|
||||
config,
|
||||
});
|
||||
@@ -1,14 +1,10 @@
|
||||
import * as actions from '../constants/login';
|
||||
import { checkLogin } from 'coral-framework/actions/auth';
|
||||
|
||||
export const showSignInDialog = () => ({
|
||||
type: actions.SHOW_SIGNIN_DIALOG,
|
||||
});
|
||||
|
||||
export const hideSignInDialog = () => dispatch => {
|
||||
dispatch(checkLogin());
|
||||
dispatch({ type: actions.HIDE_SIGNIN_DIALOG });
|
||||
};
|
||||
export const hideSignInDialog = () => ({ type: actions.HIDE_SIGNIN_DIALOG });
|
||||
|
||||
export const focusSignInDialog = () => ({
|
||||
type: actions.FOCUS_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,16 +9,12 @@ 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 => {
|
||||
this.props.setActiveTab(tab);
|
||||
};
|
||||
|
||||
getTabs() {
|
||||
const tabs = [
|
||||
<Tab
|
||||
@@ -53,6 +49,7 @@ export default class Embed extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
commentId,
|
||||
root,
|
||||
root: { asset },
|
||||
@@ -65,6 +62,8 @@ export default class Embed extends React.Component {
|
||||
parentUrl,
|
||||
} = this.props;
|
||||
const hasHighlightedComment = !!commentId;
|
||||
const popupUrl = `login?parentUrl=${encodeURIComponent(parentUrl)}`;
|
||||
const slotPassthrough = { root };
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -75,7 +74,7 @@ export default class Embed extends React.Component {
|
||||
<AutomaticAssetClosure asset={asset} />
|
||||
<IfSlotIsNotEmpty slot="login">
|
||||
<Popup
|
||||
href={`login?parentUrl=${encodeURIComponent(parentUrl)}`}
|
||||
href={popupUrl}
|
||||
title="Login"
|
||||
features="menubar=0,resizable=0,width=500,height=550,top=200,left=500"
|
||||
open={showSignInDialog}
|
||||
@@ -86,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
|
||||
@@ -112,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"
|
||||
@@ -140,9 +138,5 @@ Embed.propTypes = {
|
||||
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';
|
||||
@@ -1 +0,0 @@
|
||||
export const ADD_EXTERNAL_CONFIG = 'ADD_EXTERNAL_CONFIG';
|
||||
@@ -0,0 +1,2 @@
|
||||
const prefix = 'TALK_EMBED_STREAM';
|
||||
export const SET_ACTIVE_TAB = `${prefix}_SET_ACTIVE_TAB`;
|
||||
@@ -2,7 +2,6 @@ import React from 'react';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import get from 'lodash/get';
|
||||
import branch from 'recompose/branch';
|
||||
import renderComponent from 'recompose/renderComponent';
|
||||
@@ -14,12 +13,11 @@ import {
|
||||
hideSignInDialog,
|
||||
} from '../actions/login';
|
||||
import { updateStatus } from 'coral-framework/actions/auth';
|
||||
import { fetchAssetSuccess } from '../actions/asset';
|
||||
import {
|
||||
getDefinitionName,
|
||||
getSlotFragmentSpreads,
|
||||
} from 'coral-framework/utils';
|
||||
import { withQuery } from 'coral-framework/hocs';
|
||||
import { withQuery, withPopupAuthHandler } from 'coral-framework/hocs';
|
||||
import Embed from '../components/Embed';
|
||||
import Stream from '../tabs/stream/containers/Stream';
|
||||
import AutomaticAssetClosure from './AutomaticAssetClosure';
|
||||
@@ -107,12 +105,6 @@ class EmbedContainer extends React.Component {
|
||||
this.props.data.refetch();
|
||||
this.resubscribe(nextProps);
|
||||
}
|
||||
|
||||
const { fetchAssetSuccess } = this.props;
|
||||
if (!isEqual(nextProps.root.asset, this.props.root.asset)) {
|
||||
// TODO: remove asset data from redux store.
|
||||
fetchAssetSuccess(nextProps.root.asset);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@@ -298,7 +290,6 @@ EmbedContainer.propTypes = {
|
||||
activeTab: PropTypes.string,
|
||||
parentUrl: PropTypes.string,
|
||||
data: PropTypes.object,
|
||||
fetchAssetSuccess: PropTypes.func,
|
||||
showSignInDialog: PropTypes.bool,
|
||||
signInDialogFocus: PropTypes.bool,
|
||||
};
|
||||
@@ -322,7 +313,6 @@ const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
setActiveTab,
|
||||
fetchAssetSuccess,
|
||||
notify,
|
||||
focusSignInDialog,
|
||||
blurSignInDialog,
|
||||
@@ -333,6 +323,7 @@ const mapDispatchToProps = dispatch =>
|
||||
);
|
||||
|
||||
export default compose(
|
||||
withPopupAuthHandler,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
branch(props => !props.checkedInitialLogin, renderComponent(Spinner)),
|
||||
withEmbedQuery
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import React from 'react';
|
||||
import ExtendableTabPanel from '../components/ExtendableTabPanel';
|
||||
import { connect } from 'react-redux';
|
||||
import { TabPane } from 'coral-ui';
|
||||
import ExtendableTab from '../components/ExtendableTab';
|
||||
import { getShallowChanges } from 'coral-framework/utils';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withSlotElements } from 'coral-framework/hocs';
|
||||
import { compose } from 'recompose';
|
||||
|
||||
class ExtendableTabPanelContainer extends React.Component {
|
||||
static contextTypes = {
|
||||
plugins: PropTypes.object,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.handleFallback();
|
||||
}
|
||||
@@ -20,24 +15,6 @@ class ExtendableTabPanelContainer extends React.Component {
|
||||
this.handleFallback(next);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(next) {
|
||||
// Prevent Slot from rerendering when only reduxState has changed and
|
||||
// it does not result in a change of slot children.
|
||||
const changes = getShallowChanges(this.props, next);
|
||||
if (changes.length === 1 && changes[0] === 'reduxState') {
|
||||
const prevKeys = this.getSlotElements(this.props.tabSlot, this.props).map(
|
||||
el => el.key
|
||||
);
|
||||
const nextKeys = this.getSlotElements(next.tabSlot, next).map(
|
||||
el => el.key
|
||||
);
|
||||
return !isEqual(prevKeys, nextKeys);
|
||||
}
|
||||
|
||||
// Prevent Slot from rerendering when no props has shallowly changed.
|
||||
return changes.length !== 0;
|
||||
}
|
||||
|
||||
handleFallback(props = this.props) {
|
||||
if (this.getTabNames(props).indexOf(props.activeTab) === -1) {
|
||||
props.setActiveTab(props.fallbackTab);
|
||||
@@ -48,38 +25,26 @@ class ExtendableTabPanelContainer extends React.Component {
|
||||
return this.getTabElements(props).map(el => el.props.tabId);
|
||||
}
|
||||
|
||||
getSlotElements(slot, props = this.props) {
|
||||
const { plugins } = this.context;
|
||||
return plugins.getSlotElements(
|
||||
slot,
|
||||
props.reduxState,
|
||||
props.slotProps,
|
||||
props.queryData
|
||||
);
|
||||
}
|
||||
|
||||
getPluginTabElements(props = this.props) {
|
||||
return this.getSlotTabElements(props.tabSlot);
|
||||
return props.slotElements[0].map(this.createPluginTabFactory(props));
|
||||
}
|
||||
|
||||
getPluginTabElementsPrepend(props = this.props) {
|
||||
return this.getSlotTabElements(props.tabSlotPrepend);
|
||||
return props.slotElements[1].map(this.createPluginTabFactory(props));
|
||||
}
|
||||
|
||||
getSlotTabElements(slot) {
|
||||
return this.getSlotElements(slot).map(el => {
|
||||
return (
|
||||
<ExtendableTab
|
||||
tabId={el.type.talkPluginName}
|
||||
key={el.type.talkPluginName}
|
||||
>
|
||||
{React.cloneElement(el, {
|
||||
active: this.props.activeTab === el.type.talkPluginName,
|
||||
})}
|
||||
</ExtendableTab>
|
||||
);
|
||||
});
|
||||
}
|
||||
createPluginTabFactory = (props = this.props) => el => {
|
||||
return (
|
||||
<ExtendableTab
|
||||
tabId={el.type.talkPluginName}
|
||||
key={el.type.talkPluginName}
|
||||
>
|
||||
{React.cloneElement(el, {
|
||||
active: props.activeTab === el.type.talkPluginName,
|
||||
})}
|
||||
</ExtendableTab>
|
||||
);
|
||||
};
|
||||
|
||||
getTabElements(props = this.props) {
|
||||
const elements = [...this.getPluginTabElementsPrepend(props)];
|
||||
@@ -92,14 +57,16 @@ class ExtendableTabPanelContainer extends React.Component {
|
||||
return elements;
|
||||
}
|
||||
|
||||
createPluginTabPane(el) {
|
||||
return (
|
||||
<TabPane tabId={el.type.talkPluginName} key={el.type.talkPluginName}>
|
||||
{el}
|
||||
</TabPane>
|
||||
);
|
||||
}
|
||||
|
||||
getPluginTabPaneElements(props = this.props) {
|
||||
return this.getSlotElements(props.tabPaneSlot).map(el => {
|
||||
return (
|
||||
<TabPane tabId={el.type.talkPluginName} key={el.type.talkPluginName}>
|
||||
{el}
|
||||
</TabPane>
|
||||
);
|
||||
});
|
||||
return props.slotElements[2].map(this.createPluginTabPane);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -132,15 +99,15 @@ ExtendableTabPanelContainer.propTypes = {
|
||||
tabSlot: PropTypes.string.isRequired,
|
||||
tabSlotPrepend: PropTypes.string.isRequired,
|
||||
tabPaneSlot: PropTypes.string.isRequired,
|
||||
slotProps: PropTypes.object.isRequired,
|
||||
queryData: PropTypes.object,
|
||||
slotPassthrough: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
sub: PropTypes.bool,
|
||||
loading: PropTypes.bool,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
reduxState: state,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(ExtendableTabPanelContainer);
|
||||
export default compose(
|
||||
withSlotElements({
|
||||
slot: props => [props.tabSlot, props.tabSlotPrepend, props.tabPaneSlot],
|
||||
passthroughPropName: 'slotPassthrough',
|
||||
})
|
||||
)(ExtendableTabPanelContainer);
|
||||
|
||||
@@ -141,7 +141,7 @@ export function findCommentInAsset(asset, callbackOrId) {
|
||||
callback = node => node.id === callbackOrId;
|
||||
}
|
||||
if (asset.comment) {
|
||||
return findComment([getTopLevelParent(asset.comment)], callback);
|
||||
return findComment([reverseCommentParentTree(asset.comment)], callback);
|
||||
}
|
||||
if (!asset.comments) {
|
||||
return false;
|
||||
@@ -187,11 +187,12 @@ export function insertFetchedCommentsIntoEmbedQuery(root, comments, parent_id) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* attachCommentToParent recurses through the comment tree starting at `topLevelComment`
|
||||
* to find the parent of `comment` and attach it to the replies.
|
||||
*/
|
||||
export function attachCommentToParent(topLevelComment, comment) {
|
||||
function attachComment(topLevelComment, comment) {
|
||||
if (!topLevelComment.replies) {
|
||||
topLevelComment = update(topLevelComment, {
|
||||
replies: { $set: { nodes: [] } },
|
||||
});
|
||||
}
|
||||
if (topLevelComment.id === comment.parent.id) {
|
||||
return update(topLevelComment, {
|
||||
replies: {
|
||||
@@ -204,16 +205,40 @@ export function attachCommentToParent(topLevelComment, comment) {
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!topLevelComment.replies.nodes.length) {
|
||||
return topLevelComment;
|
||||
}
|
||||
return update(topLevelComment, {
|
||||
replies: {
|
||||
nodes: {
|
||||
$apply: nodes =>
|
||||
nodes.map(node => attachCommentToParent(node, comment)),
|
||||
$apply: nodes => nodes.map(node => attachComment(node, comment)),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* attachCommentToParent recurses through the comment tree starting at `topLevelComment`
|
||||
* to find the ancestor of `comment` and attach it to the replies.
|
||||
*/
|
||||
export function attachCommentToParent(topLevelComment, comment) {
|
||||
let result = topLevelComment;
|
||||
if (comment.parent.parent) {
|
||||
result = attachCommentToParent(result, comment.parent);
|
||||
}
|
||||
return attachComment(result, comment);
|
||||
}
|
||||
|
||||
/**
|
||||
* reverseCommentParentTree reverses a comment parent relationship tree
|
||||
* like `comment -> parent -> parent` into `parent -> parent -> comment -> replies`.
|
||||
*/
|
||||
export function reverseCommentParentTree(comment) {
|
||||
return comment.parent
|
||||
? attachCommentToParent(getTopLevelParent(comment), comment)
|
||||
: comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nest a string in itself repeatly until `level` has been reached.
|
||||
*
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import * as actions from '../constants/asset';
|
||||
|
||||
const initialState = {
|
||||
closedAt: null,
|
||||
settings: null,
|
||||
title: null,
|
||||
url: null,
|
||||
features: {},
|
||||
status: 'open',
|
||||
moderation: null,
|
||||
};
|
||||
|
||||
export default function asset(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.FETCH_ASSET_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
...action.asset,
|
||||
};
|
||||
case actions.UPDATE_ASSET_SETTINGS_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
settings: action.settings,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { ADD_EXTERNAL_CONFIG } from '../constants/config';
|
||||
|
||||
const initialState = {};
|
||||
|
||||
export default function config(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case ADD_EXTERNAL_CONFIG:
|
||||
return {
|
||||
...state,
|
||||
...action.config,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ const initialState = {
|
||||
errors: {},
|
||||
};
|
||||
|
||||
export default function config(state = initialState, action) {
|
||||
export default function configure(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.UPDATE_PENDING: {
|
||||
let next = state;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import login from './login';
|
||||
import asset from './asset';
|
||||
import embed from './embed';
|
||||
import configure from './configure';
|
||||
import stream from './stream';
|
||||
import profile from './profile';
|
||||
|
||||
export default {
|
||||
login,
|
||||
asset,
|
||||
embed,
|
||||
configure,
|
||||
stream,
|
||||
profile,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as actions from '../constants/profile';
|
||||
|
||||
const initialState = {
|
||||
activeTab: 'comments',
|
||||
previousTab: '',
|
||||
};
|
||||
|
||||
export default function stream(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.SET_ACTIVE_TAB:
|
||||
return {
|
||||
...state,
|
||||
activeTab: action.tab,
|
||||
previousTab: state.activeTab,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,7 @@ class Configure extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="talk-embed-stream-configuration-container">
|
||||
<Settings
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
asset={this.props.asset}
|
||||
/>
|
||||
<Settings root={this.props.root} asset={this.props.asset} />
|
||||
<hr />
|
||||
<AssetStatusInfo asset={this.props.asset} />
|
||||
</div>
|
||||
@@ -20,7 +16,6 @@ class Configure extends React.Component {
|
||||
}
|
||||
|
||||
Configure.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
asset: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
@@ -1,38 +1,15 @@
|
||||
import React from 'react';
|
||||
import QuestionBox from '../../../components/QuestionBox';
|
||||
import { Icon, Spinner } from 'coral-ui';
|
||||
import DefaultQuestionBoxIcon from '../../../components/DefaultQuestionBoxIcon';
|
||||
import cn from 'classnames';
|
||||
import styles from './QuestionBoxBuilder.css';
|
||||
import { Icon } from 'coral-ui';
|
||||
import MarkdownEditor from 'coral-framework/components/MarkdownEditor';
|
||||
|
||||
const DefaultIcon = <DefaultQuestionBoxIcon className={styles.defaultIcon} />;
|
||||
|
||||
const icons = [{ default: DefaultIcon }, 'forum', 'build', 'format_quote'];
|
||||
|
||||
class QuestionBoxBuilder extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
loading: true,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.loadEditor();
|
||||
}
|
||||
|
||||
async loadEditor() {
|
||||
const {
|
||||
default: MarkdownEditor,
|
||||
} = await import('coral-framework/components/MarkdownEditor');
|
||||
|
||||
return this.setState({
|
||||
loading: false,
|
||||
MarkdownEditor,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
questionBoxIcon,
|
||||
@@ -40,11 +17,6 @@ class QuestionBoxBuilder extends React.Component {
|
||||
onContentChange,
|
||||
onIconChange,
|
||||
} = this.props;
|
||||
const { loading, MarkdownEditor } = this.state;
|
||||
|
||||
if (loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
|
||||
@@ -25,9 +25,9 @@ class Settings extends React.Component {
|
||||
onQuestionBoxContentChange,
|
||||
canSave,
|
||||
onApply,
|
||||
slotProps,
|
||||
queryData,
|
||||
slotPassthrough,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.container}>
|
||||
@@ -77,7 +77,7 @@ class Settings extends React.Component {
|
||||
</div>
|
||||
)}
|
||||
</Configuration>
|
||||
<Slot fill="streamSettings" queryData={queryData} {...slotProps} />
|
||||
<Slot fill="streamSettings" passthrough={slotPassthrough} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -85,8 +85,7 @@ class Settings extends React.Component {
|
||||
}
|
||||
|
||||
Settings.propTypes = {
|
||||
queryData: PropTypes.object.isRequired,
|
||||
slotProps: PropTypes.object.isRequired,
|
||||
slotPassthrough: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
canSave: PropTypes.bool.isRequired,
|
||||
onToggleModeration: PropTypes.func.isRequired,
|
||||
|
||||
@@ -13,13 +13,7 @@ class ConfigureContainer extends React.Component {
|
||||
return <div>{this.props.data.error.message}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Configure
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
asset={this.props.asset}
|
||||
/>
|
||||
);
|
||||
return <Configure root={this.props.root} asset={this.props.asset} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,17 +57,24 @@ class SettingsContainer extends React.Component {
|
||||
const {
|
||||
mergedSettings,
|
||||
canSave,
|
||||
data,
|
||||
root,
|
||||
asset,
|
||||
errors,
|
||||
updatePending,
|
||||
} = this.props;
|
||||
|
||||
const slotPassthrough = {
|
||||
root,
|
||||
asset,
|
||||
settings: mergedSettings,
|
||||
updatePending,
|
||||
errors,
|
||||
};
|
||||
|
||||
return (
|
||||
<Settings
|
||||
settings={mergedSettings}
|
||||
queryData={{ root, asset, settings: mergedSettings }}
|
||||
slotProps={{ data, updatePending, errors }}
|
||||
slotPassthrough={slotPassthrough}
|
||||
savePending={this.savePending}
|
||||
onToggleModeration={this.toggleModeration}
|
||||
onTogglePremodLinks={this.togglePremodLinks}
|
||||
@@ -82,7 +89,6 @@ class SettingsContainer extends React.Component {
|
||||
}
|
||||
|
||||
SettingsContainer.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
asset: PropTypes.object.isRequired,
|
||||
pending: PropTypes.object.isRequired,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
.root {
|
||||
text-align: center;
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 64px;
|
||||
color: #3a4a52;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-top: 0px;
|
||||
font-size: 14px;
|
||||
color: #7d8285;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import styles from './BlankCommentHistory.css';
|
||||
import { Icon } from 'coral-ui';
|
||||
import cn from 'classnames';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export default () => (
|
||||
<section className={cn(styles.root, 'talk-my-profile-comment-history-blank')}>
|
||||
<Icon name="chat" className={styles.icon} />
|
||||
<h1 className={styles.title}>{t('comment_history_blank.title')}</h1>
|
||||
<p className={styles.info}>{t('comment_history_blank.info')}</p>
|
||||
</section>
|
||||
);
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
.main {
|
||||
min-width: 70%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
|
||||
@@ -11,10 +11,20 @@ import { getTotalReactionsCount } from 'coral-framework/utils';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class Comment extends React.Component {
|
||||
goToStory = () => {
|
||||
this.props.navigate(this.props.comment.asset.url);
|
||||
};
|
||||
|
||||
goToConversation = () => {
|
||||
this.props.navigate(
|
||||
`${this.props.comment.asset.url}?commentId=${this.props.comment.id}`
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { comment, link, data, root } = this.props;
|
||||
const { comment, root } = this.props;
|
||||
const reactionCount = getTotalReactionsCount(comment.action_summaries);
|
||||
const queryData = { root, comment, asset: comment.asset };
|
||||
const slotPassthrough = { root, comment, asset: comment.asset };
|
||||
|
||||
return (
|
||||
<div className={styles.myComment}>
|
||||
@@ -23,8 +33,7 @@ class Comment extends React.Component {
|
||||
fill="commentContent"
|
||||
defaultComponent={CommentContent}
|
||||
className={cn(styles.commentBody, 'my-comment-body')}
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
<div className={cn(styles.commentSummary, 'comment-summary')}>
|
||||
<span
|
||||
@@ -67,7 +76,7 @@ class Comment extends React.Component {
|
||||
<a
|
||||
className={cn(styles.assetURL, 'my-comment-anchor')}
|
||||
href="#"
|
||||
onClick={link(`${comment.asset.url}`)}
|
||||
onClick={this.goToStory}
|
||||
>
|
||||
{t('common.story')}:{' '}
|
||||
{comment.asset.title ? comment.asset.title : comment.asset.url}
|
||||
@@ -77,10 +86,7 @@ class Comment extends React.Component {
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
onClick={link(`${comment.asset.url}?commentId=${comment.id}`)}
|
||||
className={styles.viewLink}
|
||||
>
|
||||
<a onClick={this.goToConversation} className={styles.viewLink}>
|
||||
<Icon name="open_in_new" className={styles.iconView} />
|
||||
{t('view_conversation')}
|
||||
</a>
|
||||
@@ -91,9 +97,10 @@ class Comment extends React.Component {
|
||||
fill="historyCommentTimestamp"
|
||||
defaultComponent={CommentTimestamp}
|
||||
className={'talk-history-comment-published-date'}
|
||||
created_at={comment.created_at}
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
passthrough={{
|
||||
created_at: comment.created_at,
|
||||
...slotPassthrough,
|
||||
}}
|
||||
inline
|
||||
/>
|
||||
</li>
|
||||
@@ -105,10 +112,9 @@ class Comment extends React.Component {
|
||||
}
|
||||
|
||||
Comment.propTypes = {
|
||||
comment: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
body: PropTypes.string,
|
||||
}).isRequired,
|
||||
comment: PropTypes.object.isRequired,
|
||||
navigate: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default Comment;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Comment from './Comment';
|
||||
import Comment from '../containers/Comment';
|
||||
import LoadMore from './LoadMore';
|
||||
import BlankCommentHistory from './BlankCommentHistory';
|
||||
|
||||
class CommentHistory extends React.Component {
|
||||
state = {
|
||||
@@ -21,18 +22,20 @@ class CommentHistory extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { link, comments, data, root } = this.props;
|
||||
const { navigate, comments, root } = this.props;
|
||||
if (!comments.nodes.length) {
|
||||
return <BlankCommentHistory />;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div className="talk-my-profile-comment-history">
|
||||
<div className="commentHistory__list">
|
||||
{comments.nodes.map((comment, i) => {
|
||||
return (
|
||||
<Comment
|
||||
key={i}
|
||||
data={data}
|
||||
root={root}
|
||||
comment={comment}
|
||||
link={link}
|
||||
navigate={navigate}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -51,8 +54,7 @@ class CommentHistory extends React.Component {
|
||||
CommentHistory.propTypes = {
|
||||
comments: PropTypes.object.isRequired,
|
||||
loadMore: PropTypes.func,
|
||||
link: PropTypes.func,
|
||||
data: PropTypes.object,
|
||||
navigate: PropTypes.func,
|
||||
root: PropTypes.object,
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
.userInfo {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.email {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.username {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import styles from './Profile.css';
|
||||
import TabPanel from '../containers/TabPanel';
|
||||
|
||||
const Profile = ({ username, emailAddress, root, slotPassthrough }) => {
|
||||
return (
|
||||
<div className="talk-my-profile talk-profile-container">
|
||||
<div className={styles.userInfo}>
|
||||
<h2 className={styles.username}>{username}</h2>
|
||||
{emailAddress ? <p className={styles.email}>{emailAddress}</p> : null}
|
||||
</div>
|
||||
<Slot fill="profileSections" passthrough={slotPassthrough} />
|
||||
<TabPanel root={root} slotPassthrough={slotPassthrough} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Profile.propTypes = {
|
||||
username: PropTypes.string,
|
||||
emailAddress: PropTypes.string,
|
||||
root: PropTypes.object,
|
||||
slotPassthrough: PropTypes.object,
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Slot } from 'coral-framework/components';
|
||||
|
||||
class Settings extends React.Component {
|
||||
render() {
|
||||
const { root } = this.props;
|
||||
const slotPassthrough = { root };
|
||||
return (
|
||||
<div>
|
||||
<Slot fill="profileSettings" passthrough={slotPassthrough} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Settings.propTypes = {
|
||||
root: PropTypes.object,
|
||||
};
|
||||
|
||||
export default Settings;
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import CommentHistory from '../containers/CommentHistory';
|
||||
import ExtendableTabPanel from '../../../containers/ExtendableTabPanel';
|
||||
import { Tab, TabPane } from 'coral-ui';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import Settings from '../containers/Settings';
|
||||
|
||||
const TabPanel = ({
|
||||
root,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
showSettingsTab,
|
||||
slotPassthrough,
|
||||
}) => {
|
||||
const tabs = [
|
||||
<Tab key="comments" tabId="comments">
|
||||
{t('framework.my_comments')}
|
||||
</Tab>,
|
||||
];
|
||||
|
||||
if (showSettingsTab) {
|
||||
tabs.push(
|
||||
<Tab key="settings" tabId="settings">
|
||||
{t('profile_settings')}
|
||||
</Tab>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ExtendableTabPanel
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
fallbackTab="comments"
|
||||
tabSlot="profileTabs"
|
||||
tabSlotPrepend="profileTabsPrepend"
|
||||
tabPaneSlot="profileTabPanes"
|
||||
slotPassthrough={slotPassthrough}
|
||||
tabs={tabs}
|
||||
tabPanes={[
|
||||
<TabPane key="comments" tabId="comments">
|
||||
<CommentHistory root={root} />
|
||||
</TabPane>,
|
||||
<TabPane key="settings" tabId="settings">
|
||||
<Settings root={root} />
|
||||
</TabPane>,
|
||||
]}
|
||||
sub
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
TabPanel.propTypes = {
|
||||
root: PropTypes.object,
|
||||
slotPassthrough: PropTypes.object,
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
setActiveTab: PropTypes.func.isRequired,
|
||||
showSettingsTab: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default TabPanel;
|
||||
@@ -0,0 +1,32 @@
|
||||
import { gql, compose } from 'react-apollo';
|
||||
import Comment from '../components/Comment';
|
||||
import { withFragments } from 'coral-framework/hocs';
|
||||
import { getSlotFragmentSpreads } from 'coral-framework/utils';
|
||||
|
||||
const slots = ['commentContent', 'historyCommentTimestamp'];
|
||||
|
||||
const withCommentFragments = withFragments({
|
||||
comment: gql`
|
||||
fragment TalkEmbedStream_ProfileComment_comment on Comment {
|
||||
id
|
||||
body
|
||||
replyCount
|
||||
action_summaries {
|
||||
count
|
||||
__typename
|
||||
}
|
||||
asset {
|
||||
id
|
||||
title
|
||||
url
|
||||
${getSlotFragmentSpreads(slots, 'asset')}
|
||||
}
|
||||
created_at
|
||||
${getSlotFragmentSpreads(slots, 'comment')}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const enhance = compose(withCommentFragments);
|
||||
|
||||
export default enhance(Comment);
|
||||
@@ -0,0 +1,103 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import CommentHistory from '../components/CommentHistory';
|
||||
import Comment from './Comment';
|
||||
import { withFragments, withFetchMore } from 'coral-framework/hocs';
|
||||
|
||||
import { appendNewNodes } from 'plugin-api/beta/client/utils';
|
||||
import update from 'immutability-helper';
|
||||
import { getDefinitionName } from 'coral-framework/utils';
|
||||
|
||||
class CommentHistoryContainer extends Component {
|
||||
navigate = url => {
|
||||
this.context.pym.sendMessage('navigate', url);
|
||||
};
|
||||
|
||||
loadMore = () => {
|
||||
return this.props.fetchMore({
|
||||
query: LOAD_MORE_QUERY,
|
||||
variables: {
|
||||
limit: 5,
|
||||
cursor: this.props.root.me.comments.endCursor,
|
||||
},
|
||||
updateQuery: (previous, { fetchMoreResult: { me: { comments } } }) => {
|
||||
const updated = update(previous, {
|
||||
me: {
|
||||
comments: {
|
||||
nodes: {
|
||||
$apply: nodes => appendNewNodes(nodes, comments.nodes),
|
||||
},
|
||||
hasNextPage: { $set: comments.hasNextPage },
|
||||
endCursor: { $set: comments.endCursor },
|
||||
},
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CommentHistory
|
||||
comments={this.props.root.me.comments}
|
||||
root={this.props.root}
|
||||
loadMore={this.loadMore}
|
||||
navigate={this.navigate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CommentHistoryContainer.contextTypes = {
|
||||
pym: PropTypes.object,
|
||||
};
|
||||
|
||||
CommentHistoryContainer.propTypes = {
|
||||
root: PropTypes.object,
|
||||
fetchMore: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query TalkEmbedStream_CommentHistory_LoadMoreComments($limit: Int, $cursor: Cursor) {
|
||||
me {
|
||||
comments(query: { limit: $limit, cursor: $cursor }) {
|
||||
nodes {
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
}
|
||||
endCursor
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
${Comment.fragments.comment}
|
||||
`;
|
||||
|
||||
const withCommentHistoryFragments = withFragments({
|
||||
root: gql`
|
||||
fragment TalkEmbedStream_CommentHistory on RootQuery {
|
||||
me {
|
||||
comments(query: {limit: 10}) {
|
||||
nodes {
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
}
|
||||
endCursor
|
||||
hasNextPage
|
||||
}
|
||||
}
|
||||
}
|
||||
${Comment.fragments.comment}
|
||||
`,
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
currentUser: state.auth.user,
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, null),
|
||||
withCommentHistoryFragments,
|
||||
withFetchMore
|
||||
)(CommentHistoryContainer);
|
||||
@@ -0,0 +1,94 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { withQuery } from 'coral-framework/hocs';
|
||||
import NotLoggedIn from '../components/NotLoggedIn';
|
||||
import { Spinner } from 'coral-ui';
|
||||
import Profile from '../components/Profile';
|
||||
import TabPanel from './TabPanel';
|
||||
import { getDefinitionName } from 'coral-framework/utils';
|
||||
|
||||
import { showSignInDialog } from 'coral-embed-stream/src/actions/login';
|
||||
import { getSlotFragmentSpreads } from 'coral-framework/utils';
|
||||
|
||||
class ProfileContainer extends Component {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.props.currentUser && nextProps.currentUser) {
|
||||
// Refetch after login.
|
||||
this.props.data.refetch();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { currentUser, showSignInDialog, root } = this.props;
|
||||
const { me } = this.props.root;
|
||||
const loading = this.props.data.loading;
|
||||
|
||||
if (this.props.data.error) {
|
||||
return <div>{this.props.data.error.message}</div>;
|
||||
}
|
||||
|
||||
if (!currentUser) {
|
||||
return <NotLoggedIn showSignInDialog={showSignInDialog} />;
|
||||
}
|
||||
|
||||
if (loading || !me) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const localProfile = currentUser.profiles.find(p => p.provider === 'local');
|
||||
const emailAddress = localProfile && localProfile.id;
|
||||
const slotPassthrough = { root };
|
||||
|
||||
return (
|
||||
<Profile
|
||||
username={me.username}
|
||||
emailAddress={emailAddress}
|
||||
root={root}
|
||||
slotPassthrough={slotPassthrough}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ProfileContainer.propTypes = {
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
currentUser: PropTypes.object,
|
||||
showSignInDialog: PropTypes.func,
|
||||
};
|
||||
|
||||
const slots = ['profileSections'];
|
||||
|
||||
const withProfileQuery = withQuery(
|
||||
gql`
|
||||
query CoralEmbedStream_Profile {
|
||||
me {
|
||||
id
|
||||
username
|
||||
}
|
||||
...${getDefinitionName(TabPanel.fragments.root)}
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
}
|
||||
${TabPanel.fragments.root}
|
||||
`,
|
||||
{
|
||||
options: {
|
||||
fetchPolicy: 'network-only',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
currentUser: state.auth.user,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators({ showSignInDialog }, dispatch);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withProfileQuery
|
||||
)(ProfileContainer);
|
||||
@@ -1,178 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import React, { Component } from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { withQuery } from 'coral-framework/hocs';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import cn from 'classnames';
|
||||
import { link } from 'coral-framework/services/pym';
|
||||
import NotLoggedIn from '../components/NotLoggedIn';
|
||||
import { Spinner } from 'coral-ui';
|
||||
import CommentHistory from '../components/CommentHistory';
|
||||
|
||||
import { showSignInDialog } from 'coral-embed-stream/src/actions/login';
|
||||
import { appendNewNodes } from 'plugin-api/beta/client/utils';
|
||||
import update from 'immutability-helper';
|
||||
import { getSlotFragmentSpreads } from 'coral-framework/utils';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class ProfileContainer extends Component {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.props.currentUser && nextProps.currentUser) {
|
||||
// Refetch after login.
|
||||
this.props.data.refetch();
|
||||
}
|
||||
}
|
||||
|
||||
loadMore = () => {
|
||||
return this.props.data.fetchMore({
|
||||
query: LOAD_MORE_QUERY,
|
||||
variables: {
|
||||
limit: 5,
|
||||
cursor: this.props.root.me.comments.endCursor,
|
||||
},
|
||||
updateQuery: (previous, { fetchMoreResult: { me: { comments } } }) => {
|
||||
const updated = update(previous, {
|
||||
me: {
|
||||
comments: {
|
||||
nodes: {
|
||||
$apply: nodes => appendNewNodes(nodes, comments.nodes),
|
||||
},
|
||||
hasNextPage: { $set: comments.hasNextPage },
|
||||
endCursor: { $set: comments.endCursor },
|
||||
},
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { currentUser, showSignInDialog, root, data } = this.props;
|
||||
const { me } = this.props.root;
|
||||
const loading = this.props.data.loading;
|
||||
|
||||
if (this.props.data.error) {
|
||||
return <div>{this.props.data.error.message}</div>;
|
||||
}
|
||||
|
||||
if (!currentUser) {
|
||||
return <NotLoggedIn showSignInDialog={showSignInDialog} />;
|
||||
}
|
||||
|
||||
if (loading || !me) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const localProfile = currentUser.profiles.find(p => p.provider === 'local');
|
||||
const emailAddress = localProfile && localProfile.id;
|
||||
|
||||
return (
|
||||
<div className="talk-my-profile talk-embed-stream-profile-container">
|
||||
<h2>{me.username}</h2>
|
||||
{emailAddress ? <p>{emailAddress}</p> : null}
|
||||
<Slot fill="profileSections" data={data} queryData={{ root }} />
|
||||
<hr />
|
||||
<h3>{t('framework.my_comments')}</h3>
|
||||
<div
|
||||
className={cn('talk-my-profile-comment-history', {
|
||||
'talk-my-profile-comment-history-no-comments': !me.comments.nodes
|
||||
.length,
|
||||
})}
|
||||
>
|
||||
{me.comments.nodes.length ? (
|
||||
<CommentHistory
|
||||
data={data}
|
||||
root={root}
|
||||
comments={me.comments}
|
||||
link={link}
|
||||
loadMore={this.loadMore}
|
||||
/>
|
||||
) : (
|
||||
<p className="talk-my-profile-comment-history-no-comments-cta">
|
||||
{t('user_no_comment')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const slots = [
|
||||
'profileSections',
|
||||
|
||||
// TODO: These Slots should be included in `talk-plugin-history` instead.
|
||||
'commentContent',
|
||||
'historyCommentTimestamp',
|
||||
];
|
||||
|
||||
const CommentFragment = gql`
|
||||
fragment TalkSettings_CommentConnectionFragment on CommentConnection {
|
||||
nodes {
|
||||
id
|
||||
body
|
||||
replyCount
|
||||
action_summaries {
|
||||
count
|
||||
__typename
|
||||
}
|
||||
asset {
|
||||
id
|
||||
title
|
||||
url
|
||||
${getSlotFragmentSpreads(slots, 'asset')}
|
||||
}
|
||||
created_at
|
||||
${getSlotFragmentSpreads(slots, 'comment')}
|
||||
}
|
||||
endCursor
|
||||
hasNextPage
|
||||
}
|
||||
`;
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query TalkSettings_LoadMoreComments($limit: Int, $cursor: Cursor) {
|
||||
me {
|
||||
comments(query: { limit: $limit, cursor: $cursor }) {
|
||||
...TalkSettings_CommentConnectionFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${CommentFragment}
|
||||
`;
|
||||
|
||||
const withProfileQuery = withQuery(
|
||||
gql`
|
||||
query CoralEmbedStream_Profile {
|
||||
me {
|
||||
id
|
||||
username
|
||||
comments(query: {limit: 10}) {
|
||||
...TalkSettings_CommentConnectionFragment
|
||||
}
|
||||
}
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
}
|
||||
${CommentFragment}
|
||||
`,
|
||||
{
|
||||
options: {
|
||||
fetchPolicy: 'network-only',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
currentUser: state.auth.user,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators({ showSignInDialog }, dispatch);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withProfileQuery
|
||||
)(ProfileContainer);
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import Settings from '../components/Settings';
|
||||
import { withFragments } from 'coral-framework/hocs';
|
||||
import { getSlotFragmentSpreads } from 'coral-framework/utils';
|
||||
|
||||
const slots = ['profileSettings'];
|
||||
|
||||
class SettingsContainer extends React.Component {
|
||||
render() {
|
||||
return <Settings {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
const enhance = compose(
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment TalkEmbedStream_ProfileSettings_root on RootQuery {
|
||||
__typename
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
}
|
||||
`,
|
||||
})
|
||||
);
|
||||
|
||||
export default enhance(SettingsContainer);
|
||||
@@ -0,0 +1,64 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { withSlotElements, withFragments } from 'coral-framework/hocs';
|
||||
import Settings from './Settings';
|
||||
import CommentHistory from './CommentHistory';
|
||||
import { getDefinitionName } from 'coral-framework/utils';
|
||||
import TabPanel from '../components/TabPanel';
|
||||
import { setActiveTab } from '../../../actions/profile';
|
||||
import { getSlotFragmentSpreads } from 'coral-framework/utils';
|
||||
|
||||
class TabPanelContainer extends Component {
|
||||
render() {
|
||||
return (
|
||||
<TabPanel
|
||||
root={this.props.root}
|
||||
slotPassthrough={this.props.slotPassthrough}
|
||||
activeTab={this.props.activeTab}
|
||||
setActiveTab={this.props.setActiveTab}
|
||||
showSettingsTab={this.props.profileSettingsSlotElements.length > 0}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TabPanelContainer.propTypes = {
|
||||
root: PropTypes.object,
|
||||
slotPassthrough: PropTypes.object,
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
setActiveTab: PropTypes.func.isRequired,
|
||||
profileSettingsSlotElements: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
const slots = ['profileTabs', 'profileTabsPrepend', 'profileTabPanes'];
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
activeTab: state.profile.activeTab,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators({ setActiveTab }, dispatch);
|
||||
|
||||
export default compose(
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment TalkEmbedStream_ProfileTabPanel_root on RootQuery {
|
||||
__typename
|
||||
...${getDefinitionName(CommentHistory.fragments.root)}
|
||||
...${getDefinitionName(Settings.fragments.root)}
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
}
|
||||
${CommentHistory.fragments.root}
|
||||
${Settings.fragments.root}
|
||||
`,
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withSlotElements({
|
||||
slot: 'profileSettings',
|
||||
propName: 'profileSettingsSlotElements',
|
||||
passthroughPropName: 'slotPassthrough',
|
||||
})
|
||||
)(TabPanelContainer);
|
||||
@@ -126,7 +126,6 @@ class AllCommentsPane extends React.Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
data,
|
||||
root,
|
||||
comments,
|
||||
commentClassNames,
|
||||
@@ -164,7 +163,6 @@ class AllCommentsPane extends React.Component {
|
||||
return (
|
||||
<Comment
|
||||
commentClassNames={commentClassNames}
|
||||
data={data}
|
||||
root={root}
|
||||
disableReply={disableReply}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
@@ -205,7 +203,6 @@ class AllCommentsPane extends React.Component {
|
||||
}
|
||||
|
||||
AllCommentsPane.propTypes = {
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
comments: PropTypes.object,
|
||||
commentClassNames: PropTypes.array,
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
|
||||
.commentLevel0 {
|
||||
padding-left: 0px;
|
||||
|
||||
&.highlightedComment {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.commentLevel1 {
|
||||
@@ -41,8 +45,8 @@
|
||||
}
|
||||
|
||||
.highlightedComment {
|
||||
padding-left: 15px;
|
||||
border-left: 3px solid rgb(35,118,216);
|
||||
padding: 1px 15px 8px 15px;
|
||||
background-color: #E3F2FD;
|
||||
}
|
||||
|
||||
.bylineSecondary {
|
||||
|
||||
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
||||
import TagLabel from './TagLabel';
|
||||
import CommentTimestamp from 'coral-framework/components/CommentTimestamp';
|
||||
import ReplyButton from './ReplyButton';
|
||||
import ReplyBox from './ReplyBox';
|
||||
import ReplyBox from '../containers/ReplyBox';
|
||||
import FlagComment from './FlagComment';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import { TransitionGroup } from 'react-transition-group';
|
||||
@@ -13,15 +13,16 @@ import styles from './Comment.css';
|
||||
import { THREADING_LEVEL } from '../../../constants/stream';
|
||||
import merge from 'lodash/merge';
|
||||
import mapValues from 'lodash/mapValues';
|
||||
import get from 'lodash/get';
|
||||
|
||||
import LoadMore from './LoadMore';
|
||||
import { getEditableUntilDate } from './util';
|
||||
import { getEditableUntilDate } from '../util';
|
||||
import { findCommentWithId } from '../../../graphql/utils';
|
||||
import CommentContent from 'coral-framework/components/CommentContent';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import CommentTombstone from './CommentTombstone';
|
||||
import InactiveCommentLabel from './InactiveCommentLabel';
|
||||
import { EditableCommentContent } from './EditableCommentContent';
|
||||
import EditableCommentContent from '../containers/EditableCommentContent';
|
||||
import {
|
||||
getActionSummary,
|
||||
iPerformedThisAction,
|
||||
@@ -169,7 +170,7 @@ export default class Comment extends React.Component {
|
||||
postFlag: PropTypes.func.isRequired,
|
||||
deleteAction: PropTypes.func.isRequired,
|
||||
parentId: PropTypes.string,
|
||||
highlighted: PropTypes.string,
|
||||
highlighted: PropTypes.object,
|
||||
notify: PropTypes.func.isRequired,
|
||||
postComment: PropTypes.func.isRequired,
|
||||
depth: PropTypes.number.isRequired,
|
||||
@@ -180,34 +181,12 @@ export default class Comment extends React.Component {
|
||||
}),
|
||||
charCountEnable: PropTypes.bool.isRequired,
|
||||
maxCharCount: PropTypes.number,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
loadMore: PropTypes.func,
|
||||
postDontAgree: PropTypes.func,
|
||||
animateEnter: PropTypes.bool,
|
||||
commentClassNames: PropTypes.array,
|
||||
comment: PropTypes.shape({
|
||||
depth: PropTypes.number,
|
||||
action_summaries: PropTypes.array.isRequired,
|
||||
body: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
tags: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
})
|
||||
),
|
||||
replies: PropTypes.object,
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
editing: PropTypes.shape({
|
||||
edited: PropTypes.bool,
|
||||
|
||||
// ISO8601
|
||||
editableUntil: PropTypes.string,
|
||||
}),
|
||||
}).isRequired,
|
||||
comment: PropTypes.object.isRequired,
|
||||
setCommentStatus: PropTypes.func.isRequired,
|
||||
|
||||
// edit a comment, passed (id, asset_id, { body })
|
||||
@@ -301,6 +280,14 @@ export default class Comment extends React.Component {
|
||||
this.props.setActiveReplyBox('');
|
||||
};
|
||||
|
||||
undoStatus = () =>
|
||||
this.props.setCommentStatus({
|
||||
commentId: this.props.comment.id,
|
||||
status: this.props.comment.status_history[
|
||||
this.props.comment.status_history.length - 2
|
||||
].type,
|
||||
});
|
||||
|
||||
// getVisibileReplies returns a list containing comments
|
||||
// which were authored by current user or comes before the `idCursor`.
|
||||
getVisibileReplies() {
|
||||
@@ -329,6 +316,27 @@ export default class Comment extends React.Component {
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* getConditionalClassNames
|
||||
* conditionalClassNames adds classNames based on condition
|
||||
* classnames is an array of objects with key as classnames and value as conditions
|
||||
* i.e:
|
||||
* {
|
||||
* 'myClassName': { tags: [STAFF]}
|
||||
* }
|
||||
*
|
||||
* This will add myClassName to comments tagged with STAFF TAG.
|
||||
**/
|
||||
getConditionalClassNames() {
|
||||
const { commentClassNames = [] } = this.props;
|
||||
return mapValues(merge({}, ...commentClassNames), condition => {
|
||||
if (condition.tags) {
|
||||
return condition.tags.some(tag => hasTag(this.props.comment.tags, tag));
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
if (this.editWindowExpiryTimeout) {
|
||||
@@ -343,25 +351,54 @@ export default class Comment extends React.Component {
|
||||
}, Math.max(msLeftToEdit, 0));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.editWindowExpiryTimeout) {
|
||||
this.editWindowExpiryTimeout = clearTimeout(this.editWindowExpiryTimeout);
|
||||
}
|
||||
this._isMounted = false;
|
||||
}
|
||||
render() {
|
||||
|
||||
renderReplyBox() {
|
||||
const {
|
||||
asset,
|
||||
data,
|
||||
depth,
|
||||
comment,
|
||||
parentId,
|
||||
postComment,
|
||||
currentUser,
|
||||
setActiveReplyBox,
|
||||
maxCharCount,
|
||||
notify,
|
||||
charCountEnable,
|
||||
root,
|
||||
} = this.props;
|
||||
return (
|
||||
<ReplyBox
|
||||
root={root}
|
||||
comment={comment}
|
||||
commentPostedHandler={this.commentPostedHandler}
|
||||
charCountEnable={charCountEnable}
|
||||
maxCharCount={maxCharCount}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
parentId={depth < THREADING_LEVEL ? comment.id : parentId}
|
||||
notify={notify}
|
||||
postComment={postComment}
|
||||
currentUser={currentUser}
|
||||
assetId={asset.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderReplies(view) {
|
||||
const {
|
||||
asset,
|
||||
depth,
|
||||
comment,
|
||||
postFlag,
|
||||
parentId,
|
||||
highlighted,
|
||||
postComment,
|
||||
currentUser,
|
||||
postDontAgree,
|
||||
setActiveReplyBox,
|
||||
activeReplyBox,
|
||||
loadMore,
|
||||
@@ -372,39 +409,46 @@ export default class Comment extends React.Component {
|
||||
charCountEnable,
|
||||
showSignInDialog,
|
||||
liveUpdates,
|
||||
animateEnter,
|
||||
emit,
|
||||
commentClassNames = [],
|
||||
} = this.props;
|
||||
return (
|
||||
<TransitionGroup key="transitionGroup">
|
||||
{view.map(reply => {
|
||||
return (
|
||||
<CommentContainer
|
||||
root={this.props.root}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
disableReply={disableReply}
|
||||
activeReplyBox={activeReplyBox}
|
||||
notify={notify}
|
||||
parentId={comment.id}
|
||||
postComment={postComment}
|
||||
editComment={this.props.editComment}
|
||||
depth={depth + 1}
|
||||
asset={asset}
|
||||
highlighted={highlighted}
|
||||
currentUser={currentUser}
|
||||
postFlag={postFlag}
|
||||
deleteAction={deleteAction}
|
||||
loadMore={loadMore}
|
||||
charCountEnable={charCountEnable}
|
||||
maxCharCount={maxCharCount}
|
||||
showSignInDialog={showSignInDialog}
|
||||
liveUpdates={liveUpdates}
|
||||
reactKey={reply.id}
|
||||
key={reply.id}
|
||||
comment={reply}
|
||||
emit={emit}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TransitionGroup>
|
||||
);
|
||||
}
|
||||
|
||||
if (!highlighted && this.commentIsRejected(comment)) {
|
||||
return (
|
||||
<CommentTombstone
|
||||
action="reject"
|
||||
onUndo={() => {
|
||||
this.props.setCommentStatus({
|
||||
commentId: comment.id,
|
||||
status:
|
||||
comment.status_history[comment.status_history.length - 2].type,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.commentIsIgnored(comment)) {
|
||||
return <CommentTombstone action="ignore" />;
|
||||
}
|
||||
|
||||
const view = this.getVisibileReplies();
|
||||
|
||||
// Inactive comments can be viewed by moderators and admins (e.g. using permalinks).
|
||||
const isActive = isCommentActive(comment.status);
|
||||
|
||||
renderLoadMoreReplies(view) {
|
||||
const { comment } = this.props;
|
||||
const { loadingState } = this.state;
|
||||
const isPending = comment.id.indexOf('pending') >= 0;
|
||||
const isHighlighted = highlighted === comment.id;
|
||||
|
||||
const hasMoreComments =
|
||||
comment.replies &&
|
||||
(comment.replies.hasNextPage ||
|
||||
@@ -412,6 +456,56 @@ export default class Comment extends React.Component {
|
||||
const moreRepliesCount = this.hasIgnoredReplies()
|
||||
? -1
|
||||
: comment.replyCount - view.length;
|
||||
return (
|
||||
<div className="talk-load-more-replies" key="loadMoreReplies">
|
||||
<LoadMore
|
||||
topLevel={false}
|
||||
replyCount={moreRepliesCount}
|
||||
moreComments={hasMoreComments}
|
||||
loadMore={this.loadNewReplies}
|
||||
loadingState={loadingState}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderRepliesContainer() {
|
||||
const { highlighted, comment } = this.props;
|
||||
|
||||
// Only render highlighted reply when we are the parent of it.
|
||||
if (get(highlighted, 'parent.id') === comment.id) {
|
||||
return this.renderReplies([highlighted]);
|
||||
}
|
||||
|
||||
// Otherwise render replies in current view and a load more button if needed.
|
||||
const view = this.getVisibileReplies();
|
||||
return [this.renderReplies(view), this.renderLoadMoreReplies(view)];
|
||||
}
|
||||
|
||||
renderComment() {
|
||||
const {
|
||||
asset,
|
||||
root,
|
||||
depth,
|
||||
comment,
|
||||
postFlag,
|
||||
parentId,
|
||||
highlighted,
|
||||
currentUser,
|
||||
postDontAgree,
|
||||
deleteAction,
|
||||
disableReply,
|
||||
maxCharCount,
|
||||
notify,
|
||||
charCountEnable,
|
||||
showSignInDialog,
|
||||
} = this.props;
|
||||
|
||||
// Inactive comments can be viewed by moderators and admins (e.g. using permalinks).
|
||||
const isActive = isCommentActive(comment.status);
|
||||
|
||||
const isPending = comment.id.indexOf('pending') >= 0;
|
||||
const isHighlighted = highlighted && highlighted.id === comment.id;
|
||||
const flagSummary = getActionSummary('FlagActionSummary', comment);
|
||||
const dontAgreeSummary = getActionSummary(
|
||||
'DontAgreeActionSummary',
|
||||
@@ -424,38 +518,6 @@ export default class Comment extends React.Component {
|
||||
myFlag = dontAgreeSummary.find(s => s.current_user);
|
||||
}
|
||||
|
||||
/**
|
||||
* conditionClassNames
|
||||
* adds classNames based on condition
|
||||
* classnames is an array of objects with key as classnames and value as conditions
|
||||
* i.e:
|
||||
* {
|
||||
* 'myClassName': { tags: [STAFF]}
|
||||
* }
|
||||
*
|
||||
* This will add myClassName to comments tagged with STAFF TAG.
|
||||
* **/
|
||||
const conditionalClassNames = mapValues(
|
||||
merge({}, ...commentClassNames),
|
||||
condition => {
|
||||
if (condition.tags) {
|
||||
return condition.tags.some(tag => hasTag(comment.tags, tag));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
const rootClassName = cn(
|
||||
'talk-stream-comment-wrapper',
|
||||
`talk-stream-comment-wrapper-level-${depth}`,
|
||||
styles.root,
|
||||
styles[`rootLevel${depth}`],
|
||||
{
|
||||
...conditionalClassNames,
|
||||
[styles.enter]: animateEnter,
|
||||
}
|
||||
);
|
||||
|
||||
const commentClassName = cn(
|
||||
'talk-stream-comment',
|
||||
`talk-stream-comment-level-${depth}`,
|
||||
@@ -470,252 +532,222 @@ export default class Comment extends React.Component {
|
||||
);
|
||||
|
||||
// props that are passed down the slots.
|
||||
const slotProps = {
|
||||
data,
|
||||
const slotPassthrough = {
|
||||
depth,
|
||||
};
|
||||
|
||||
const queryData = {
|
||||
root,
|
||||
asset,
|
||||
comment,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={rootClassName} id={`c_${comment.id}`}>
|
||||
<div className={commentClassName}>
|
||||
<Slot
|
||||
className={`${styles.commentAvatar} talk-stream-comment-avatar`}
|
||||
fill="commentAvatar"
|
||||
{...slotProps}
|
||||
queryData={queryData}
|
||||
inline
|
||||
/>
|
||||
<div className={commentClassName}>
|
||||
<Slot
|
||||
className={cn(styles.commentAvatar, 'talk-stream-comment-avatar')}
|
||||
fill="commentAvatar"
|
||||
passthrough={slotPassthrough}
|
||||
inline
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
styles.commentContainer,
|
||||
'talk-stream-comment-container'
|
||||
)}
|
||||
>
|
||||
<div className={cn(styles.header, 'talk-stream-comment-header')}>
|
||||
<div
|
||||
className={cn(
|
||||
styles.headerContainer,
|
||||
'talk-stream-comment-header-container'
|
||||
)}
|
||||
>
|
||||
<Slot
|
||||
className={cn(styles.username, 'talk-stream-comment-user-name')}
|
||||
fill="commentAuthorName"
|
||||
defaultComponent={CommentAuthorName}
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
styles.commentContainer,
|
||||
'talk-stream-comment-container'
|
||||
)}
|
||||
>
|
||||
<div className={cn(styles.header, 'talk-stream-comment-header')}>
|
||||
<div
|
||||
className={cn(
|
||||
styles.headerContainer,
|
||||
'talk-stream-comment-header-container'
|
||||
styles.tagsContainer,
|
||||
'talk-stream-comment-header-tags-container'
|
||||
)}
|
||||
>
|
||||
{isStaff(comment.tags) ? <TagLabel>Staff</TagLabel> : null}
|
||||
|
||||
<Slot
|
||||
className={cn(
|
||||
styles.commentAuthorTagsSlot,
|
||||
'talk-stream-comment-author-tags'
|
||||
)}
|
||||
fill="commentAuthorTags"
|
||||
passthrough={slotPassthrough}
|
||||
inline
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
styles.bylineSecondary,
|
||||
'talk-stream-comment-user-byline'
|
||||
)}
|
||||
>
|
||||
<Slot
|
||||
className={cn(
|
||||
styles.username,
|
||||
'talk-stream-comment-user-name'
|
||||
)}
|
||||
fill="commentAuthorName"
|
||||
defaultComponent={CommentAuthorName}
|
||||
queryData={queryData}
|
||||
{...slotProps}
|
||||
fill="commentTimestamp"
|
||||
defaultComponent={CommentTimestamp}
|
||||
className={'talk-stream-comment-published-date'}
|
||||
passthrough={{
|
||||
created_at: comment.created_at,
|
||||
...slotPassthrough,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
styles.tagsContainer,
|
||||
'talk-stream-comment-header-tags-container'
|
||||
)}
|
||||
>
|
||||
{isStaff(comment.tags) ? <TagLabel>Staff</TagLabel> : null}
|
||||
|
||||
<Slot
|
||||
className={cn(
|
||||
styles.commentAuthorTagsSlot,
|
||||
'talk-stream-comment-author-tags'
|
||||
)}
|
||||
fill="commentAuthorTags"
|
||||
queryData={queryData}
|
||||
{...slotProps}
|
||||
inline
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className={`${
|
||||
styles.bylineSecondary
|
||||
} talk-stream-comment-user-byline`}
|
||||
>
|
||||
<Slot
|
||||
fill="commentTimestamp"
|
||||
defaultComponent={CommentTimestamp}
|
||||
className={'talk-stream-comment-published-date'}
|
||||
created_at={comment.created_at}
|
||||
queryData={queryData}
|
||||
{...slotProps}
|
||||
/>
|
||||
{comment.editing && comment.editing.edited ? (
|
||||
<span>
|
||||
<span className={styles.editedMarker}>
|
||||
({t('comment.edited')})
|
||||
</span>
|
||||
{comment.editing && comment.editing.edited ? (
|
||||
<span>
|
||||
<span className={styles.editedMarker}>
|
||||
({t('comment.edited')})
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Slot
|
||||
className={styles.commentInfoBar}
|
||||
fill="commentInfoBar"
|
||||
{...slotProps}
|
||||
queryData={queryData}
|
||||
/>
|
||||
|
||||
{isActive &&
|
||||
(currentUser && comment.user.id === currentUser.id) && (
|
||||
/* User can edit/delete their own comment for a short window after posting */
|
||||
<span className={cn(styles.topRight)}>
|
||||
{this.state.isEditable && (
|
||||
<a
|
||||
className={cn(styles.link, {
|
||||
[styles.active]: this.state.isEditing,
|
||||
})}
|
||||
onClick={this.onClickEdit}
|
||||
>
|
||||
Edit
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{!isActive && <InactiveCommentLabel status={comment.status} />}
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
{this.state.isEditing ? (
|
||||
<EditableCommentContent
|
||||
editComment={this.editComment}
|
||||
notify={notify}
|
||||
comment={comment}
|
||||
currentUser={currentUser}
|
||||
charCountEnable={charCountEnable}
|
||||
maxCharCount={maxCharCount}
|
||||
parentId={parentId}
|
||||
stopEditing={this.stopEditing}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
<Slot
|
||||
fill="commentContent"
|
||||
className="talk-stream-comment-content"
|
||||
defaultComponent={CommentContent}
|
||||
{...slotProps}
|
||||
queryData={queryData}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={cn(styles.footer, 'talk-stream-comment-footer')}>
|
||||
{isActive && (
|
||||
<div className={'talk-stream-comment-actions-container'}>
|
||||
<div className="talk-embed-stream-comment-actions-container-left commentActionsLeft comment__action-container">
|
||||
<Slot
|
||||
fill="commentReactions"
|
||||
{...slotProps}
|
||||
queryData={queryData}
|
||||
inline
|
||||
/>
|
||||
|
||||
{!disableReply && (
|
||||
<ActionButton>
|
||||
<ReplyButton
|
||||
onClick={this.showReplyBox}
|
||||
parentCommentId={parentId || comment.id}
|
||||
currentUserId={currentUser && currentUser.id}
|
||||
/>
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
<div className="talk-embed-stream-comment-actions-container-right commentActionsRight comment__action-container">
|
||||
<Slot
|
||||
fill="commentActions"
|
||||
wrapperComponent={ActionButton}
|
||||
{...slotProps}
|
||||
queryData={queryData}
|
||||
inline
|
||||
/>
|
||||
<ActionButton>
|
||||
<FlagComment
|
||||
flaggedByCurrentUser={!!myFlag}
|
||||
flag={myFlag}
|
||||
id={comment.id}
|
||||
author_id={comment.user.id}
|
||||
postFlag={postFlag}
|
||||
notify={notify}
|
||||
postDontAgree={postDontAgree}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
<Slot
|
||||
className={styles.commentInfoBar}
|
||||
fill="commentInfoBar"
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
|
||||
{isActive &&
|
||||
(currentUser && comment.user.id === currentUser.id) && (
|
||||
/* User can edit/delete their own comment for a short window after posting */
|
||||
<span className={cn(styles.topRight)}>
|
||||
{this.state.isEditable && (
|
||||
<a
|
||||
className={cn(styles.link, {
|
||||
[styles.active]: this.state.isEditing,
|
||||
})}
|
||||
onClick={this.onClickEdit}
|
||||
>
|
||||
Edit
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!isActive && <InactiveCommentLabel status={comment.status} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeReplyBox === comment.id ? (
|
||||
<ReplyBox
|
||||
commentPostedHandler={this.commentPostedHandler}
|
||||
charCountEnable={charCountEnable}
|
||||
maxCharCount={maxCharCount}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
parentId={depth < THREADING_LEVEL ? comment.id : parentId}
|
||||
notify={notify}
|
||||
postComment={postComment}
|
||||
currentUser={currentUser}
|
||||
assetId={asset.id}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<TransitionGroup>
|
||||
{view.map(reply => {
|
||||
return (
|
||||
<CommentContainer
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
disableReply={disableReply}
|
||||
activeReplyBox={activeReplyBox}
|
||||
<div className={styles.content}>
|
||||
{this.state.isEditing ? (
|
||||
<EditableCommentContent
|
||||
editComment={this.editComment}
|
||||
notify={notify}
|
||||
parentId={comment.id}
|
||||
postComment={postComment}
|
||||
editComment={this.props.editComment}
|
||||
depth={depth + 1}
|
||||
asset={asset}
|
||||
highlighted={highlighted}
|
||||
root={root}
|
||||
comment={comment}
|
||||
currentUser={currentUser}
|
||||
postFlag={postFlag}
|
||||
deleteAction={deleteAction}
|
||||
loadMore={loadMore}
|
||||
charCountEnable={charCountEnable}
|
||||
maxCharCount={maxCharCount}
|
||||
showSignInDialog={showSignInDialog}
|
||||
liveUpdates={liveUpdates}
|
||||
reactKey={reply.id}
|
||||
key={reply.id}
|
||||
comment={reply}
|
||||
emit={emit}
|
||||
parentId={parentId}
|
||||
stopEditing={this.stopEditing}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TransitionGroup>
|
||||
<div className="talk-load-more-replies">
|
||||
<LoadMore
|
||||
topLevel={false}
|
||||
replyCount={moreRepliesCount}
|
||||
moreComments={hasMoreComments}
|
||||
loadMore={this.loadNewReplies}
|
||||
loadingState={loadingState}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
<Slot
|
||||
fill="commentContent"
|
||||
className="talk-stream-comment-content"
|
||||
defaultComponent={CommentContent}
|
||||
size={1}
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={cn(styles.footer, 'talk-stream-comment-footer')}>
|
||||
{isActive && (
|
||||
<div className={'talk-stream-comment-actions-container'}>
|
||||
<div className="talk-embed-stream-comment-actions-container-left commentActionsLeft comment__action-container">
|
||||
<Slot
|
||||
fill="commentReactions"
|
||||
passthrough={slotPassthrough}
|
||||
inline
|
||||
/>
|
||||
|
||||
{!disableReply && (
|
||||
<ActionButton>
|
||||
<ReplyButton
|
||||
onClick={this.showReplyBox}
|
||||
parentCommentId={parentId || comment.id}
|
||||
currentUserId={currentUser && currentUser.id}
|
||||
/>
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
<div className="talk-embed-stream-comment-actions-container-right commentActionsRight comment__action-container">
|
||||
<Slot
|
||||
fill="commentActions"
|
||||
passthrough={slotPassthrough}
|
||||
inline
|
||||
/>
|
||||
<ActionButton>
|
||||
<FlagComment
|
||||
flaggedByCurrentUser={!!myFlag}
|
||||
flag={myFlag}
|
||||
id={comment.id}
|
||||
author_id={comment.user.id}
|
||||
postFlag={postFlag}
|
||||
notify={notify}
|
||||
postDontAgree={postDontAgree}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
depth,
|
||||
comment,
|
||||
activeReplyBox,
|
||||
highlighted,
|
||||
animateEnter,
|
||||
} = this.props;
|
||||
|
||||
if (!highlighted && this.commentIsRejected(comment)) {
|
||||
return <CommentTombstone action="reject" onUndo={this.undoStatus} />;
|
||||
}
|
||||
|
||||
if (this.commentIsIgnored(comment)) {
|
||||
return <CommentTombstone action="ignore" />;
|
||||
}
|
||||
|
||||
const rootClassName = cn(
|
||||
'talk-stream-comment-wrapper',
|
||||
`talk-stream-comment-wrapper-level-${depth}`,
|
||||
styles.root,
|
||||
styles[`rootLevel${depth}`],
|
||||
{
|
||||
...this.getConditionalClassNames(),
|
||||
[styles.enter]: animateEnter,
|
||||
}
|
||||
);
|
||||
|
||||
const id = `c_${comment.id}`;
|
||||
|
||||
return (
|
||||
<div className={rootClassName} id={id}>
|
||||
{this.renderComment()}
|
||||
{activeReplyBox === comment.id && this.renderReplyBox()}
|
||||
{this.renderRepliesContainer()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// return whether the comment is editable
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
.buttonContainer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.button {
|
||||
float: right;
|
||||
padding: 5px 10px;
|
||||
background: rgb(105, 105, 105);
|
||||
color: #FFF;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Example loading state animations on the button.
|
||||
*/
|
||||
|
||||
@@ -13,19 +13,13 @@ import DraftArea from '../containers/DraftArea';
|
||||
/**
|
||||
* Common UI for Creating or Editing a Comment
|
||||
*/
|
||||
export class CommentForm extends React.Component {
|
||||
class CommentForm extends React.Component {
|
||||
static propTypes = {
|
||||
charCountEnable: PropTypes.bool.isRequired,
|
||||
maxCharCount: PropTypes.number,
|
||||
|
||||
// DOM ID for form input that edits comment body
|
||||
bodyInputId: PropTypes.string,
|
||||
|
||||
// screen reader label for input that edits comment body
|
||||
bodyLabel: PropTypes.string,
|
||||
|
||||
// Placeholder for input that edits comment body
|
||||
bodyPlaceholder: PropTypes.string,
|
||||
// Unique identifier for this form
|
||||
id: PropTypes.string,
|
||||
|
||||
// render at start of button container (useful for extra buttons)
|
||||
buttonContainerStart: PropTypes.node,
|
||||
@@ -37,24 +31,28 @@ export class CommentForm extends React.Component {
|
||||
submitButtonCStyle: PropTypes.string,
|
||||
|
||||
// return whether the submit button should be enabled for the provided
|
||||
// comment ({ body }) (for reasons other than charCount)
|
||||
// input (for reasons other than charCount)
|
||||
submitEnabled: PropTypes.func,
|
||||
|
||||
// className to add to buttons
|
||||
submitButtonClassName: PropTypes.string,
|
||||
cancelButtonClassName: PropTypes.string,
|
||||
|
||||
body: PropTypes.string.isRequired,
|
||||
onBodyChange: PropTypes.func.isRequired,
|
||||
input: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func,
|
||||
state: PropTypes.string,
|
||||
loadingState: PropTypes.oneOf(['', 'loading', 'success', 'error']),
|
||||
registerHook: PropTypes.func,
|
||||
unregisterHook: PropTypes.func,
|
||||
isReply: PropTypes.bool,
|
||||
isEdit: PropTypes.bool,
|
||||
root: PropTypes.object.isRequired,
|
||||
comment: PropTypes.object,
|
||||
};
|
||||
static get defaultProps() {
|
||||
return {
|
||||
bodyLabel: t('comment_box.comment'),
|
||||
bodyPlaceholder: t('comment_box.comment'),
|
||||
submitText: t('comment_box.post'),
|
||||
submitButtonCStyle: 'darkGrey',
|
||||
submitEnabled: () => true,
|
||||
@@ -85,18 +83,20 @@ export class CommentForm extends React.Component {
|
||||
cancelButtonClassName,
|
||||
submitButtonClassName,
|
||||
charCountEnable,
|
||||
body,
|
||||
input,
|
||||
loadingState,
|
||||
comment,
|
||||
root,
|
||||
} = this.props;
|
||||
|
||||
const length = body.length;
|
||||
const length = input.body.length;
|
||||
const isRespectingMaxCount = length =>
|
||||
charCountEnable && maxCharCount && length > maxCharCount;
|
||||
const disableSubmitButton =
|
||||
!length ||
|
||||
body.trim().length === 0 ||
|
||||
input.body.trim().length === 0 ||
|
||||
isRespectingMaxCount(length) ||
|
||||
!submitEnabled({ body }) ||
|
||||
!submitEnabled(input) ||
|
||||
loadingState === 'loading';
|
||||
const disableCancelButton = loadingState === 'loading';
|
||||
const disableTextArea = loadingState === 'loading';
|
||||
@@ -104,16 +104,20 @@ export class CommentForm extends React.Component {
|
||||
return (
|
||||
<div>
|
||||
<DraftArea
|
||||
id={this.props.bodyInputId}
|
||||
label={this.props.bodyLabel}
|
||||
value={body}
|
||||
placeholder={this.props.bodyPlaceholder}
|
||||
onChange={this.props.onBodyChange}
|
||||
root={root}
|
||||
comment={comment}
|
||||
id={this.props.id}
|
||||
input={input}
|
||||
onInputChange={this.props.onInputChange}
|
||||
disabled={disableTextArea}
|
||||
charCountEnable={this.props.charCountEnable}
|
||||
maxCharCount={this.props.maxCharCount}
|
||||
registerHook={this.props.registerHook}
|
||||
unregisterHook={this.props.unregisterHook}
|
||||
isReply={this.props.isReply}
|
||||
isEdit={this.props.isEdit}
|
||||
/>
|
||||
<div className={`${name}-button-container`}>
|
||||
<div className={cn(styles.buttonContainer, `${name}-button-container`)}>
|
||||
{this.props.buttonContainerStart}
|
||||
{typeof this.props.onCancel === 'function' && (
|
||||
<Button
|
||||
@@ -130,6 +134,7 @@ export class CommentForm extends React.Component {
|
||||
disableSubmitButton ? 'lightGrey' : this.props.submitButtonCStyle
|
||||
}
|
||||
className={cn(
|
||||
styles.button,
|
||||
`${name}-button`,
|
||||
submitButtonClassName,
|
||||
this.getButtonClassName()
|
||||
@@ -144,3 +149,5 @@ export class CommentForm extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CommentForm;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user