mirror of
https://github.com/wassname/talk.git
synced 2026-09-14 11:36:51 +08:00
Compare commits
993
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85e2bbc089 | ||
|
|
59aaec48fc | ||
|
|
60c8f9a0d5 | ||
|
|
b1efdad981 | ||
|
|
ab938985e4 | ||
|
|
a91de05af9 | ||
|
|
205e6fcd08 | ||
|
|
a92dcd6224 | ||
|
|
5150cdf60e | ||
|
|
dbbc1af42e | ||
|
|
b63c00f26f | ||
|
|
3b31e3b02d | ||
|
|
08e8e61e88 | ||
|
|
5e90f028a9 | ||
|
|
d217fedf9c | ||
|
|
f9114ef4be | ||
|
|
618646e71c | ||
|
|
e0627925fb | ||
|
|
538e1fca9d | ||
|
|
e70f6f5c7f | ||
|
|
647227e66c | ||
|
|
7ce02620e9 | ||
|
|
41db413bea | ||
|
|
5a7340ac3a | ||
|
|
18127ee72a | ||
|
|
7501155078 | ||
|
|
9eb5afbb2b | ||
|
|
c738d9a355 | ||
|
|
3bdb9e645d | ||
|
|
547e73f0ae | ||
|
|
d05bece722 | ||
|
|
d9d6403cda | ||
|
|
ff6cb22c25 | ||
|
|
20b6e6b3ff | ||
|
|
1bf63285bc | ||
|
|
d37333be89 | ||
|
|
37959f9398 | ||
|
|
4e043638f6 | ||
|
|
625809b42c | ||
|
|
60f5b7e3c0 | ||
|
|
7ad724e576 | ||
|
|
aa2346b715 | ||
|
|
c91a0fafa5 | ||
|
|
51880bcfc9 | ||
|
|
f4037ce6fb | ||
|
|
68839c721c | ||
|
|
3d7cb42434 | ||
|
|
710de07fa6 | ||
|
|
7ec6780ee1 | ||
|
|
b6aa1da817 | ||
|
|
a9431cd4d8 | ||
|
|
53e168ae93 | ||
|
|
9fa5900acc | ||
|
|
9b0e6ed53b | ||
|
|
7e8ef2189d | ||
|
|
939152ee81 | ||
|
|
94eb72a9bf | ||
|
|
0e941222c5 | ||
|
|
98a8c8dc71 | ||
|
|
01a2ace3f0 | ||
|
|
065cb4b03a | ||
|
|
326a10dc5d | ||
|
|
1fc49f8e50 | ||
|
|
90e67ca479 | ||
|
|
779c0292f3 | ||
|
|
097294909b | ||
|
|
6f538d3235 | ||
|
|
e134b4de04 | ||
|
|
8a64e6f62d | ||
|
|
4584c3d4fb | ||
|
|
5b62082693 | ||
|
|
a5c98cd587 | ||
|
|
71ce74e1d1 | ||
|
|
3fcb9a179a | ||
|
|
50cb46a053 | ||
|
|
f160eae500 | ||
|
|
21e1a5cbef | ||
|
|
13147c4ba4 | ||
|
|
6fa0c7f538 | ||
|
|
9af523318c | ||
|
|
ecdb4e1307 | ||
|
|
1a65b734d3 | ||
|
|
05350d651f | ||
|
|
3f949b3712 | ||
|
|
ca12b73b55 | ||
|
|
7aa9a6b4f7 | ||
|
|
b3cfe32631 | ||
|
|
6597658734 | ||
|
|
625ef9cbee | ||
|
|
c81d612c70 | ||
|
|
3c64bb0240 | ||
|
|
8435ccaee6 | ||
|
|
f0753214e1 | ||
|
|
f39da1f76b | ||
|
|
bc5db7b599 | ||
|
|
092e707785 | ||
|
|
93b91a0573 | ||
|
|
1dc400ba58 | ||
|
|
58e51b299d | ||
|
|
c47032f257 | ||
|
|
2571f048ad | ||
|
|
6fc2eabc88 | ||
|
|
33ecbaecda | ||
|
|
38fba52839 | ||
|
|
3c613754f0 | ||
|
|
d1e6d297f3 | ||
|
|
6dab5836c3 | ||
|
|
ffd0f90965 | ||
|
|
eadf7bde43 | ||
|
|
e7131e1567 | ||
|
|
977161db0a | ||
|
|
45cbac7972 | ||
|
|
0bb64f1d97 | ||
|
|
3b6f3c9ea8 | ||
|
|
fa72d5deda | ||
|
|
9436600e31 | ||
|
|
cd2ccf8792 | ||
|
|
b5e98d1025 | ||
|
|
d3f6762458 | ||
|
|
f5324f65bb | ||
|
|
544c21440b | ||
|
|
a3aca825e3 | ||
|
|
917fffc292 | ||
|
|
2e6237b9d9 | ||
|
|
1f0cba5657 | ||
|
|
bc4d746291 | ||
|
|
ecc4bdc4bb | ||
|
|
df29c087df | ||
|
|
9608198d86 | ||
|
|
dda4443ab4 | ||
|
|
01b4094b4e | ||
|
|
1176080aee | ||
|
|
4e0d578280 | ||
|
|
9a129af21e | ||
|
|
ea5d907ff5 | ||
|
|
a8d69a0c5d | ||
|
|
b74e8f3390 | ||
|
|
dcc4b4a892 | ||
|
|
bee35c3865 | ||
|
|
b6ee59eb91 | ||
|
|
aeb75666de | ||
|
|
6f51f68776 | ||
|
|
2b5ef018d0 | ||
|
|
847d5d0ea9 | ||
|
|
70465ebd31 | ||
|
|
3a9bcaa8b0 | ||
|
|
50dd2e28b0 | ||
|
|
80e6ccbc98 | ||
|
|
9f37c3b3da | ||
|
|
e9c80fc02a | ||
|
|
14658d3d5d | ||
|
|
3633fd7afb | ||
|
|
9347815276 | ||
|
|
c2ffb30431 | ||
|
|
3636e47b16 | ||
|
|
5362a26976 | ||
|
|
d25862e035 | ||
|
|
cda72ba249 | ||
|
|
ba15cbf23c | ||
|
|
cf2be96a13 | ||
|
|
ff5a94b91f | ||
|
|
8184c3932e | ||
|
|
44e6d4b26a | ||
|
|
343784dcb1 | ||
|
|
0d588f2f28 | ||
|
|
7c9b8e5377 | ||
|
|
49ec2bf18e | ||
|
|
ae63e9a975 | ||
|
|
0feb7384b2 | ||
|
|
b69313a01e | ||
|
|
ec539c2dae | ||
|
|
51b7f5470e | ||
|
|
64487728f4 | ||
|
|
11fb12a22a | ||
|
|
7e7a5856c2 | ||
|
|
333e1a6dac | ||
|
|
8dae12b5ab | ||
|
|
7c7f7f2a19 | ||
|
|
bd7903fc53 | ||
|
|
945cdfc298 | ||
|
|
d3fb04225c | ||
|
|
aa105061c9 | ||
|
|
9d3363198a | ||
|
|
44b589d04f | ||
|
|
5bf4c66b40 | ||
|
|
2a4dfdd3de | ||
|
|
bd1d677b63 | ||
|
|
929dfb9989 | ||
|
|
c9ba749c76 | ||
|
|
7f4d8d8b2f | ||
|
|
66cc0752a4 | ||
|
|
84234f9ac6 | ||
|
|
a09b50a9e7 | ||
|
|
5c77ed2c31 | ||
|
|
fabe52a0a6 | ||
|
|
ef91eaab20 | ||
|
|
13b039099f | ||
|
|
d22ba3ec89 | ||
|
|
7beb1a2fcd | ||
|
|
bfeab31c37 | ||
|
|
9dc702d29e | ||
|
|
0ec04aa561 | ||
|
|
9463fa8ec2 | ||
|
|
45b39c9a0f | ||
|
|
74fc0fab61 | ||
|
|
733ea936d6 | ||
|
|
b53aa354ad | ||
|
|
7948ed72ee | ||
|
|
0a17fc7e64 | ||
|
|
52cdec056b | ||
|
|
52b35c53ab | ||
|
|
ede3951309 | ||
|
|
5aa3024921 | ||
|
|
41575303a1 | ||
|
|
98f90e125d | ||
|
|
c462bcfe68 | ||
|
|
abb6f3aea6 | ||
|
|
639b439f02 | ||
|
|
ccb6f347ae | ||
|
|
465603526e | ||
|
|
073036e24f | ||
|
|
2c66143d39 | ||
|
|
708bfe17a3 | ||
|
|
adee5377c9 | ||
|
|
929fa8ca31 | ||
|
|
3ffdb09afe | ||
|
|
2e9d0a21fe | ||
|
|
d49a30c50a | ||
|
|
c23708af14 | ||
|
|
9966d5a644 | ||
|
|
bc447dbde0 | ||
|
|
5acd19790a | ||
|
|
16cbbe8d2c | ||
|
|
210146e998 | ||
|
|
66c3a48668 | ||
|
|
fd01fec70b | ||
|
|
06ed324393 | ||
|
|
473e41cdc4 | ||
|
|
2f361d4437 | ||
|
|
4dc46f7fee | ||
|
|
3343605957 | ||
|
|
fa240376b5 | ||
|
|
20170333ed | ||
|
|
f5c8b455f1 | ||
|
|
6dea28ba74 | ||
|
|
e127648a51 | ||
|
|
4b4b68b885 | ||
|
|
5c284ba82b | ||
|
|
35787c15af | ||
|
|
b916914faa | ||
|
|
1e8a8d306c | ||
|
|
411f5bc302 | ||
|
|
6e374aba8f | ||
|
|
6475f0c0f6 | ||
|
|
d03583df5e | ||
|
|
7b85197174 | ||
|
|
f7f24a37dd | ||
|
|
c2023672d3 | ||
|
|
05f7dbd115 | ||
|
|
b664b9abc9 | ||
|
|
e3a9f0f89b | ||
|
|
b9c3340026 | ||
|
|
b5b3c66d61 | ||
|
|
b6554b344c | ||
|
|
bbf98d0b67 | ||
|
|
45802c651e | ||
|
|
45e942a345 | ||
|
|
2f492c1f83 | ||
|
|
122b9e9871 | ||
|
|
3edaa6bf2d | ||
|
|
9326016e98 | ||
|
|
1b75b46e64 | ||
|
|
192c8e63cf | ||
|
|
04b4b7e715 | ||
|
|
4c898e8825 | ||
|
|
bd56a39d73 | ||
|
|
303c7489e4 | ||
|
|
9852c4e62f | ||
|
|
72bdf5d40b | ||
|
|
5695bee776 | ||
|
|
ff0f00a486 | ||
|
|
3a91a89488 | ||
|
|
2ce955bc96 | ||
|
|
a318b56233 | ||
|
|
8c192d283d | ||
|
|
c280ff9e21 | ||
|
|
7cce84b7f8 | ||
|
|
02909fb399 | ||
|
|
460429d991 | ||
|
|
3c640f87d3 | ||
|
|
8c2d811b55 | ||
|
|
3cdaa284ed | ||
|
|
a19ac4e2cc | ||
|
|
10748347b9 | ||
|
|
526a3c634a | ||
|
|
7139563ceb | ||
|
|
dee14709c8 | ||
|
|
8c3b0071c2 | ||
|
|
dc3ea366bc | ||
|
|
acdcd16b4d | ||
|
|
6e743b9b75 | ||
|
|
62a1fc5901 | ||
|
|
936347e8e4 | ||
|
|
661f392281 | ||
|
|
2502dfedd2 | ||
|
|
0cea633b0b | ||
|
|
d8417eda2e | ||
|
|
0e25c8d446 | ||
|
|
d072df8043 | ||
|
|
a8f315db4b | ||
|
|
a8ccf29bea | ||
|
|
5dd9d2d509 | ||
|
|
8589993401 | ||
|
|
50a691c1e5 | ||
|
|
32fdc51506 | ||
|
|
2edd923b9e | ||
|
|
ae60311e85 | ||
|
|
1702040537 | ||
|
|
0fbd27cdcb | ||
|
|
4107fd367f | ||
|
|
65156c0528 | ||
|
|
30d0541c69 | ||
|
|
fa17582e93 | ||
|
|
007e6f5e66 | ||
|
|
6e8c0df9fe | ||
|
|
106a5d36ed | ||
|
|
85af8d1bbf | ||
|
|
daee96dc4c | ||
|
|
ce1002b16c | ||
|
|
ebe43a28e6 | ||
|
|
6b814cee0d | ||
|
|
a996c5da2b | ||
|
|
029f4641d0 | ||
|
|
18e6638162 | ||
|
|
bc78efc428 | ||
|
|
a14c0205aa | ||
|
|
aff29d7818 | ||
|
|
be3ef78875 | ||
|
|
af22b9808b | ||
|
|
7eb1755c5e | ||
|
|
7ae3e4886a | ||
|
|
6df0568378 | ||
|
|
8efeb3106f | ||
|
|
f87b2b05ba | ||
|
|
8b1aec5f24 | ||
|
|
7e10f4a7bb | ||
|
|
2f1c4fb9a3 | ||
|
|
2a640d2bbc | ||
|
|
6c75298b8c | ||
|
|
8d86900376 | ||
|
|
395cfe4ade | ||
|
|
1d98bc78f2 | ||
|
|
dbd7f4f820 | ||
|
|
4e9b00934d | ||
|
|
8283932c29 | ||
|
|
9044fc2400 | ||
|
|
2af01e5f4b | ||
|
|
b00a7f9618 | ||
|
|
0983d31c10 | ||
|
|
0b0aa7b1ee | ||
|
|
3314bc3ba6 | ||
|
|
fa67da7183 | ||
|
|
5bf6161e46 | ||
|
|
601436cf99 | ||
|
|
cb2cfe1581 | ||
|
|
c199bede53 | ||
|
|
1bd31285a6 | ||
|
|
52c8c8a625 | ||
|
|
f391d7b7ad | ||
|
|
ced45a914c | ||
|
|
60d61d40b6 | ||
|
|
4f9e1ba535 | ||
|
|
b044b0f225 | ||
|
|
935c2ff629 | ||
|
|
1009045c4c | ||
|
|
05296d4802 | ||
|
|
a30e3ce526 | ||
|
|
2969dad58b | ||
|
|
a68ca34d62 | ||
|
|
7aaffdc887 | ||
|
|
8eeb9db48f | ||
|
|
38f1068a77 | ||
|
|
4b1e2dd2fd | ||
|
|
a3526ab241 | ||
|
|
23a5243611 | ||
|
|
ecc9eadc67 | ||
|
|
db483849dd | ||
|
|
72f5af9f3b | ||
|
|
ba2c2c32b2 | ||
|
|
3955c960bf | ||
|
|
bb7007214d | ||
|
|
c8cd68c6b7 | ||
|
|
185b3de8e1 | ||
|
|
aa904ab0e2 | ||
|
|
1ccb789b97 | ||
|
|
ed5ea3b759 | ||
|
|
2c8bdc6c9a | ||
|
|
32e56e805c | ||
|
|
632c4b263e | ||
|
|
55b65e6d6a | ||
|
|
e6f8b7623b | ||
|
|
eedeef9617 | ||
|
|
6f367a0078 | ||
|
|
ff9c6af7c6 | ||
|
|
8b09b52be8 | ||
|
|
2292e5d53f | ||
|
|
0c045eff0a | ||
|
|
b1e7c6d2d4 | ||
|
|
4d7edd24b4 | ||
|
|
1f3523c7f3 | ||
|
|
d04b9d818c | ||
|
|
cc67f2f572 | ||
|
|
19b1a7882c | ||
|
|
b1aac28eec | ||
|
|
8b83a0de41 | ||
|
|
76c222dbed | ||
|
|
7a4bf847b0 | ||
|
|
a794fe8cd7 | ||
|
|
7dbd2fae91 | ||
|
|
cf070ddb0f | ||
|
|
26b59fc17c | ||
|
|
7fa0132211 | ||
|
|
8a25e68266 | ||
|
|
5a8ce7655c | ||
|
|
7b221c40a0 | ||
|
|
170fb47dae | ||
|
|
4e63858c66 | ||
|
|
f7fa1dae23 | ||
|
|
91d421ad46 | ||
|
|
1ca2c5ec95 | ||
|
|
a35ee79098 | ||
|
|
b5db80b1e1 | ||
|
|
a36d944e4a | ||
|
|
b608e8a521 | ||
|
|
a8b24187cb | ||
|
|
49bfb6bcbf | ||
|
|
3b01af66b3 | ||
|
|
e9ab3d1d7f | ||
|
|
04d7ddf41a | ||
|
|
cb3a6b0533 | ||
|
|
96515eac0c | ||
|
|
71c5c7c64f | ||
|
|
195d61d104 | ||
|
|
89312e3d98 | ||
|
|
7644f93e7c | ||
|
|
5010e342df | ||
|
|
dce2177dcd | ||
|
|
b37ec2b944 | ||
|
|
88e44b2baf | ||
|
|
a7e4af46d2 | ||
|
|
8b938616c9 | ||
|
|
a33a1c47ab | ||
|
|
994b0f7fb3 | ||
|
|
e909d21039 | ||
|
|
fb1b9c51ee | ||
|
|
df664bdb73 | ||
|
|
c1cefe52d0 | ||
|
|
b765a7c3e9 | ||
|
|
845fbc1b1b | ||
|
|
1bd841ec01 | ||
|
|
e132087682 | ||
|
|
3a1b043eb7 | ||
|
|
53e548d77a | ||
|
|
0477d456a2 | ||
|
|
35b4040274 | ||
|
|
f5e0c6163d | ||
|
|
4d860ab71d | ||
|
|
942bcba5a1 | ||
|
|
2bf9b5f2c1 | ||
|
|
46ff3dea89 | ||
|
|
90a875f40e | ||
|
|
441cc9d57b | ||
|
|
28c8252b63 | ||
|
|
444c86cb3c | ||
|
|
110becb075 | ||
|
|
ee30003390 | ||
|
|
c96112fc8c | ||
|
|
31bc5fa913 | ||
|
|
eb74162eef | ||
|
|
3897b06029 | ||
|
|
59cf728681 | ||
|
|
76d198f2a6 | ||
|
|
aa2b70fd8c | ||
|
|
ccf91480da | ||
|
|
bcf24cbb4b | ||
|
|
a4002b262b | ||
|
|
51e6d5b29e | ||
|
|
0135cb9cc3 | ||
|
|
7001510049 | ||
|
|
4c1f2e72cb | ||
|
|
25e8cb517b | ||
|
|
428c35a931 | ||
|
|
792ceed2ad | ||
|
|
aeada424fa | ||
|
|
942ce366b8 | ||
|
|
0dcff65344 | ||
|
|
b83d965d6c | ||
|
|
09caad7bdd | ||
|
|
570bf4815d | ||
|
|
e2bcc4631c | ||
|
|
7ab70b8484 | ||
|
|
0497718764 | ||
|
|
c66c5a1ade | ||
|
|
60afa5518a | ||
|
|
a99fe3c0ff | ||
|
|
0b3a67601c | ||
|
|
13c196983f | ||
|
|
885a724876 | ||
|
|
fbb194cdac | ||
|
|
bacd09e5c7 | ||
|
|
9c5dd285a3 | ||
|
|
9945c099a7 | ||
|
|
99e34979d1 | ||
|
|
dcb3c23f56 | ||
|
|
1684ed44ef | ||
|
|
334c8933c9 | ||
|
|
5381b75a49 | ||
|
|
17563824fd | ||
|
|
123bbf6e65 | ||
|
|
5a87e6abc6 | ||
|
|
f57773393a | ||
|
|
9bea53911f | ||
|
|
5a26b319d8 | ||
|
|
39edec79cb | ||
|
|
b7208cfa44 | ||
|
|
d1d993d0b7 | ||
|
|
9cef7044c7 | ||
|
|
3dc3e1435a | ||
|
|
10df1de98e | ||
|
|
eab8f0e18b | ||
|
|
b2b9b20efb | ||
|
|
c419ee5ad1 | ||
|
|
130c7bac62 | ||
|
|
53cc69ac1f | ||
|
|
d8b5b00adc | ||
|
|
65a212b531 | ||
|
|
5501ee7832 | ||
|
|
0455293c0b | ||
|
|
89749926c6 | ||
|
|
21cad87ffd | ||
|
|
6fa28d6410 | ||
|
|
7b5a87207b | ||
|
|
e9db68269b | ||
|
|
94a42fc57f | ||
|
|
bace76decc | ||
|
|
ec5cdb5681 | ||
|
|
5aad008261 | ||
|
|
8e3c62c9db | ||
|
|
059292e979 | ||
|
|
348ce99828 | ||
|
|
d58221fb5e | ||
|
|
fe3f6e8d41 | ||
|
|
3f560231aa | ||
|
|
1c135a9ccb | ||
|
|
fb2af720ed | ||
|
|
310f250212 | ||
|
|
071cf1ce1d | ||
|
|
63d43caa23 | ||
|
|
e1dc50a494 | ||
|
|
d1e938112e | ||
|
|
6e1b9c99ac | ||
|
|
94ad23ce9b | ||
|
|
e4ebb2553f | ||
|
|
ecb82cf9a9 | ||
|
|
a48bcc3076 | ||
|
|
e704c6d968 | ||
|
|
551ce1521c | ||
|
|
8e9ac0748e | ||
|
|
4d7702c0e3 | ||
|
|
7c5ff1bcb1 | ||
|
|
a41dd703eb | ||
|
|
c955fbd22f | ||
|
|
0b3aead1d2 | ||
|
|
c40e5c58b2 | ||
|
|
a94e56d45f | ||
|
|
b4480f4bf7 | ||
|
|
6648dfe492 | ||
|
|
4688f170e4 | ||
|
|
f19ada431d | ||
|
|
4c86d842dd | ||
|
|
7e081bbf07 | ||
|
|
8a7ea9cdef | ||
|
|
ab4ca7f24c | ||
|
|
5c2ac9ee75 | ||
|
|
9e39a093fd | ||
|
|
b30404699b | ||
|
|
443ccd61ce | ||
|
|
de5c37ebcf | ||
|
|
78fa0b270f | ||
|
|
1dec622e64 | ||
|
|
d2106b3de5 | ||
|
|
7cb5ab37a7 | ||
|
|
505bb48f8b | ||
|
|
bb39b4e8fa | ||
|
|
22fa940ddb | ||
|
|
06f9c0a212 | ||
|
|
e1c60bc18d | ||
|
|
bf8549c811 | ||
|
|
37f733dfa0 | ||
|
|
9510fbed57 | ||
|
|
ea02e47241 | ||
|
|
4e493ad1da | ||
|
|
c2f6095868 | ||
|
|
3d11335100 | ||
|
|
dbcf3c5756 | ||
|
|
32264fbc5f | ||
|
|
2a0f9cd935 | ||
|
|
594c74c8c1 | ||
|
|
797a02b2a3 | ||
|
|
3eceb22198 | ||
|
|
60843f2322 | ||
|
|
5f4fbfc209 | ||
|
|
d3da5abd5c | ||
|
|
92a266363a | ||
|
|
15038fc8ab | ||
|
|
3f64a0d1a6 | ||
|
|
04f03b7f3b | ||
|
|
0dfdea0e4e | ||
|
|
e63137f302 | ||
|
|
52ab434740 | ||
|
|
361aab7085 | ||
|
|
d140e2449f | ||
|
|
f3403d4223 | ||
|
|
7483414981 | ||
|
|
e696479592 | ||
|
|
639fa4f721 | ||
|
|
a99f907a7d | ||
|
|
31835d2b7c | ||
|
|
699b07ad08 | ||
|
|
b5d626ad8c | ||
|
|
c926d6b721 | ||
|
|
1235cc7aad | ||
|
|
8634647c9f | ||
|
|
bec5b85039 | ||
|
|
dd45b345b7 | ||
|
|
ff62dc4ecc | ||
|
|
606bcb6393 | ||
|
|
22a0e80c66 | ||
|
|
0ab3ba2a7b | ||
|
|
2e6d326eb2 | ||
|
|
781519956a | ||
|
|
2fb3ab2e8f | ||
|
|
229569256e | ||
|
|
c1956a307d | ||
|
|
ab292cd4a4 | ||
|
|
577629c65b | ||
|
|
6df52aadbb | ||
|
|
f75a2ac81c | ||
|
|
ac4b431d71 | ||
|
|
245b8648d1 | ||
|
|
b21f3560ff | ||
|
|
d6ff25dd10 | ||
|
|
5d58681daa | ||
|
|
685dbfd7be | ||
|
|
fe7b734b2c | ||
|
|
34fb53b3b0 | ||
|
|
52b36e2db0 | ||
|
|
dff3e40005 | ||
|
|
8a97ed162c | ||
|
|
3166478ec3 | ||
|
|
95716da133 | ||
|
|
1879edbf58 | ||
|
|
8ecae1b9dd | ||
|
|
77eb538082 | ||
|
|
c792dd4e7e | ||
|
|
d0bcd53695 | ||
|
|
820ee12485 | ||
|
|
7c99dcf8df | ||
|
|
59ce08e8b1 | ||
|
|
a63da509a3 | ||
|
|
7c91df5103 | ||
|
|
6c284feea7 | ||
|
|
608534b2d0 | ||
|
|
11ba91c20e | ||
|
|
b32553ef00 | ||
|
|
797b8cacc8 | ||
|
|
ef4637cf7e | ||
|
|
9ddc02fff5 | ||
|
|
4fd735bc04 | ||
|
|
41f92b2144 | ||
|
|
c7a96cc0f1 | ||
|
|
be1e5a2f7f | ||
|
|
0a5e6f7250 | ||
|
|
66d8244acc | ||
|
|
7dedf2c129 | ||
|
|
b3fa5e1e73 | ||
|
|
8594b82d90 | ||
|
|
2a87fb83f7 | ||
|
|
dd546c227d | ||
|
|
085723435b | ||
|
|
09cc76a973 | ||
|
|
41d4c67728 | ||
|
|
1376ed255b | ||
|
|
d05508d574 | ||
|
|
fbdc10e577 | ||
|
|
15865d1db2 | ||
|
|
c6b8840b47 | ||
|
|
c21b8a044e | ||
|
|
051cb9885b | ||
|
|
5fb23c7634 | ||
|
|
0c9dd07f1f | ||
|
|
ba01263731 | ||
|
|
4606626ec4 | ||
|
|
18d95b603b | ||
|
|
6d7056d831 | ||
|
|
93044b3c6a | ||
|
|
9e2b1aa9e3 | ||
|
|
5c7f4b461d | ||
|
|
64da3f02f9 | ||
|
|
67c8ca9d57 | ||
|
|
cf3b706bbc | ||
|
|
201e5fbcb1 | ||
|
|
c221f827b4 | ||
|
|
2ac6883737 | ||
|
|
5fd7062a3a | ||
|
|
64b579695e | ||
|
|
7c446bc9d8 | ||
|
|
bf959691eb | ||
|
|
466cecda5f | ||
|
|
45e25581f7 | ||
|
|
387e53e4e6 | ||
|
|
e1d534391c | ||
|
|
2aa725d61c | ||
|
|
d7c3796c9f | ||
|
|
2c4ce9169d | ||
|
|
325f453476 | ||
|
|
1c60e2fdea | ||
|
|
ea44227519 | ||
|
|
f51f989021 | ||
|
|
db09033367 | ||
|
|
89472bfe9f | ||
|
|
ceaa0599a1 | ||
|
|
8b56e408f8 | ||
|
|
ad4fcfd002 | ||
|
|
57d80af2e7 | ||
|
|
1923e929ee | ||
|
|
f0e0051c97 | ||
|
|
b787f260f2 | ||
|
|
e0a8f2e067 | ||
|
|
906940f152 | ||
|
|
e519503129 | ||
|
|
40ab723cf1 | ||
|
|
b421ea97b3 | ||
|
|
41791ebf79 | ||
|
|
aee0fbf7ab | ||
|
|
31e6735ddd | ||
|
|
21120bf760 | ||
|
|
96e7407d4c | ||
|
|
50e46b4606 | ||
|
|
1ec1572efb | ||
|
|
4b5c7c1c7b | ||
|
|
49400d116f | ||
|
|
13d293b9aa | ||
|
|
81e8f68e62 | ||
|
|
acb86f4b44 | ||
|
|
afe816ba41 | ||
|
|
6bbb645552 | ||
|
|
558f81c9aa | ||
|
|
0a0f7ce216 | ||
|
|
2c1111109a | ||
|
|
bdd4bfc272 | ||
|
|
2e4d23e900 | ||
|
|
a015a474aa | ||
|
|
cb40f061be | ||
|
|
4c6b2cfb84 | ||
|
|
a6bfad0833 | ||
|
|
c77d8739e7 | ||
|
|
0280d300f5 | ||
|
|
b2e2e3e75d | ||
|
|
e7c36c0b4c | ||
|
|
6eb82ed555 | ||
|
|
c5951117c3 | ||
|
|
e33ea6d0aa | ||
|
|
f8e9b5dd50 | ||
|
|
7f265a93c8 | ||
|
|
5e64961347 | ||
|
|
69b459d02b | ||
|
|
521af6f0aa | ||
|
|
726f2f0d17 | ||
|
|
05605d6d50 | ||
|
|
d31d8d94e2 | ||
|
|
49d0296d5c | ||
|
|
508d9618a2 | ||
|
|
212932486a | ||
|
|
b87f984fd3 | ||
|
|
6f11ec3338 | ||
|
|
49552e7772 | ||
|
|
b0d8c1348b | ||
|
|
78d00d69fe | ||
|
|
236a740497 | ||
|
|
0ee20febd2 | ||
|
|
90cb56d92f | ||
|
|
a2a0cebb06 | ||
|
|
a4104f8bd8 | ||
|
|
d09835020d | ||
|
|
9c98330ea4 | ||
|
|
5acd48c49f | ||
|
|
8487f22b8e | ||
|
|
c7feaa344f | ||
|
|
189623a367 | ||
|
|
7c139a37b4 | ||
|
|
2cf7f1e2ef | ||
|
|
54e99dec12 | ||
|
|
caec45a0c6 | ||
|
|
e064ad49ae | ||
|
|
de739554c8 | ||
|
|
b9a8fdb77b | ||
|
|
cecb0b7da7 | ||
|
|
b1cb624e4f | ||
|
|
7b929c356e | ||
|
|
31113e7ecf | ||
|
|
4982d8d83e | ||
|
|
0cc705d71b | ||
|
|
f71d449079 | ||
|
|
35ab62af92 | ||
|
|
2c2213893e | ||
|
|
22f9631cb8 | ||
|
|
6f6bedb07a | ||
|
|
f24c21c94e | ||
|
|
b34b9e5986 | ||
|
|
524994d139 | ||
|
|
34f42c2991 | ||
|
|
7c88c251ea | ||
|
|
b07073b882 | ||
|
|
01dab3213a | ||
|
|
7e98580264 | ||
|
|
4db619d825 | ||
|
|
db3db21140 | ||
|
|
f5ef551fb4 | ||
|
|
f97cab79c8 | ||
|
|
abfa39529f | ||
|
|
0aa556d288 | ||
|
|
f693820244 | ||
|
|
09c941a906 | ||
|
|
5f756056b1 | ||
|
|
1cd4d537af | ||
|
|
70ccd646ff | ||
|
|
34ddd2ea0b | ||
|
|
2a7fa08bd6 | ||
|
|
a2a49b8e45 | ||
|
|
b6305bec16 | ||
|
|
b1a7cdb78f | ||
|
|
bd4091e41b | ||
|
|
27334c7bd1 | ||
|
|
399e9fd59f | ||
|
|
1c33490bb6 | ||
|
|
34b80165c7 | ||
|
|
07b910d7a3 | ||
|
|
911a03ba3d | ||
|
|
035d241a72 | ||
|
|
d1d80544ba | ||
|
|
6ce5d8492f | ||
|
|
42d30af27b | ||
|
|
cac0afa61e | ||
|
|
9c6a06b2c0 | ||
|
|
e76740c31f | ||
|
|
9684b6046b | ||
|
|
d8021f0f33 | ||
|
|
05552f7a84 | ||
|
|
487a108624 | ||
|
|
1a65aa7635 | ||
|
|
885e53c34d | ||
|
|
542df81387 | ||
|
|
ea115dd5ef | ||
|
|
057d4254fa | ||
|
|
809b0d8530 | ||
|
|
c7fc6d4243 | ||
|
|
ab2c9a2195 | ||
|
|
c60c4b3ef8 | ||
|
|
ec0adf8492 | ||
|
|
067464104a | ||
|
|
6467348296 | ||
|
|
11d667cfb8 | ||
|
|
cf1c5c58c2 | ||
|
|
ecf9dae9ec | ||
|
|
01dc5cd47e | ||
|
|
59f8c68153 | ||
|
|
ea0456ac76 | ||
|
|
9f5ce08952 | ||
|
|
aac63bfd3c | ||
|
|
4f8d016ac8 | ||
|
|
2d69982ebb | ||
|
|
ff41fabb37 | ||
|
|
1a08aba969 | ||
|
|
a79e916a96 | ||
|
|
70ff49ec4f | ||
|
|
871206740f | ||
|
|
35ba69c8ed | ||
|
|
c90f6cbe53 | ||
|
|
53190f75f1 | ||
|
|
43c13c0731 | ||
|
|
d2368f4cc5 | ||
|
|
bfede26ddd | ||
|
|
5118d5ae58 | ||
|
|
fc616d1753 | ||
|
|
5f9e6b359a | ||
|
|
ec1eff2b9d | ||
|
|
8e87cba52f | ||
|
|
6efe36ceaa | ||
|
|
186f8e1987 | ||
|
|
358fe97069 | ||
|
|
d46145f04c | ||
|
|
c5dbe4659b | ||
|
|
5e5b0a3888 | ||
|
|
c0da4e97aa | ||
|
|
093e4fd736 | ||
|
|
15cbb94756 | ||
|
|
f7ef7ae2b0 | ||
|
|
3355400248 | ||
|
|
37338ceab9 | ||
|
|
805492931b | ||
|
|
6298a50bbe | ||
|
|
8690646409 | ||
|
|
d89d4c25b7 | ||
|
|
ca12ec652d | ||
|
|
a18a9ab0c4 | ||
|
|
50fdabc0c0 | ||
|
|
37fb9ece66 | ||
|
|
7ee9f23542 | ||
|
|
11de453aac | ||
|
|
25c931cc05 | ||
|
|
99b06445c9 | ||
|
|
155b8c7316 | ||
|
|
b15ea811e5 | ||
|
|
6210ae6077 | ||
|
|
50de9a7f84 | ||
|
|
205ef6b4a4 | ||
|
|
b77f9b8eaf | ||
|
|
89c697b18f | ||
|
|
84dd673c2e | ||
|
|
d7acd23884 | ||
|
|
041e2e9cda | ||
|
|
b83dabc616 | ||
|
|
f7a6b974a3 | ||
|
|
4fe85b8dbc | ||
|
|
01e14cc110 | ||
|
|
4a9e620eaf | ||
|
|
2c3e53e573 | ||
|
|
15478fe54f | ||
|
|
b3f850724a | ||
|
|
87ce65755c | ||
|
|
0fc57ab6a7 | ||
|
|
37cb23dd59 | ||
|
|
e4c0e16e1b | ||
|
|
e1cf3a06f3 | ||
|
|
ea6eaee0d7 | ||
|
|
3e90877012 | ||
|
|
e450fa05e0 | ||
|
|
6a185e587e | ||
|
|
eb7ddd7310 | ||
|
|
bdd4702a8f | ||
|
|
67790ac9e1 | ||
|
|
94588951d3 | ||
|
|
9f79ee1c6d | ||
|
|
95bf54366c | ||
|
|
c9c99623bc | ||
|
|
52594dc028 | ||
|
|
85c06617e2 | ||
|
|
e58e7c5cad | ||
|
|
b43b8abede | ||
|
|
6615dc0ac9 | ||
|
|
1a14c13105 | ||
|
|
ed97f80a00 | ||
|
|
a1fc11a893 | ||
|
|
c1ab0d96d8 | ||
|
|
9afc322314 | ||
|
|
ec6955715f | ||
|
|
679ad01a7d | ||
|
|
eacdb7db59 | ||
|
|
0784e7dd63 | ||
|
|
70b88de51a | ||
|
|
6ac94c8c2a | ||
|
|
044e1c2863 | ||
|
|
796d24db3d | ||
|
|
1236946312 | ||
|
|
ab8fd935e7 | ||
|
|
9375f3ec23 | ||
|
|
4494ac8900 | ||
|
|
986716b3c3 | ||
|
|
986c92598d | ||
|
|
65c8da0f34 | ||
|
|
68794d5919 | ||
|
|
b1f71a8b3e | ||
|
|
bcf5658c36 | ||
|
|
3527d350da | ||
|
|
b51e1c9867 | ||
|
|
fa57c72842 | ||
|
|
6f80a2458c | ||
|
|
a3e3d93607 | ||
|
|
1917a17929 | ||
|
|
7177988059 | ||
|
|
4318e1ddbe | ||
|
|
02e1236792 |
@@ -0,0 +1,208 @@
|
||||
# job_environment will setup the environment for any job being executed.
|
||||
job_environment: &job_environment
|
||||
NODE_ENV: test
|
||||
|
||||
# job_defaults applies all the defaults for each job.
|
||||
job_defaults: &job_defaults
|
||||
working_directory: ~/coralproject/talk
|
||||
docker:
|
||||
- image: circleci/node:10
|
||||
environment:
|
||||
<<: *job_environment
|
||||
|
||||
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:
|
||||
keys:
|
||||
# Find a cache corresponding to this specific package-lock.json
|
||||
# checksum when this file is changed, this key will fail.
|
||||
- v1-dependency-cache-{{ checksum "package-lock.json" }}
|
||||
# Find the most recently generated cache used from any branch
|
||||
- v1-dependency-cache-
|
||||
- run:
|
||||
name: Update NPM
|
||||
command: sudo npm update -g npm
|
||||
# Disabled until there's capabilities to ignore a specific vun. Related:
|
||||
# https://npm.community/t/please-provide-option-to-ignore-packages-in-npm-audit/403/4
|
||||
# https://github.com/npm/cli/pull/10
|
||||
# https://github.com/npm/rfcs/pull/18
|
||||
#
|
||||
# - run:
|
||||
# name: Audit dependencies
|
||||
# command: npm audit
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: npm ci
|
||||
- save_cache:
|
||||
key: v1-dependency-cache-{{ checksum "package-lock.json" }}
|
||||
paths:
|
||||
- ~/.npm
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths: node_modules
|
||||
|
||||
# lint will perform file linting.
|
||||
lint:
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/coralproject/talk
|
||||
- run:
|
||||
name: Generate schemas and types
|
||||
command: npm run generate
|
||||
- run:
|
||||
name: Lint Source Code
|
||||
command: npm run lint
|
||||
- run:
|
||||
name: Lint README.md
|
||||
command: |
|
||||
cp README.md README.md.orig
|
||||
npm run doctoc
|
||||
diff -q README.md README.md.orig
|
||||
|
||||
# unit_tests will run the unit tests.
|
||||
unit_tests:
|
||||
<<: *job_defaults
|
||||
environment:
|
||||
<<: *job_environment
|
||||
CI: true
|
||||
JEST_JUNIT_OUTPUT: "reports/junit/js-test-results.xml"
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/coralproject/talk
|
||||
- run:
|
||||
name: Generate schemas and types
|
||||
command: npm run generate
|
||||
- run:
|
||||
name: Perform testing
|
||||
# We're running these tests in band to avoid errors where the circleci
|
||||
# test runner runs out of RAM trying to run them all in parallel.
|
||||
command: npm run test -- --ci --runInBand --testResultsProcessor="jest-junit"
|
||||
- store_test_results:
|
||||
path: reports/junit
|
||||
- store_artifacts:
|
||||
path: reports/junit
|
||||
|
||||
# build will build the static assets and server typescript files.
|
||||
build:
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: ~/coralproject/talk
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-build-cache-{{ .Branch }}-{{ .Revision }}
|
||||
- v1-build-cache-{{ .Branch }}-
|
||||
- v1-build-cache-
|
||||
- run:
|
||||
name: Build
|
||||
command: npm run build
|
||||
no_output_timeout: 30m
|
||||
- run:
|
||||
name: Verify Bundle Size
|
||||
command: npx bundlesize
|
||||
- save_cache:
|
||||
key: v1-build-cache-{{ .Branch }}-{{ .Revision }}
|
||||
paths:
|
||||
- ./dist
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths: dist
|
||||
|
||||
# docker_tests will test that the docker build process completes.
|
||||
docker_tests:
|
||||
docker:
|
||||
- image: node:10-alpine
|
||||
environment:
|
||||
NODE_ENV: development
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: Update Image Dependencies
|
||||
command: apk --no-cache add git python
|
||||
- run:
|
||||
name: Install NodeJS Dependencies
|
||||
command: npm install
|
||||
|
||||
# release_docker will build and push the Docker image.
|
||||
release_docker:
|
||||
<<: *job_defaults
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker
|
||||
- deploy:
|
||||
name: Deploy the code
|
||||
command: bash ./scripts/docker.sh deploy
|
||||
no_output_timeout: 30m
|
||||
|
||||
# filter_release will add the filters for a deploy job in a workflow to make it
|
||||
# only execute on a deploy related job.
|
||||
filter_release: &filter_release
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- next
|
||||
tags:
|
||||
only: /^v.*/
|
||||
|
||||
# filter_develop will add the filters for a development related commit.
|
||||
filter_develop: &filter_develop
|
||||
filters:
|
||||
branches:
|
||||
ignore:
|
||||
- master
|
||||
- next
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build-test:
|
||||
jobs:
|
||||
- docker_tests:
|
||||
<<: *filter_develop
|
||||
- npm_dependencies:
|
||||
<<: *filter_develop
|
||||
- lint:
|
||||
<<: *filter_develop
|
||||
requires:
|
||||
- npm_dependencies
|
||||
- unit_tests:
|
||||
<<: *filter_develop
|
||||
requires:
|
||||
- npm_dependencies
|
||||
- build:
|
||||
<<: *filter_develop
|
||||
requires:
|
||||
- npm_dependencies
|
||||
build-test-deploy:
|
||||
jobs:
|
||||
- npm_dependencies:
|
||||
<<: *filter_release
|
||||
- lint:
|
||||
<<: *filter_release
|
||||
requires:
|
||||
- npm_dependencies
|
||||
- unit_tests:
|
||||
<<: *filter_release
|
||||
requires:
|
||||
- npm_dependencies
|
||||
- build:
|
||||
<<: *filter_release
|
||||
requires:
|
||||
- npm_dependencies
|
||||
- release_docker:
|
||||
<<: *filter_release
|
||||
requires:
|
||||
- lint
|
||||
- unit_tests
|
||||
- build
|
||||
+26
-14
@@ -1,18 +1,30 @@
|
||||
# excluded because we'll likely need to rebuild this.
|
||||
node_modules
|
||||
|
||||
# scripts are used during development and testing, not
|
||||
# production.
|
||||
scripts
|
||||
|
||||
# documentation should not be visable in production.
|
||||
docs
|
||||
|
||||
# static assets are rebuild in the docker container.
|
||||
dist
|
||||
|
||||
# tests are not run in the docker container.
|
||||
test
|
||||
__tests__
|
||||
coverage
|
||||
|
||||
# we won't use the .git folder in production.
|
||||
.git
|
||||
|
||||
# don't include the dependancies
|
||||
node_modules
|
||||
|
||||
# don't include any logs
|
||||
npm-debug.log*
|
||||
|
||||
# don't include any yarn files
|
||||
yarn-error.log
|
||||
yarn.lock
|
||||
|
||||
# don't include any OS/editor files
|
||||
.env
|
||||
.idea/
|
||||
.vs
|
||||
.docz
|
||||
*.swp
|
||||
*.DS_STORE
|
||||
|
||||
# don't include any generated files
|
||||
dist
|
||||
*.css.d.ts
|
||||
__generated__
|
||||
README.md.orig
|
||||
|
||||
+5
-2
@@ -8,5 +8,8 @@ charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
[*.json]
|
||||
insert_final_newline = false
|
||||
|
||||
[*.ts]
|
||||
max_line_length = 80
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
dist
|
||||
docs
|
||||
client/lib
|
||||
**/*.html
|
||||
plugins/*
|
||||
!plugins/coral-plugin-facebook-auth
|
||||
!plugins/coral-plugin-auth
|
||||
!plugins/coral-plugin-respect
|
||||
!plugins/coral-plugin-offtopic
|
||||
!plugins/coral-plugin-like
|
||||
!plugins/coral-plugin-mod
|
||||
!plugins/coral-plugin-love
|
||||
!plugins/coral-plugin-viewing-options
|
||||
!plugins/coral-plugin-comment-content
|
||||
!plugins/talk-plugin-permalink
|
||||
node_modules
|
||||
@@ -1,66 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2017
|
||||
},
|
||||
"rules": {
|
||||
"indent": ["error",
|
||||
2
|
||||
],
|
||||
"no-console": [
|
||||
0
|
||||
],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": ["error", "single"],
|
||||
"semi": ["error", "always"],
|
||||
"no-template-curly-in-string": [1],
|
||||
"no-unsafe-negation": [1],
|
||||
"array-callback-return": [1],
|
||||
"arrow-parens": ["warn", "always"],
|
||||
"template-curly-spacing": "warn",
|
||||
"eqeqeq": [2, "smart"],
|
||||
"no-eval": [2],
|
||||
"no-global-assign": [2],
|
||||
"no-implied-eval": [2],
|
||||
"lines-around-comment": ["warn", {"beforeLineComment": true}],
|
||||
"spaced-comment": ["warn", "always", { "line": { "exceptions": ["-", "="] } }],
|
||||
"no-script-url": [2],
|
||||
"no-throw-literal": [2],
|
||||
"yoda": [1],
|
||||
"no-path-concat": [2],
|
||||
"eol-last": [1],
|
||||
"no-nested-ternary": [1],
|
||||
"no-tabs": [2],
|
||||
"no-unneeded-ternary": [1],
|
||||
"object-curly-spacing": [1],
|
||||
"space-infix-ops": ["error"],
|
||||
"space-in-parens": ["error", "never"],
|
||||
"space-unary-ops": ["error", {
|
||||
"words": true,
|
||||
"nonwords": false
|
||||
}],
|
||||
"no-const-assign": [2],
|
||||
"no-duplicate-imports": [2],
|
||||
"prefer-template": [1],
|
||||
"comma-spacing": ["error", {
|
||||
"after": true
|
||||
}],
|
||||
"no-var": [2],
|
||||
"no-lonely-if": [2],
|
||||
"curly": [2],
|
||||
"no-unused-vars": ["error", {
|
||||
"argsIgnorePattern": "^_|next",
|
||||
"varsIgnorePattern": "^_"
|
||||
}],
|
||||
"no-multiple-empty-lines": ["error", {
|
||||
"max": 1
|
||||
}],
|
||||
"newline-per-chained-call": ["error", {
|
||||
"ignoreChainWithDepth": 2
|
||||
}]
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
### Expected behavior
|
||||
|
||||
### Actual behavior
|
||||
|
||||
### Steps to reproduce behavior
|
||||
@@ -1,7 +0,0 @@
|
||||
## What does this PR do?
|
||||
|
||||
## How do I test this PR?
|
||||
|
||||
- click a button
|
||||
- view the cat
|
||||
- see the cat meow
|
||||
+15
-22
@@ -1,30 +1,23 @@
|
||||
# don't include the dependancies
|
||||
node_modules
|
||||
|
||||
dist
|
||||
|
||||
# don't include any logs
|
||||
npm-debug.log*
|
||||
dump.rdb
|
||||
|
||||
# don't include any yarn files
|
||||
yarn-error.log
|
||||
yarn.lock
|
||||
|
||||
# don't include any OS/editor files
|
||||
.env
|
||||
*.cfg
|
||||
|
||||
.idea/
|
||||
.vs
|
||||
.docz
|
||||
*.swp
|
||||
*.DS_STORE
|
||||
|
||||
test/e2e/reports
|
||||
coverage/
|
||||
|
||||
plugins.json
|
||||
plugins/*
|
||||
!plugins/coral-plugin-facebook-auth
|
||||
!plugins/coral-plugin-auth
|
||||
!plugins/coral-plugin-respect
|
||||
!plugins/coral-plugin-offtopic
|
||||
!plugins/coral-plugin-like
|
||||
!plugins/coral-plugin-mod
|
||||
!plugins/coral-plugin-love
|
||||
!plugins/coral-plugin-viewing-options
|
||||
!plugins/coral-plugin-comment-content
|
||||
!plugins/talk-plugin-permalink
|
||||
|
||||
**/node_modules/*
|
||||
# don't include any generated files
|
||||
dist
|
||||
*.css.d.ts
|
||||
__generated__
|
||||
README.md.orig
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"projects": {
|
||||
"tenant": {
|
||||
"schemaPath": "src/core/server/graph/tenant/schema/schema.graphql"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"verbose": true,
|
||||
"ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"],
|
||||
"ext": "js,json,graphql"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"args": [
|
||||
"-r",
|
||||
"ts-node/register",
|
||||
"-r",
|
||||
"tsconfig-paths/register",
|
||||
"${workspaceFolder}/src/index.ts"
|
||||
],
|
||||
"env": {
|
||||
"TS_NODE_PROJECT": "${workspaceFolder}/src/tsconfig.json",
|
||||
// Set to 1 here beacuse clustering gets all weird with logging + output.
|
||||
"CONCURRENCY": "1"
|
||||
},
|
||||
"outputCapture": "std"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"files.associations": {
|
||||
"*.css": "postcss"
|
||||
},
|
||||
"files.exclude": {
|
||||
"**/.git": true,
|
||||
"**/.svn": true,
|
||||
"**/.hg": true,
|
||||
"**/CVS": true,
|
||||
"**/.DS_Store": true,
|
||||
".vs": true
|
||||
},
|
||||
"tslint.exclude": "**/node_modules/**",
|
||||
"tslint.autoFixOnSave": true,
|
||||
"tslint.jsEnable": true,
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"postcss.validate": false
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
# Contributor's Guide
|
||||
|
||||
Welcome! We are very excited that you are interested in contributing to Talk.
|
||||
|
||||
This document is a companion to help you approach contributing. If it does not do so, please [let us know how we can improve it](https://github.com/coralproject/talk/issues)!
|
||||
|
||||
By contributing to this project you agree to the [Code of Conduct](https://coralproject.net/code-of-conduct.html).
|
||||
|
||||
## What should I Contribute?
|
||||
|
||||
There are at least three ways to contribute to Talk:
|
||||
|
||||
* Writing Code
|
||||
* Writing Docs
|
||||
* Providing Translations
|
||||
|
||||
## Writing Code
|
||||
|
||||
Conversation surrounding contributions begins in [issues](https://github.com/coralproject/talk/issues).
|
||||
|
||||
### When should I Create an Issue?
|
||||
|
||||
File an issue as soon as you have an idea of something you'd like to contribute. We would love to hear what you're thinking and help refine the idea to make it into the Talk ecosystem.
|
||||
|
||||
Please file issues if:
|
||||
|
||||
* you would like to contribute to Talk Core.
|
||||
* you are building a Plugin that the current Plugin API doesn't support.
|
||||
* you are building a Plugin and want advice.
|
||||
|
||||
### What should I include?
|
||||
|
||||
Coral has adopted an iterative, agile development philosophy. All contributions that make it into the Talk repo should start with a story or this form:
|
||||
|
||||
`As a [type of person] I'd like to be able to [do something] so that I can [get some result].`
|
||||
|
||||
This exercise does two things:
|
||||
|
||||
* allows us to ground our technical choices in a clear, simple product need.
|
||||
* expresses that product need in a way that doesn't imply a specific technical solution allowing for debate as to the best way to solve the problem.
|
||||
|
||||
Please feel free to provide as much detail as possible when filing the issue but please do keep the initial issue specific to one need and try to avoid including technical or design solutions.
|
||||
|
||||
If you have a specific technical or design solution in mind, please submit it as the first comment on the thread.
|
||||
|
||||
## Contributing Documentation
|
||||
|
||||
Clear docs are a prerequisite for a successful open source project. Contributing to the documentation is often more important than contributing to the code!
|
||||
|
||||
We are looking for _documentarians_ to:
|
||||
|
||||
* make clarity, grammar and completeness updates,
|
||||
* create new / missing sections, and
|
||||
* take the lead in making sections, or the over all structure better.
|
||||
|
||||
Information about how to update docs can be found in our [FAQ](faq.html#how-do-i-contribute-to-these-docs).
|
||||
|
||||
If you'd like to discuss a contribution, please [file an issue](https://github.com/coralproject/talk/issues) describing the changes you would like to see.
|
||||
|
||||
## Contributing Translations
|
||||
|
||||
Talk's tranlations are stored in `.yml` files [here](https://github.com/coralproject/talk/tree/master/locales).
|
||||
|
||||
Translations can be submitted via pull request. If you do not use github, you can use 'en.yml' as a template and [email](https://coralproject.net/contact.html) the translations to us. We can import it into the repo.
|
||||
|
||||
## I want to contribute but I'm not sure what to do!
|
||||
|
||||
If you want to contribute but don't have a clear idea of exactly what that may be, here are some resources that may help:
|
||||
|
||||
### Product Roadmap
|
||||
|
||||
Please visit our product roadmap here: https://www.pivotaltracker.com/n/projects/1863625. If you'd like to take on any of our scheduled tasks we'd be forever grateful!
|
||||
|
||||
### Discussion Forum
|
||||
|
||||
If you'd like to discuss what we're up to, please visit or [community](https://community.coralproject.net/) or [contact us](https://coralproject.net/contact.html).
|
||||
|
||||
### Integrations
|
||||
|
||||
Have a favorite analytics engine? Data science service? CMS? Auth platform? Deployment platform or pipeline? Pet project? Consider building a plugin to integrate them!
|
||||
|
||||
### Favorite Features?
|
||||
|
||||
Do you have a favorite feature of an existing platform that's not yet been done in Talk? Sounds like Talk needs that feature.
|
||||
|
||||
## Thanks!
|
||||
+27
-20
@@ -1,26 +1,33 @@
|
||||
FROM node:7.8
|
||||
FROM node:10-alpine
|
||||
|
||||
# Create app directory
|
||||
# Install build dependancies.
|
||||
RUN apk --no-cache add git python
|
||||
|
||||
# Create app directory.
|
||||
RUN mkdir -p /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Setup the environment
|
||||
ENV PATH /usr/src/app/bin:$PATH
|
||||
ENV TALK_PORT 5000
|
||||
EXPOSE 5000
|
||||
|
||||
# Bundle app source
|
||||
COPY . /usr/src/app
|
||||
|
||||
# Install app dependencies and build static assets.
|
||||
RUN yarn global add node-gyp && \
|
||||
yarn install --frozen-lockfile && \
|
||||
cli plugins reconcile && \
|
||||
yarn build && \
|
||||
yarn install --production && \
|
||||
yarn cache clean
|
||||
|
||||
# Ensure the runtime of the container is in production mode.
|
||||
# Setup the environment for production.
|
||||
ENV NODE_ENV production
|
||||
|
||||
CMD ["yarn", "start"]
|
||||
# Bundle application source.
|
||||
COPY . /usr/src/app
|
||||
|
||||
# Store the current git revision.
|
||||
ARG REVISION_HASH
|
||||
RUN mkdir -p dist/core/common/__generated__ && \
|
||||
echo "{\"revision\": \"${REVISION_HASH}\"}" > dist/core/common/__generated__/revision.json
|
||||
|
||||
# Install build static assets and clear caches.
|
||||
RUN NODE_ENV=development npm install && \
|
||||
npm run generate && \
|
||||
npm run build && \
|
||||
npm prune --production
|
||||
|
||||
# Setup the environment
|
||||
ENV PATH /usr/src/app/bin:$PATH
|
||||
ENV PORT 5000
|
||||
EXPOSE 5000
|
||||
ENV NODE_ENV production
|
||||
|
||||
CMD ["npm", "run", "start"]
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
FROM coralproject/talk:latest
|
||||
|
||||
# Bundle app source
|
||||
ONBUILD COPY . /usr/src/app
|
||||
|
||||
# At this stage, we need to install the development dependancies again because
|
||||
# we need to have webpack available. We then build the new dependancies and
|
||||
# clear out the development dependancies again. After this we of course need to
|
||||
# clear out the yarn cache, this saves quite a lot of size.
|
||||
ONBUILD RUN NODE_ENV=development yarn install --frozen-lockfile && \
|
||||
NODE_ENV=production cli plugins reconcile && \
|
||||
NODE_ENV=production yarn build && \
|
||||
NODE_ENV=production yarn install --production --force && \
|
||||
yarn cache clean
|
||||
-275
@@ -1,275 +0,0 @@
|
||||
## Contents
|
||||
|
||||
- [Installation](#installation) - install the application on a machine
|
||||
- [Via Docker](#installation-from-docker)
|
||||
- [Via Source](#installation-from-source)
|
||||
- [Setup](#setup) - setup the application for first use
|
||||
- [Usage](#usage) - connect the application to a website
|
||||
|
||||
# Installation
|
||||
|
||||
## Requirements
|
||||
|
||||
- Any flavour of Linux, OSX or Windows
|
||||
- 1GB memory (minimum)
|
||||
- 5GB storage (minimum)
|
||||
- [MongoDB](https://www.mongodb.com/) v3.4 or later
|
||||
- [Redis](https://redis.io/) v3.2 or later
|
||||
- SSL Certificate
|
||||
- This application assumes that you will be serving this application in a
|
||||
production environment, and therefore requires that you serve it behind a
|
||||
webserver with a valid SSL certificate. This is chosen in order to secure
|
||||
user's sessions.
|
||||
|
||||
## Installation From Docker
|
||||
|
||||
We currently support packaging the Talk application via Docker, which automates
|
||||
the dependency install and asset build process. This is the recommended way to
|
||||
deploy the application when used in production.
|
||||
|
||||
Available as [coralproject/talk](https://hub.docker.com/r/coralproject/talk/) on Docker Hub.
|
||||
|
||||
Images are tagged using the following notation:
|
||||
|
||||
- `x` (where `x` is the major version number): any minor or patch updates will be included in this. If you're ok getting
|
||||
new features occasionally and all the bug fixes, this is the tag for you.
|
||||
- `x.y` (where `y` is the minor version number): any patch updates will be
|
||||
included with this tag. If you like getting fixes and having features change
|
||||
only when you want, this is the tag for you. **(recommended)**
|
||||
- `x.y.z` (where `z` is the patch version): this tag never gets updated, and
|
||||
essentially freezes your version, this should only be used when you are either
|
||||
extending Talk or are sure of a specific version you want to freeze.
|
||||
|
||||
We provide tags with `*-onbuild` that can be used for easy plugin integration and
|
||||
acts as a customization endpoint. Instructions are provided in the `PLUGINS.md`
|
||||
document as to how to use it.
|
||||
|
||||
### Requirements
|
||||
|
||||
There are some runtime requirements for running Talk for Docker:
|
||||
|
||||
- [Docker](https://www.docker.com/) v1.13.0 or later
|
||||
- [Docker Compose](https://docs.docker.com/compose/) v1.10.0 or later
|
||||
|
||||
_Please be sure to check the versions of these requirements. Incorrect versions
|
||||
of these may lead to unexpected errors!_
|
||||
|
||||
### Installing
|
||||
|
||||
An example docker-compose.yml:
|
||||
|
||||
```yaml
|
||||
version: '2'
|
||||
services:
|
||||
talk:
|
||||
image: coralproject/talk:1.5
|
||||
restart: always
|
||||
ports:
|
||||
- "5000:5000"
|
||||
depends_on:
|
||||
- mongo
|
||||
- redis
|
||||
environment:
|
||||
- TALK_MONGO_URL=mongodb://mongo/talk
|
||||
- TALK_REDIS_URL=redis://redis
|
||||
mongo:
|
||||
image: mongo:3.2
|
||||
restart: always
|
||||
volumes:
|
||||
- mongo:/data/db
|
||||
redis:
|
||||
image: redis:3.2
|
||||
restart: always
|
||||
volumes:
|
||||
- redis:/data
|
||||
volumes:
|
||||
mongo:
|
||||
external: false
|
||||
redis:
|
||||
external: false
|
||||
```
|
||||
|
||||
At this stage, you should refer to the `README.md` for configuration variables
|
||||
that are specific to your installation. Some pre-defined fields have been filled
|
||||
in the above example which are consistent with Docker Compose naming conventions
|
||||
for [Docker Links](https://docs.docker.com/compose/networking/#links).
|
||||
|
||||
### Scaling
|
||||
|
||||
If you are interested in splitting apart services, you can simply adjust the
|
||||
command being executed in the container to optimize for your use case. An
|
||||
example would be if you wanted to run the API server and the job processor
|
||||
on different machines. You can achieve this easily with docker compose:
|
||||
|
||||
```yaml
|
||||
version: '2'
|
||||
services:
|
||||
talk-api:
|
||||
image: coralproject/talk:1.5
|
||||
command: cli serve
|
||||
restart: always
|
||||
ports:
|
||||
- "5000:5000"
|
||||
depends_on:
|
||||
- mongo
|
||||
- redis
|
||||
environment:
|
||||
- TALK_MONGO_URL=mongodb://mongo/talk
|
||||
- TALK_REDIS_URL=redis://redis
|
||||
talk-jobs:
|
||||
image: coralproject/talk:1.5
|
||||
command: cli jobs process
|
||||
restart: always
|
||||
ports:
|
||||
- "5001:5000"
|
||||
depends_on:
|
||||
- mongo
|
||||
- redis
|
||||
environment:
|
||||
- TALK_MONGO_URL=mongodb://mongo/talk
|
||||
- TALK_REDIS_URL=redis://redis
|
||||
mongo:
|
||||
image: mongo:3.2
|
||||
restart: always
|
||||
volumes:
|
||||
- mongo:/data/db
|
||||
redis:
|
||||
image: redis:3.2
|
||||
restart: always
|
||||
volumes:
|
||||
- redis:/data
|
||||
volumes:
|
||||
mongo:
|
||||
external: false
|
||||
redis:
|
||||
external: false
|
||||
```
|
||||
|
||||
Note that the only difference is in the `command` key. From this, you are able
|
||||
to discretely control which modules are running in order to have the maximum
|
||||
flexibility when managing your application.
|
||||
|
||||
### Running
|
||||
|
||||
If you're using docker compose:
|
||||
|
||||
```bash
|
||||
# Start the services using compose
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
If you're using plain docker:
|
||||
|
||||
```bash
|
||||
docker run -d -P coralproject/talk:latest
|
||||
```
|
||||
|
||||
## Installation From Source
|
||||
|
||||
This provides information on how to setup the application from source. Note that
|
||||
this is not recommended for production deploys, but will work for development
|
||||
and testing purposes.
|
||||
|
||||
### Requirements
|
||||
|
||||
There are some runtime requirements for running Talk from source:
|
||||
|
||||
- [Node](https://nodejs.org/) ~7.8
|
||||
- [Yarn](https://yarnpkg.com/) ^0.22.0
|
||||
|
||||
_Please be sure to check the versions of these requirements. Incorrect versions
|
||||
of these may lead to unexpected errors!_
|
||||
|
||||
### Installing
|
||||
|
||||
#### Download
|
||||
|
||||
It is highly recommended that you download a released version as the code
|
||||
available in `master` may not be stable. You can download the latest release
|
||||
from the [releases page](https://github.com/coralproject/talk/releases).
|
||||
|
||||
You can also clone the git repository via:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/coralproject/talk.git
|
||||
```
|
||||
|
||||
#### Building
|
||||
|
||||
We now have to install the dependencies and build the static assets.
|
||||
|
||||
```bash
|
||||
# Install package dependancies
|
||||
yarn
|
||||
|
||||
# Build static files
|
||||
yarn build
|
||||
```
|
||||
|
||||
After you create/modify the `plugins.json` (refer to `PLUGINS.md` for plugin
|
||||
docs) file, you can re-run the following to install their dependencies:
|
||||
|
||||
```bash
|
||||
# Reconcile plugins
|
||||
./bin/cli plugins reconcile
|
||||
|
||||
# Build static files
|
||||
yarn build
|
||||
```
|
||||
|
||||
### Running
|
||||
|
||||
Refer to the `README.md` file for required configuration variables to add to the
|
||||
environment.
|
||||
|
||||
You can start the server after configuring the server using the command:
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
```
|
||||
|
||||
This will setup the server to serve everything on a single node.js process and
|
||||
is designed to be used in production.
|
||||
|
||||
You can see other scripts we've made available by consulting the `package.json`
|
||||
file under the `scripts` key including:
|
||||
|
||||
- `yarn test` run unit tests
|
||||
- `yarn e2e` run end to end tests
|
||||
- `yarn build-watch` watch for changes to client files and build static assets
|
||||
- `yarn dev-start` watch for changes to server files and reload the server while
|
||||
also sourcing a `.env` file in your local directory for configuration
|
||||
|
||||
# Setup
|
||||
|
||||
Once you've installed Talk (either via Docker or source), you still need to
|
||||
setup the application. If you are unfamiliar with any terminology used in the
|
||||
setup process, refer to the `TERMINOLOGY.md` document.
|
||||
|
||||
## Via Web
|
||||
|
||||
If you want to perform your setup via the web, you can navigate to your
|
||||
installation of Talk at the path `/admin/install`. There you will be asked a
|
||||
series of questions for your installation.
|
||||
|
||||
## Via CLI
|
||||
|
||||
If you want to perform your setup through the terminal, you can simply run:
|
||||
|
||||
```bash
|
||||
cli setup
|
||||
```
|
||||
|
||||
And follow the instructions to perform initial setup and create your first user
|
||||
account.
|
||||
|
||||
|
||||
# Usage
|
||||
|
||||
After setup is complete, you can then refer to the `/admin/configure` path to
|
||||
get the embed code that you can copy/paste onto your blog or website in order to
|
||||
start using Talk.
|
||||
|
||||
_In order for the embed to work correctly, you will need to whitelist the domain
|
||||
that is allowed to embed your site on the `/admin/configure` page, failure to do
|
||||
so will result in the comment stream not loading._
|
||||
@@ -1,11 +1,15 @@
|
||||
Copyright 2017 Mozilla Foundation
|
||||
Copyright 2019 Vox Media, Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
|
||||
either express or implied.
|
||||
|
||||
See the License for the specific language governing permissions and limitations under the License.
|
||||
See the License for the specific language governing permissions
|
||||
and limitations under the License.
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# Talk Plugins
|
||||
|
||||
Our documentation for Talk has moved! Utilize [this hyperlink](https://coralproject.github.io/talk/plugins.html) via click or tap to navigate your browser to the new location!
|
||||
@@ -1,116 +1,244 @@
|
||||
# Talk [](https://circleci.com/gh/coralproject/talk)
|
||||
# talk 
|
||||
|
||||
Online comments are broken. Our open-source Talk tool 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)
|
||||
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/talk).
|
||||
|
||||
Third party licenses are available via the `/client/3rdpartylicenses.txt`
|
||||
endpoint when the server is running with built assets.
|
||||
Built with <3 by The Coral Project, a part of [Vox Media](https://product.voxmedia.com/).
|
||||
|
||||
## Contributing to Talk
|
||||
Preview Talk easily by running Talk via a Heroku App:
|
||||
|
||||
See our [Contribution Guide](https://github.com/coralproject/talk/blob/master/CONTRIBUTING.md).
|
||||
[](https://heroku.com/deploy?template=https://github.com/coralproject/talk/tree/next)
|
||||
|
||||
## Documentation
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table of Contents
|
||||
|
||||
### General
|
||||
- [Requirements](#requirements)
|
||||
- [Running](#running)
|
||||
- [Docker](#docker)
|
||||
- [Source](#source)
|
||||
- [Development](#development)
|
||||
- [Design Language System (UI Components)](#design-language-system-ui-components)
|
||||
- [Configuration](#configuration)
|
||||
- [License](#license)
|
||||
|
||||
See our [Talk Documentation & Guides](https://coralproject.github.io/talk/index.html).
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
### Plugins
|
||||
## Requirements
|
||||
|
||||
See our guide to using and building [Talk Plugins](https://github.com/coralproject/talk/blob/master/PLUGINS.md).
|
||||
- MongoDB >=3.6
|
||||
- Redis >=3.2
|
||||
- NodeJS >=10
|
||||
- NPM >=6.7
|
||||
|
||||
## Usage
|
||||
## Running
|
||||
|
||||
### Installation
|
||||
You can install Talk using Docker or via Source. We recommend Docker, as it
|
||||
provides the easiest deployment solution going forward, as all the dependencies
|
||||
are baked and shipped with the provided
|
||||
[coralproject/talk:next](https://hub.docker.com/r/coralproject/talk) image.
|
||||
When v5 releases to master, you'll be able to select it using
|
||||
`coralproject/talk:5`.
|
||||
|
||||
To set up a development environment or build from source, see [INSTALL.md](https://github.com/coralproject/talk/blob/master/INSTALL.md).
|
||||
### Docker
|
||||
|
||||
To launch a Talk server of your own from your browser without any need to muck about in a terminal or think about engineering concepts, stay tuned. We will launch [our installer](https://github.com/coralproject/talk-install) shortly!
|
||||
The easiest way to get started with Talk is through our published Docker image
|
||||
and provided example `docker-compose.yml` file. The following assumes that you
|
||||
have Docker and Docker Compose installed on your local machine:
|
||||
|
||||
### Configuration
|
||||
- Install Docker: https://docs.docker.com/install/
|
||||
- Install Docker Compose: https://docs.docker.com/compose/install/ (this is typically included in the Docker Desktop editions already)
|
||||
|
||||
The Talk application looks for the following configuration values either as environment variables:
|
||||
```bash
|
||||
# Create directories to persist the data in MongoDB and Redis.
|
||||
mkdir -p data/{mongo,redis}
|
||||
|
||||
- `TALK_MONGO_URL` (*required*) - the database connection string for the MongoDB database.
|
||||
- `TALK_REDIS_URL` (*required*) - the database connection string for the Redis database.
|
||||
- `TALK_ROOT_URL` (*required*) - root url of the installed application externally
|
||||
available in the format: `<scheme>://<host>` without the path.
|
||||
- `TALK_JWT_SECRET` (*required*) - a long and cryptographical secure random string which will be used to
|
||||
sign and verify tokens via a `HS256` algorithm.
|
||||
- `TALK_JWT_EXPIRY` (_optional_) - the expiry duration (`exp`) for the tokens issued for logged in sessions (Default `1 day`)
|
||||
- `TALK_JWT_ISSUER` (_optional_) - the issuer (`iss`) claim for login JWT tokens (Default `process.env.TALK_ROOT_URL`)
|
||||
- `TALK_JWT_AUDIENCE` (_optional_) - the audience (`aud`) claim for login JWT tokens (Default `talk`)
|
||||
- `TALK_SMTP_EMAIL` (*required for email*) - the address to send emails from using the
|
||||
SMTP provider.
|
||||
- `TALK_SMTP_USERNAME` (*required for email*) - username of the SMTP provider you are using.
|
||||
- `TALK_SMTP_PASSWORD` (*required for email*) - password for the SMTP provider you are using.
|
||||
- `TALK_SMTP_HOST` (*required for email*) - SMTP host url with format `smtp.domain.com`.
|
||||
- `TALK_SMTP_PORT` (*required for email*) - SMTP port.
|
||||
- `TALK_INSTALL_LOCK` (_optional for dynamic setup_) - Defaults to `FALSE`. When `TRUE`, disables the dynamic setup endpoint.
|
||||
- `TALK_RECAPTCHA_SECRET` (*required for reCAPTCHA support*) - server secret used for enabling reCAPTCHA powered logins. If not provided it will instead default to providing only a time based lockout.
|
||||
- `TALK_RECAPTCHA_PUBLIC` (*required for reCAPTCHA support*) - client secret used for enabling reCAPTCHA powered logins. If not provided it will instead default to providing only a time based lockout.
|
||||
- `TALK_PLUGINS_JSON` (_optional_) - used to specify the plugin config via the environment
|
||||
- `TALK_KEEP_ALIVE` (_optional_) - The keepalive timeout that should be used to send keep alive messages through the websocket to keep the socket alive. (Default `30s`)
|
||||
# Create the `docker-compose.yml` file to get started.
|
||||
cat > docker-compose.yml <<EOF
|
||||
version: "2"
|
||||
services:
|
||||
talk:
|
||||
image: coralproject/talk:next
|
||||
restart: always
|
||||
ports:
|
||||
- "127.0.0.1:3000:5000"
|
||||
depends_on:
|
||||
- mongo
|
||||
- redis
|
||||
environment:
|
||||
- MONGODB_URI=mongodb://mongo:27017/talk
|
||||
- REDIS_URI=redis://redis:6379
|
||||
- SIGNING_SECRET=<replace me with something secret>
|
||||
mongo:
|
||||
image: mongo:3.6
|
||||
volumes:
|
||||
- ./data/mongo:/data/db
|
||||
redis:
|
||||
image: redis:3.2
|
||||
volumes:
|
||||
- ./data/redis:/data
|
||||
EOF
|
||||
|
||||
Refer to the wiki page on [Configuration Loading](https://github.com/coralproject/talk/wiki/Configuration-Loading) for
|
||||
alternative methods of loading configuration during development.
|
||||
# Start up Talk using Docker.
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Running Migrations
|
||||
Then head on over to http://localhost:3000 to install Talk!
|
||||
|
||||
We have a migration tool that can be run using `bin/cli migration run`. This will detect new migrations available and prompt you to backup your database before proceeding with the migration. Migrations are required with major version releases.
|
||||
### Source
|
||||
|
||||
### Using Trust
|
||||
Talk requires NodeJS >=10, we recommend using `nvm` to help manage node
|
||||
versions: https://github.com/creationix/nvm.
|
||||
|
||||
Talk ships with core components we call "Trust". This allows Talk to automate certain actions based on previous user behavior.
|
||||
```bash
|
||||
# Clone and cd into the Talk directory.
|
||||
git clone https://github.com/coralproject/talk.git
|
||||
cd talk
|
||||
|
||||
Our first feature is the notion of Karma. Talk will automatically pre-moderate comments of users who have a negative karma score. You can [see more how karma works here](/services/karma.js).
|
||||
# Install dependencies.
|
||||
npm install
|
||||
|
||||
## Supported Browsers
|
||||
# Build the application dependencies.
|
||||
# This might take a while.
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Web
|
||||
This should output all the compiled application code to `./dist`.
|
||||
|
||||
- Chrome: latest 2 versions
|
||||
- Firefox: latest 2 versions, and most recent extended support version, if any
|
||||
- Safari: latest 2 versions
|
||||
- Internet Explorer: IE Edge, 11
|
||||
Running Talk with default settings assumes that you have:
|
||||
|
||||
### iOS Devices
|
||||
- MongoDB >=3.6 running on `127.0.0.1:27017`
|
||||
- Redis >=3.2 running on `127.0.0.1:6379`
|
||||
|
||||
- iPad
|
||||
- iPad Pro
|
||||
- iPhone 7 Plus
|
||||
- iPhone 7
|
||||
- iPhone 6 Plus
|
||||
- iPhone 6
|
||||
- iPhone 5
|
||||
If you don't already have these databases running, you can execute the following
|
||||
assuming you have Docker installed on your local machine:
|
||||
|
||||
### iOS Browsers
|
||||
```bash
|
||||
docker run -d -p 27017:27017 --restart always --name mongo mongo:3.6
|
||||
docker run -d -p 6379:6379 --restart always --name redis redis:3.2
|
||||
```
|
||||
|
||||
- Chrome for iOS: latest version
|
||||
- Firefox for iOS: latest version
|
||||
- Safari for iOS: latest version
|
||||
Then start Talk with:
|
||||
|
||||
### Android Devices
|
||||
```bash
|
||||
# Start the server in production mode.
|
||||
npm run start
|
||||
```
|
||||
|
||||
- Galaxy S5
|
||||
- Nexus 5X
|
||||
- Nexus 6P
|
||||
Then head on over to http://localhost:3000 to install Talk!
|
||||
|
||||
### Android Browsers
|
||||
### Development
|
||||
|
||||
- Chrome for Android: latest version
|
||||
- Firefox for Android: latest version
|
||||
Running Talk for development is very similar to installing Talk via Source as
|
||||
described above.
|
||||
|
||||
Talk requires NodeJS >=10, we recommend using `nvm` to help manage node
|
||||
versions: https://github.com/creationix/nvm.
|
||||
|
||||
```bash
|
||||
# Clone and cd into the Talk directory.
|
||||
git clone https://github.com/coralproject/talk.git
|
||||
cd talk
|
||||
|
||||
# Install dependencies.
|
||||
npm install
|
||||
```
|
||||
|
||||
Running Talk with default settings assumes that you have:
|
||||
|
||||
- MongoDB >=3.6 running on `127.0.0.1:27017`
|
||||
- Redis >=3.2 running on `127.0.0.1:6379`
|
||||
|
||||
If you don't already have these databases running, you can execute the following
|
||||
assuming you have Docker installed on your local machine:
|
||||
|
||||
```bash
|
||||
docker run -d -p 27017:27017 --restart always --name mongo mongo:3.6
|
||||
docker run -d -p 6379:6379 --restart always --name redis redis:3.2
|
||||
```
|
||||
|
||||
Then start Talk with:
|
||||
|
||||
```bash
|
||||
# Run the server in development mode in order to facilitate auto-restarting and
|
||||
# rebuilding when file changes are detected. This might take a while to fully run.
|
||||
npm run watch
|
||||
```
|
||||
|
||||
When the client code has been built, navigate to http://localhost:8080/install.html
|
||||
to start the installation wizard. **Note: Ensure `localhost:8080` is used in the permitted domains list.**
|
||||
|
||||
To see the comment stream goto http://localhost:8080/.
|
||||
|
||||
To run linting and tests use the following commands:
|
||||
|
||||
```bash
|
||||
# Run the linters.
|
||||
npm run lint
|
||||
|
||||
# Run our unit and integration tests.
|
||||
npm run test
|
||||
```
|
||||
|
||||
#### Design Language System (UI Components)
|
||||
|
||||
We use [docz](https://docz.site) to document and develop our Design Language System. To start docz run:
|
||||
|
||||
```bash
|
||||
# Make sure CSS types are generated.
|
||||
# This is not required when `npm run watch` is already running.
|
||||
npm run generate:css-types
|
||||
|
||||
# Run docz in development.
|
||||
npm run docz -- dev
|
||||
```
|
||||
|
||||
After compilation has finished you can access docz at http://localhost:3030/.
|
||||
|
||||
## Configuration
|
||||
|
||||
The following environment variables can be set to configure the Talk Server. You
|
||||
can expose them in your shell via `export NODE_ENV=development` or by placing
|
||||
the variables in a `.env` file in the root of the project in a simple
|
||||
`NODE_ENV=development` format delimited by newlines.
|
||||
|
||||
- `NODE_ENV` - Can be one of `production` or `development`. All production
|
||||
deployments should use `production`. Defaults to `production` when ran with
|
||||
`npm run start` and `development` when run with `npm run start:development`.
|
||||
- `PORT` - The port to listen for HTTP and Websocket requests. (Default `3000`)
|
||||
- `MONGODB_URI` - The MongoDB database URI to connect to.
|
||||
(Default `mongodb://127.0.0.1:27017/talk`)
|
||||
- `REDIS_URI` - The Redis database URI to connect to.
|
||||
(Default `redis://127.0.0.1:6379`)
|
||||
- `SIGNING_SECRET` - The shared secret to use to sign JSON Web Tokens (JWT) with
|
||||
the selected signing algorithm. 🚨 **Don't forget to set this variable!** 🚨
|
||||
(Default: `keyboard cat`)
|
||||
- `SIGNING_ALGORITHM` - The signing algorithm to use for signing JWT's.
|
||||
(Default `HS256`).
|
||||
- `LOGGING_LEVEL` - The logging level that can be set to one of `fatal`,
|
||||
`error`, `warn`, `info`, `debug`, or `trace`. (Default `info`)
|
||||
- `STATIC_URI` - The URI that static assets can be accessed from. This URI can
|
||||
be to a proxy that uses this Talk server on `PORT` as the upstream. Disabled
|
||||
by default.
|
||||
- `DISABLE_TENANT_CACHING` - When `true`, all tenants will be loaded from the
|
||||
database when needed rather than keeping a in-memory copy in sync via
|
||||
published events on Redis. (Default `false`)
|
||||
- `DISABLE_MONGODB_AUTOINDEXING` - When `true`, Talk will not perform indexing
|
||||
operations when it starts up. This can be desired when you've already
|
||||
installed Talk on the target MongoDB, but want to improve start performance.
|
||||
(Default `false`)
|
||||
- `LOCALE` - Specify the default locale to use for all requests without a locale
|
||||
specified. (Default `en-US`)
|
||||
- `ENABLE_GRAPHIQL` - When `true`, it will enable the `/graphiql` even in
|
||||
production, use with care. (Default `false`)
|
||||
- `CONCURRENCY` - The number of worker nodes to spawn to handle web traffic,
|
||||
this should be tied to the number of CPU's available. (Default
|
||||
`os.cpus().length`)
|
||||
- `DEV_PORT` - The port where the Webpack Development server is running on.
|
||||
(Default `8080`)
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2017 Mozilla Foundation
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
||||
See the License for the specific language governing permissions and limitations under the License.
|
||||
Talk is released under the [Apache License, v2.0](/LICENSE).
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
# Product's Terminology
|
||||
|
||||
This is a guide to have a common language to talk about "Talk".
|
||||
|
||||
## Definitions
|
||||
|
||||
* Site - a top level site, aka nytimes.com
|
||||
* Section - the section of a site, aka, Politics.
|
||||
* Subsection - the section of a site, aka, Politics.
|
||||
* Asset - An article/video/etc identified by URL.
|
||||
|
||||
* Embed - Things we put on a asset: comment box, ToS, Stream, etc…
|
||||
* Stream - All the activity on a certain asset. Container for Comments, actions, user
|
||||
* Thread - defined by a parent and everything below. All replies to a comment and their replies, etc…
|
||||
* Comment - a kind of user-generated content submitted by a comment author
|
||||
* A parent comment has replies to it
|
||||
* A child comments is a reply to another comment
|
||||
* A comment can be both a parent comment and a child of another comment
|
||||
* A top-level comment is a comment that is not a reply to any other comment
|
||||
* A nth-level comment refers to the number of replies away from the top-level comment
|
||||
|
||||
* User - an item to represent a person using Talk. It could be a moderator, reader, etc.
|
||||
* User Roles:
|
||||
* Active: some who takes action (logged in or not)
|
||||
* Passive: some who just reads, no actions performed
|
||||
* Comment Author: The user who wrote the comment
|
||||
* Staff Member: someone who works for an organization (tagged for leverage in trust)
|
||||
* Moderator: someone with the ability to access the moderation queue and perform moderation actions
|
||||
* Administrator: has the ability to change the setup of their coral space
|
||||
* Public Profile: information about users shown in public
|
||||
* Private Profile: information about users shown only to user about themselves
|
||||
* Protected Profile: information about users that only moderators and admins can see
|
||||
|
||||
* Queue - Group of items based on a query, aka - moderation queue
|
||||
* Target - The item/s on which an action is performed
|
||||
|
||||
## Actions
|
||||
|
||||
Actions are performed by users on items. Actions themselves are items. This requires two relationships: action on item, and user performs action.
|
||||
|
||||
### Flag
|
||||
* A Flagger(user) performs a Flag
|
||||
* A Flag is performed on a Comment or a username or profile content
|
||||
|
||||
|
||||
## Moderation Actions and Status
|
||||
|
||||
Comments contain a field `status`. As moderation actions are peformed, the status changes.
|
||||
|
||||
* Initial status is empty.
|
||||
* When a moderator Approves, the status is set to 'approved'.
|
||||
* When a moderator Rejects, the status is set to 'reject'.
|
||||
|
||||
### Pre and post moderation
|
||||
|
||||
Comments can be set to be premoderated or postmoderated.
|
||||
|
||||
Premoderation means that moderation has to occur _before_ a comment is shown on the site:
|
||||
|
||||
* New comments are shown in the moderator queues immediately.
|
||||
* The are not shown to users until (aka in streams) until they are approved by a moderator.
|
||||
|
||||
Postmoderation means that comments appear on the site _before_ any moderation action is taken.
|
||||
|
||||
* New comments appear in comment streams immediately.
|
||||
* New comments do not appear in moderation queues unless they are flagged by other users.
|
||||
|
||||
### Word lists
|
||||
|
||||
* Banned words - words that the site never allows in a comment
|
||||
* Suspect words - words whose usage needs to be approved by a moderator before being shown in the stream
|
||||
* Approved words - words that are usually Banned or Suspect sitewide, but approved for use in a specific article stream
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const morgan = require('morgan');
|
||||
const path = require('path');
|
||||
const helmet = require('helmet');
|
||||
const authentication = require('./middleware/authentication');
|
||||
const {passport} = require('./services/passport');
|
||||
const plugins = require('./services/plugins');
|
||||
const i18n = require('./services/i18n');
|
||||
const enabled = require('debug').enabled;
|
||||
const errors = require('./errors');
|
||||
const {createGraphOptions} = require('./graph');
|
||||
const apollo = require('graphql-server-express');
|
||||
const accepts = require('accepts');
|
||||
const compression = require('compression');
|
||||
const cookieParser = require('cookie-parser');
|
||||
|
||||
const app = express();
|
||||
|
||||
// Middleware declarations.
|
||||
|
||||
// Add the logging middleware only if we aren't testing.
|
||||
if (app.get('env') !== 'test') {
|
||||
app.use(morgan('dev'));
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// APP MIDDLEWARE
|
||||
//==============================================================================
|
||||
|
||||
app.set('trust proxy', 1);
|
||||
|
||||
// We disable frameward on helmet to allow crossdomain injection of the embed
|
||||
app.use(helmet({
|
||||
frameguard: false
|
||||
}));
|
||||
app.use(compression());
|
||||
app.use(cookieParser());
|
||||
app.use(bodyParser.json());
|
||||
|
||||
//==============================================================================
|
||||
// STATIC FILES
|
||||
//==============================================================================
|
||||
|
||||
// If the application is in production mode, then add gzip rewriting for the
|
||||
// content.
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
app.get('*.js', (req, res, next) => {
|
||||
const accept = accepts(req);
|
||||
if (accept.encoding(['gzip']) === 'gzip') {
|
||||
|
||||
// Adjsut the headers on the request by adding a content type header
|
||||
// because express won't be able to detect the mime-type with the .gz
|
||||
// extension and we need to decalre support for the gzip encoding.
|
||||
res.set('Content-Type', 'application/javascript');
|
||||
res.set('Content-Encoding', 'gzip');
|
||||
|
||||
// Rewrite the url so that the gzip version will be served instead.
|
||||
req.url = `${req.url}.gz`;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
app.use('/client', express.static(path.join(__dirname, 'dist')));
|
||||
app.use('/public', express.static(path.join(__dirname, 'public')));
|
||||
|
||||
//==============================================================================
|
||||
// VIEW CONFIGURATION
|
||||
//==============================================================================
|
||||
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
//==============================================================================
|
||||
// PASSPORT MIDDLEWARE
|
||||
//==============================================================================
|
||||
|
||||
const passportDebug = require('debug')('talk:passport');
|
||||
|
||||
// Install the passport plugins.
|
||||
plugins.get('server', 'passport').forEach((plugin) => {
|
||||
passportDebug(`added plugin '${plugin.plugin.name}'`);
|
||||
|
||||
// Pass the passport.js instance to the plugin to allow it to inject it's
|
||||
// functionality.
|
||||
plugin.passport(passport);
|
||||
});
|
||||
|
||||
// Setup the PassportJS Middleware.
|
||||
app.use(passport.initialize());
|
||||
|
||||
// Attach the authentication middleware, this will be responsible for decoding
|
||||
// (if present) the JWT on the request.
|
||||
app.use('/api', authentication);
|
||||
|
||||
//==============================================================================
|
||||
// GraphQL Router
|
||||
//==============================================================================
|
||||
|
||||
// GraphQL endpoint.
|
||||
app.use('/api/v1/graph/ql', apollo.graphqlExpress(createGraphOptions));
|
||||
|
||||
// Only include the graphiql tool if we aren't in production mode.
|
||||
if (app.get('env') !== 'production') {
|
||||
|
||||
// Interactive graphiql interface.
|
||||
app.use('/api/v1/graph/iql', (req, res) => {
|
||||
res.render('graphiql', {
|
||||
endpointURL: '/api/v1/graph/ql'
|
||||
});
|
||||
});
|
||||
|
||||
// GraphQL documention.
|
||||
app.get('/admin/docs', (req, res) => {
|
||||
res.render('admin/docs');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// ROUTES
|
||||
//==============================================================================
|
||||
|
||||
app.use('/', require('./routes'));
|
||||
|
||||
//==============================================================================
|
||||
// ERROR HANDLING
|
||||
//==============================================================================
|
||||
|
||||
// Catch 404 and forward to error handler.
|
||||
app.use((req, res, next) => {
|
||||
next(errors.ErrNotFound);
|
||||
});
|
||||
|
||||
// General error handler. Respond with the message and error if we have it while
|
||||
// returning a status code that makes sense.
|
||||
app.use('/api', (err, req, res, next) => {
|
||||
if (err !== errors.ErrNotFound) {
|
||||
if (app.get('env') !== 'test' || enabled('talk:errors')) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (err instanceof errors.APIError) {
|
||||
res.status(err.status).json({
|
||||
message: err.message,
|
||||
error: err
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({});
|
||||
}
|
||||
});
|
||||
|
||||
app.use('/', (err, req, res, next) => {
|
||||
if (err !== errors.ErrNotFound) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
i18n.init(req);
|
||||
|
||||
if (err instanceof errors.APIError) {
|
||||
res.status(err.status);
|
||||
res.render('error', {
|
||||
message: err.message,
|
||||
error: app.get('env') === 'development' ? err : {}
|
||||
});
|
||||
} else {
|
||||
res.render('error', {
|
||||
message: err.message,
|
||||
error: app.get('env') === 'development' ? err : {}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
@@ -1,33 +1,40 @@
|
||||
{
|
||||
"name": "The Coral Project: Talk",
|
||||
"env": {
|
||||
"TALK_SESSION_SECRET": {
|
||||
"description": "The session secret",
|
||||
"generator": "secret"
|
||||
},
|
||||
"TALK_FACEBOOK_APP_ID": {
|
||||
"value": "",
|
||||
"required": true
|
||||
},
|
||||
"TALK_FACEBOOK_APP_SECRET": {
|
||||
"value": "",
|
||||
"required": true
|
||||
},
|
||||
"NODE_ENV": "production",
|
||||
"TALK_SMTP_PORT": "2525",
|
||||
"REWRITE_ENV": "TALK_PORT:PORT,TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:POSTMARK_SMTP_SERVER,TALK_SMTP_USERNAME:POSTMARK_API_TOKEN,TALK_SMTP_PASSWORD:POSTMARK_API_TOKEN",
|
||||
"NPM_CONFIG_PRODUCTION": "false"
|
||||
"name": "Talk",
|
||||
"description": "A better commenting experience from Vox Media.",
|
||||
"env": {
|
||||
"REWRITE_ENV": {
|
||||
"description": "Used to rewrite the environment variables set by Heroku.",
|
||||
"value": "REDIS_URI:REDIS_URL,MONGODB_URI:MONGO_URI"
|
||||
},
|
||||
"addons": [{
|
||||
"plan": "mongolab:sandbox",
|
||||
"as": "MONGO"
|
||||
}, {
|
||||
"plan": "rediscloud:30",
|
||||
"as": "REDIS"
|
||||
}, {
|
||||
"plan": "postmark:10k",
|
||||
"as": "POSTMARK"
|
||||
}],
|
||||
"image": "heroku/nodejs",
|
||||
"success_url": "/admin/install"
|
||||
"SIGNING_SECRET": {
|
||||
"description":
|
||||
"The shared secret to use to sign JSON Web Tokens (JWT) with the selected signing algorithm.",
|
||||
"generator": "secret"
|
||||
},
|
||||
"CONCURRENCY": {
|
||||
"description": "The number of worker nodes to spawn to handle traffic.",
|
||||
"value": "1"
|
||||
},
|
||||
"LOCALE": {
|
||||
"description":
|
||||
"Specify the default locale to use for all requests without a locale specified",
|
||||
"value": "en-US"
|
||||
},
|
||||
"ENABLE_GRAPHIQL": {
|
||||
"description": "When true, this will enable the GraphiQL routes",
|
||||
"value": "false"
|
||||
}
|
||||
},
|
||||
"addons": [
|
||||
{
|
||||
"plan": "mongolab:sandbox",
|
||||
"as": "MONGO"
|
||||
},
|
||||
{
|
||||
"plan": "rediscloud:30",
|
||||
"as": "REDIS"
|
||||
}
|
||||
],
|
||||
"success_url": "/install",
|
||||
"website": "https://github.com/coralproject/talk"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* This is a project wide babel configuration.
|
||||
* https://babeljs.io/docs/en/config-files#project-wide-configuration
|
||||
*
|
||||
* We use this file to apply babel configuration to packages in `node_modules`
|
||||
*/
|
||||
|
||||
const lodashOptimizations =
|
||||
process.env.WEBPACK === "true" ? ["use-lodash-es", "lodash"] : [];
|
||||
|
||||
module.exports = {
|
||||
env: {
|
||||
production: {
|
||||
plugins: [...lodashOptimizations],
|
||||
},
|
||||
development: {
|
||||
plugins: [...lodashOptimizations],
|
||||
},
|
||||
test: {
|
||||
presets: [
|
||||
[
|
||||
"@babel/env",
|
||||
{ targets: "last 2 versions, ie 11", modules: "commonjs" },
|
||||
],
|
||||
"@babel/react",
|
||||
],
|
||||
plugins: ["dynamic-import-node"],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
// const util = require('./util');
|
||||
const program = require('./commander');
|
||||
|
||||
program
|
||||
.command('serve', 'serve the application')
|
||||
.command('settings', 'interact with the application settings')
|
||||
.command('assets', 'interact with assets')
|
||||
.command('setup', 'setup the application')
|
||||
.command('jobs', 'work with the job queues')
|
||||
.command('token', 'work with the access tokens')
|
||||
.command('users', 'work with the application auth')
|
||||
.command('migration', 'provides utilities for migrating the database')
|
||||
.command(
|
||||
'plugins',
|
||||
'provides utilities for interacting with the plugin system'
|
||||
)
|
||||
.parse(process.argv);
|
||||
|
||||
/**
|
||||
* When this provess exists, check to see if we have a running command, if we do
|
||||
* check to see if it is still running. If it is, then kill it with a SIGINT
|
||||
* signal. This is for the use case where we want to kill the process that is
|
||||
* labled with the PID written out by the parent process.
|
||||
*/
|
||||
process.once('exit', () => {
|
||||
if (
|
||||
program.runningCommand.killed === false &&
|
||||
program.runningCommand.exitCode === null
|
||||
) {
|
||||
program.runningCommand.kill('SIGINT');
|
||||
}
|
||||
});
|
||||
-164
@@ -1,164 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const parseDuration = require('ms');
|
||||
const Table = require('cli-table');
|
||||
const AssetModel = require('../models/asset');
|
||||
const CommentModel = require('../models/comment');
|
||||
const AssetsService = require('../services/assets');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const scraper = require('../services/scraper');
|
||||
const util = require('./util');
|
||||
const inquirer = require('inquirer');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
/**
|
||||
* Lists all the assets registered in the database.
|
||||
*/
|
||||
async function listAssets() {
|
||||
try {
|
||||
let assets = await AssetModel.find({}).sort({'created_at': 1});
|
||||
|
||||
let table = new Table({
|
||||
head: [
|
||||
'ID',
|
||||
'Title',
|
||||
'URL'
|
||||
]
|
||||
});
|
||||
|
||||
assets.forEach((asset) => {
|
||||
table.push([
|
||||
asset.id,
|
||||
asset.title ? asset.title : '',
|
||||
asset.url ? asset.url : ''
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAssets(ageString) {
|
||||
try {
|
||||
const now = new Date().getTime();
|
||||
const ageMs = parseDuration(ageString);
|
||||
const age = new Date(now - ageMs);
|
||||
|
||||
let assets = await AssetModel.find({
|
||||
$or: [
|
||||
{
|
||||
scraped: {
|
||||
$lte: age
|
||||
}
|
||||
},
|
||||
{
|
||||
scraped: null
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Queue all the assets for scraping.
|
||||
await Promise.all(assets.map(scraper.create));
|
||||
|
||||
console.log('Assets were queued to be scraped');
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateURL(assetID, assetURL) {
|
||||
try {
|
||||
await AssetsService.updateURL(assetID, assetURL);
|
||||
|
||||
console.log(`Asset ${assetID} was updated to have url ${assetURL}.`);
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function merge(srcID, dstID) {
|
||||
try {
|
||||
|
||||
// Grab the assets...
|
||||
let [srcAsset, dstAsset] = await AssetsService.findByIDs([srcID, dstID]);
|
||||
if (!srcAsset || !dstAsset) {
|
||||
throw new Error('Not all assets indicated by id exist, cannot merge');
|
||||
}
|
||||
|
||||
// Count the affected resources...
|
||||
let srcCommentCount = await CommentModel.find({asset_id: srcID}).count();
|
||||
|
||||
console.log(`Now going to update ${srcCommentCount} comments and delete the source Asset[${srcID}].`);
|
||||
|
||||
let {confirm} = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: 'Proceed with merge',
|
||||
default: false
|
||||
}
|
||||
]);
|
||||
|
||||
if (confirm) {
|
||||
|
||||
// Perform the merge!
|
||||
await AssetsService.merge(srcID, dstID);
|
||||
} else {
|
||||
console.warn('Aborting merge');
|
||||
}
|
||||
|
||||
util.shutdown(0);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
program
|
||||
.command('list')
|
||||
.description('list all the assets in the database')
|
||||
.action(listAssets);
|
||||
|
||||
program
|
||||
.command('refresh <age>')
|
||||
.description('queues the assets that exceed the age requested')
|
||||
.action(refreshAssets);
|
||||
|
||||
program
|
||||
.command('update-url <assetID> <url>')
|
||||
.description('update the URL of an asset')
|
||||
.action(updateURL);
|
||||
|
||||
program
|
||||
.command('merge <srcID> <dstID>')
|
||||
.description('merges two assets together by moving comments from src to dst and deleting the src asset')
|
||||
.action(merge);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.outputHelp();
|
||||
util.shutdown();
|
||||
}
|
||||
-116
@@ -1,116 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const scraper = require('../services/scraper');
|
||||
const mailer = require('../services/mailer');
|
||||
const util = require('./util');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const kue = require('../services/kue');
|
||||
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
/**
|
||||
* Starts the job processor.
|
||||
*/
|
||||
function processJobs() {
|
||||
|
||||
// Start the scraper processor.
|
||||
scraper.process();
|
||||
|
||||
// Start the mail processor.
|
||||
mailer.process();
|
||||
|
||||
// The scraper only needs to shutdown when the scraper has actually been
|
||||
// started.
|
||||
util.onshutdown([
|
||||
() => kue.Task.shutdown()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a single job.
|
||||
* @param {Object} job the job to be removed
|
||||
* @return {Promise}
|
||||
*/
|
||||
function removeJob(job) {
|
||||
return new Promise((resolve, reject) => job.remove((err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve(job);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the jobs passed in and returns a promise.
|
||||
* @param {Array} jobs array of jobs
|
||||
* @return {Promise}
|
||||
*/
|
||||
function removeJobs(jobs) {
|
||||
return Promise.all(jobs.map(removeJob));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the top n jobs with a specific state.
|
||||
* @param {String} [state='complete'] state to list jobs by
|
||||
* @param {Number} limit limit of jobs to load
|
||||
* @return {Promise}
|
||||
*/
|
||||
function rangeJobsByState(state = 'complete', limit) {
|
||||
return new Promise((resolve, reject) => {
|
||||
kue.Job.rangeByState(state, 0, limit, 'asc', (err, jobs) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
resolve(jobs);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the jobs that are in the queue.
|
||||
*/
|
||||
function cleanupJobs(options) {
|
||||
const n = 100;
|
||||
|
||||
Promise.all([
|
||||
rangeJobsByState('complete', n),
|
||||
options.stuck ? rangeJobsByState('failed', n) : false
|
||||
])
|
||||
.then((joblists) => joblists.filter((jobs) => jobs).map(removeJobs))
|
||||
.then(() => {
|
||||
util.shutdown();
|
||||
console.log('Removed old jobs');
|
||||
});
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
program
|
||||
.command('process')
|
||||
.description('starts job processing')
|
||||
.action(processJobs);
|
||||
|
||||
program
|
||||
.command('cleanup')
|
||||
.option('-s, --stuck', 'cleans up jobs that have been stuck', false)
|
||||
.description('cleans up inactive jobs')
|
||||
.action(cleanupJobs);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
if (process.argv.length <= 2) {
|
||||
program.outputHelp();
|
||||
util.shutdown();
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const util = require('./util');
|
||||
const inquirer = require('inquirer');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const MigrationService = require('../services/migration');
|
||||
|
||||
// Register shutdown hooks.
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
async function createMigration(name) {
|
||||
try {
|
||||
|
||||
// Create the migration.
|
||||
await MigrationService.create(name);
|
||||
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function runMigrations() {
|
||||
|
||||
try {
|
||||
|
||||
let {backedUp} = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'backedUp',
|
||||
message: 'Did you perform a database backup',
|
||||
default: false
|
||||
}
|
||||
]);
|
||||
|
||||
if (!backedUp) {
|
||||
throw new Error('Please backup your databases prior to migrations occuring');
|
||||
}
|
||||
|
||||
// Get the migrations to run.
|
||||
let migrations = await MigrationService.listPending();
|
||||
|
||||
console.log('Now going to run the following migrations:\n');
|
||||
|
||||
for (let {filename} of migrations) {
|
||||
console.log(`\tmigrations/${filename}`);
|
||||
}
|
||||
|
||||
let {confirm} = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: 'Proceed with migrations',
|
||||
default: false
|
||||
}
|
||||
]);
|
||||
|
||||
if (confirm) {
|
||||
|
||||
// Run the migrations.
|
||||
await MigrationService.run(migrations);
|
||||
} else {
|
||||
console.warn('Skipping migrations');
|
||||
}
|
||||
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
program
|
||||
.command('create <name>')
|
||||
.description('creates a new migration')
|
||||
.action(createMigration);
|
||||
|
||||
program
|
||||
.command('run')
|
||||
.description('runs all pending migrations')
|
||||
.action(runMigrations);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
if (process.argv.length <= 2) {
|
||||
program.outputHelp();
|
||||
util.shutdown();
|
||||
}
|
||||
-298
@@ -1,298 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
// Interface heavily inspired by the yarn package manager:
|
||||
// https://yarnpkg.com/
|
||||
|
||||
const program = require('./commander');
|
||||
|
||||
// Make things colorful!
|
||||
require('colors');
|
||||
const emoji = require('node-emoji');
|
||||
|
||||
const dir = process.cwd();
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const spawn = require('cross-spawn');
|
||||
const semver = require('semver');
|
||||
const resolve = require('resolve');
|
||||
const {plugins, itteratePlugins, isInternal} = require('../plugins');
|
||||
|
||||
function existsInNodeModules(name) {
|
||||
try {
|
||||
resolve.sync(name, {basedir: dir});
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function versionMatch(name, version) {
|
||||
try {
|
||||
let matched = false;
|
||||
|
||||
resolve.sync(name, {
|
||||
basedir: dir,
|
||||
packageFilter: (pkg) => {
|
||||
if (pkg && pkg.version && semver.satisfies(pkg.version, version)) {
|
||||
matched = true;
|
||||
}
|
||||
|
||||
return pkg;
|
||||
}
|
||||
});
|
||||
|
||||
return matched;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const EXTERNAL = /^\w[a-z\-0-9\.]+$/; // Match "react", "path", "fs", "lodash.random", etc.
|
||||
|
||||
function reconcilePackages({quiet = false, upgradeRemote = false}) {
|
||||
const fetchable = [];
|
||||
const local = [];
|
||||
const upgradable = [];
|
||||
|
||||
if (!quiet) {
|
||||
console.log();
|
||||
console.log(' +local (l) packages in your project');
|
||||
console.log(' +external (e) packages are external');
|
||||
console.log(' +outofdate (oe) packages are external but are out of date');
|
||||
console.log(' +missing (m) packages are not found');
|
||||
console.log();
|
||||
}
|
||||
|
||||
for (let i in plugins) {
|
||||
let section = itteratePlugins(plugins[i]);
|
||||
|
||||
for (let j in section) {
|
||||
let {name, version} = section[j];
|
||||
|
||||
let namespaced = name.charAt(0) === '@';
|
||||
let dep = name.split('/')
|
||||
.slice(0, namespaced ? 2 : 1)
|
||||
.join('/');
|
||||
|
||||
// Ignore relative modules, which aren't installed by NPM
|
||||
if (!dep.match(EXTERNAL) && !namespaced) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInternal(dep)) {
|
||||
if (!quiet) {
|
||||
console.log(` l ${name}`);
|
||||
}
|
||||
|
||||
local.push({name, version});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!existsInNodeModules(dep)) {
|
||||
if (!quiet) {
|
||||
console.log(` m ${name}`);
|
||||
}
|
||||
fetchable.push({name, version});
|
||||
} else if (!versionMatch(dep, version)) {
|
||||
|
||||
// A plugin was found, yet the current version does not match the
|
||||
// current version installed. We should warn if upgradeRemote is
|
||||
// not enabled that it is currently not supported.
|
||||
if (!upgradeRemote) {
|
||||
if (!quiet) {
|
||||
console.warn(` oe ${name} (package upgrade may be required)`.bgRed);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(` oe ${name} (package upgrade may be required)`);
|
||||
|
||||
upgradable.push({name, version});
|
||||
} else {
|
||||
if (!quiet) {
|
||||
console.log(` e ${name}`);
|
||||
}
|
||||
|
||||
if (upgradeRemote) {
|
||||
upgradable.push({name, version});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!quiet) {
|
||||
console.log();
|
||||
}
|
||||
|
||||
return {local, fetchable, upgradable};
|
||||
}
|
||||
|
||||
async function reconcileRemotePlugins({skipLocal, dryRun, upgradeRemote}) {
|
||||
console.log(`\n[${skipLocal ? '1/2' : '2/3'}] ${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);
|
||||
|
||||
if (fetchable.length > 0) {
|
||||
|
||||
console.log(`$ yarn add --ignore-scripts ${fetchable.map(({name, version}) => `${name}@${version}`.cyan)}`);
|
||||
|
||||
if (!dryRun) {
|
||||
|
||||
let args = [
|
||||
'add',
|
||||
'--ignore-scripts',
|
||||
...fetchable.map(({name, version}) => `${name}@${version}`)
|
||||
];
|
||||
|
||||
let output = spawn.sync('yarn', args, {
|
||||
stdio: ['ignore', 'pipe', 'inherit']
|
||||
});
|
||||
|
||||
if (output.status) {
|
||||
throw new Error('Could not install external plugins, errors occured during install');
|
||||
}
|
||||
|
||||
console.log(output.stdout.toString());
|
||||
}
|
||||
}
|
||||
|
||||
if (upgradable.length > 0) {
|
||||
console.log(`$ yarn upgrade ${upgradable.map(({name, version}) => `${name}@${version}`.cyan)}`);
|
||||
|
||||
if (!dryRun) {
|
||||
|
||||
let args = [
|
||||
'upgrade',
|
||||
...upgradable.map(({name, version}) => `${name}@${version}`)
|
||||
];
|
||||
|
||||
let output = spawn.sync('yarn', args, {
|
||||
stdio: ['ignore', 'pipe', 'inherit']
|
||||
});
|
||||
|
||||
if (output.status) {
|
||||
throw new Error('Could not install external plugins, errors occured during install');
|
||||
}
|
||||
|
||||
console.log(output.stdout.toString());
|
||||
}
|
||||
}
|
||||
|
||||
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 occured 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}) {
|
||||
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) {
|
||||
let results = [];
|
||||
try {
|
||||
results = await reconcileRemotePlugins({skipLocal, skipRemote, dryRun, upgradeRemote});
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
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(2);
|
||||
console.log(`✨ Done in ${totalTime}s.`);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
program
|
||||
.command('list')
|
||||
.description('')
|
||||
.action(reconcilePackages);
|
||||
|
||||
program
|
||||
.command('reconcile')
|
||||
.description('reconciles local plugin dependencies and downloads 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);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.outputHelp();
|
||||
}
|
||||
-181
@@ -1,181 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const program = require('./commander');
|
||||
const app = require('../app');
|
||||
const debug = require('debug')('talk:cli:serve');
|
||||
const errors = require('../errors');
|
||||
const {createServer} = require('http');
|
||||
const scraper = require('../services/scraper');
|
||||
const mailer = require('../services/mailer');
|
||||
const MigrationService = require('../services/migration');
|
||||
const SetupService = require('../services/setup');
|
||||
const kue = require('../services/kue');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const util = require('./util');
|
||||
const {createSubscriptionManager} = require('../graph/subscriptions');
|
||||
const {
|
||||
PORT
|
||||
} = require('../config');
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
|
||||
const port = normalizePort(PORT);
|
||||
app.set('port', port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
const server = createServer(app);
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
*/
|
||||
function onError(error) {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
let bind = typeof port === 'string'
|
||||
? `Pipe ${port}`
|
||||
: `Port ${port}`;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(`${bind} requires elevated privileges`);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(`${bind} is already in use`);
|
||||
break;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
*/
|
||||
|
||||
function normalizePort(val) {
|
||||
let port = parseInt(val, 10);
|
||||
|
||||
if (isNaN(port)) {
|
||||
|
||||
// named pipe
|
||||
return val;
|
||||
}
|
||||
|
||||
if (port >= 0) {
|
||||
|
||||
// port number
|
||||
return port;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "listening" event.
|
||||
*/
|
||||
|
||||
function onListening() {
|
||||
let addr = server.address();
|
||||
let bind = typeof addr === 'string'
|
||||
? `pipe ${addr}`
|
||||
: `port ${addr.port}`;
|
||||
console.log(`API Server Listening on ${bind}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the app.
|
||||
*/
|
||||
async function startApp(program) {
|
||||
|
||||
try {
|
||||
|
||||
// Check to see if the application is installed. If the application
|
||||
// has been installed, then it will throw errors.ErrSettingsNotInit, this
|
||||
// just means we don't have to check that the migrations have run.
|
||||
await SetupService.isAvailable();
|
||||
|
||||
debug('setup is currently available, migrations not being checked');
|
||||
|
||||
} catch (e) {
|
||||
|
||||
// Check the error.
|
||||
switch (e) {
|
||||
case errors.ErrInstallLock, errors.ErrSettingsInit:
|
||||
|
||||
debug('setup is not currently available, migrations now being checked');
|
||||
|
||||
// The error was expected, just continue.
|
||||
break;
|
||||
default:
|
||||
|
||||
// The error was not expected, throw the error!
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Now try and check the migration status.
|
||||
try {
|
||||
|
||||
// Verify that the minimum migration version is met.
|
||||
await MigrationService.verify();
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
debug('migrations do not have to be run');
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
server.listen(port, () => {
|
||||
|
||||
// Mount the websocket server if requested.
|
||||
if (program.websockets) {
|
||||
console.log(`Websocket Server Listening on ${port}`);
|
||||
|
||||
// Mount the subscriptions server on the application server.
|
||||
createSubscriptionManager(server);
|
||||
}
|
||||
});
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
program
|
||||
.option('-j, --jobs', 'enable job processing on this thread')
|
||||
.option('-w, --websockets', 'enable the websocket (subscriptions) handler on this thread')
|
||||
.parse(process.argv);
|
||||
|
||||
// Start the application serving.
|
||||
startApp(program);
|
||||
|
||||
// Enable job processing on the thread if enabled.
|
||||
if (program.jobs) {
|
||||
|
||||
// Start the scraper processor.
|
||||
scraper.process();
|
||||
|
||||
// Start the mail processor.
|
||||
mailer.process();
|
||||
}
|
||||
|
||||
// Define a safe shutdown function to call in the event we need to shutdown
|
||||
// because the node hooks are below which will interrupt the shutdown process.
|
||||
// Shutdown the mongoose connection, the app server, and the scraper.
|
||||
util.onshutdown([
|
||||
() => program.jobs ? kue.Task.shutdown() : null,
|
||||
() => mongoose.disconnect(),
|
||||
() => server.close()
|
||||
]);
|
||||
@@ -1,59 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const program = require('./commander');
|
||||
const inquirer = require('inquirer');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const SettingsService = require('../services/settings');
|
||||
const util = require('./util');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([() => mongoose.disconnect()]);
|
||||
|
||||
/**
|
||||
* Change the organization name
|
||||
*/
|
||||
async function changeOrgName() {
|
||||
try {
|
||||
let settings = await SettingsService.retrieve();
|
||||
|
||||
let {organizationName} = await inquirer.prompt([
|
||||
{
|
||||
name: 'organizationName',
|
||||
message: 'Organization Name',
|
||||
default: settings.organizationName
|
||||
}
|
||||
]);
|
||||
|
||||
if (settings.organizationName !== organizationName) {
|
||||
settings.organizationName = organizationName;
|
||||
|
||||
await SettingsService.update(settings);
|
||||
|
||||
console.log('Settings were updated.');
|
||||
} else {
|
||||
console.log('No update needed, no change was made.');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
}
|
||||
|
||||
util.shutdown();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
program
|
||||
.command('change-org-name')
|
||||
.description('change the organization name')
|
||||
.action(changeOrgName);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.outputHelp();
|
||||
util.shutdown();
|
||||
}
|
||||
-187
@@ -1,187 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const inquirer = require('inquirer');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const SettingModel = require('../models/setting');
|
||||
const MODERATION_OPTIONS = require('../models/enum/moderation_options');
|
||||
const SettingsService = require('../services/settings');
|
||||
const SetupService = require('../services/setup');
|
||||
const UsersService = require('../services/users');
|
||||
const util = require('./util');
|
||||
const errors = require('../errors');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
program
|
||||
.description('runs the setup wizard to setup the application')
|
||||
.option('--defaults', 'apply defaults for config instead of prompting')
|
||||
.parse(process.argv);
|
||||
|
||||
//==============================================================================
|
||||
// Setup the application
|
||||
//==============================================================================
|
||||
|
||||
const performSetup = async () => {
|
||||
|
||||
// Get the current settings, we are expecing an error here.
|
||||
try {
|
||||
|
||||
// Try to get the settings.
|
||||
await SettingsService.retrieve();
|
||||
|
||||
// We should NOT have gotten a settings object, this means that the
|
||||
// application is already setup. Error out here.
|
||||
throw errors.ErrSettingsInit;
|
||||
|
||||
} catch (e) {
|
||||
|
||||
// If the error is `not init`, then we're good, otherwise, it's something
|
||||
// else.
|
||||
if (e !== errors.ErrSettingsNotInit) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
if (program.defaults) {
|
||||
await SettingsService.init();
|
||||
|
||||
console.log('Settings created.');
|
||||
console.log('\nTalk is now installed!');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the base settings model.
|
||||
let settings = new SettingModel();
|
||||
|
||||
console.log('\nWe\'ll ask you some questions in order to setup your installation of Talk.\n');
|
||||
|
||||
let answers = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'organizationName',
|
||||
message: 'Organization Name',
|
||||
default: settings.organizationName,
|
||||
validate: (input) => {
|
||||
if (input && input.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return 'Organization Name is required.';
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
choices: MODERATION_OPTIONS,
|
||||
name: 'moderation',
|
||||
default: settings.moderation,
|
||||
message: 'Select a moderation mode'
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'requireEmailConfirmation',
|
||||
default: settings.requireEmailConfirmation,
|
||||
message: 'Should emails always be confirmed'
|
||||
}
|
||||
]);
|
||||
|
||||
// Update the settings that were changed.
|
||||
Object.keys(answers).forEach((key) => {
|
||||
if (answers[key] !== undefined) {
|
||||
settings[key] = answers[key];
|
||||
}
|
||||
});
|
||||
|
||||
console.log('\nWe\'ll ask you some questions about your first admin user.\n');
|
||||
|
||||
let user = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'username',
|
||||
message: 'Username',
|
||||
filter: (username) => {
|
||||
return UsersService
|
||||
.isValidUsername(username, false)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
message: 'Email',
|
||||
format: 'email',
|
||||
validate: (value) => {
|
||||
if (value && value.length >= 3) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return 'Email is required';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
type: 'password',
|
||||
filter: (password) => {
|
||||
return UsersService
|
||||
.isValidPassword(password)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'confirmPassword',
|
||||
message: 'Confirm Password',
|
||||
type: 'password',
|
||||
filter: (confirmPassword) => {
|
||||
return UsersService
|
||||
.isValidPassword(confirmPassword)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
if (user.password !== user.confirmPassword) {
|
||||
return Promise.reject(new Error('Passwords do not match'));
|
||||
}
|
||||
|
||||
let {user: newUser} = await SetupService.setup({
|
||||
settings: settings.toObject(),
|
||||
user: {
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
password: user.password
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Settings created.');
|
||||
console.log(`User ${newUser.id} created.`);
|
||||
console.log('\nTalk is now installed!');
|
||||
console.log('\nWe recommend adding TALK_INSTALL_LOCK=TRUE to your environment to turn off the dynamic setup.');
|
||||
};
|
||||
|
||||
// Start tthe setup process.
|
||||
performSetup()
|
||||
.then(() => {
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
});
|
||||
-100
@@ -1,100 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const TokensService = require('../services/tokens');
|
||||
const util = require('./util');
|
||||
const Table = require('cli-table');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
async function listTokens(userID) {
|
||||
try {
|
||||
let tokens = await TokensService.list(userID);
|
||||
|
||||
let table = new Table({
|
||||
head: [
|
||||
'ID',
|
||||
'Name',
|
||||
'Status'
|
||||
]
|
||||
});
|
||||
|
||||
tokens.forEach((token) => {
|
||||
table.push([
|
||||
token.id,
|
||||
token.name,
|
||||
token.active ? 'Active' : 'Revoked'
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function revokeToken(tokenID) {
|
||||
try {
|
||||
|
||||
await TokensService.revoke(null, tokenID);
|
||||
|
||||
console.log(`Revoked Token[${tokenID}]`);
|
||||
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function createToken(userID, tokenName) {
|
||||
try {
|
||||
|
||||
let {pat: {id}, jwt} = await TokensService.create(userID, tokenName);
|
||||
|
||||
console.log(`Created Token[${id}] for User[${userID}] = ${jwt}`);
|
||||
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
program
|
||||
.command('list <userID>')
|
||||
.description('list tokens for a user')
|
||||
.action(listTokens);
|
||||
|
||||
program
|
||||
.command('revoke <tokenID>')
|
||||
.description('revokes a token with a given id')
|
||||
.action(revokeToken);
|
||||
|
||||
program
|
||||
.command('create <userID> <tokenName>')
|
||||
.description('create a token for a user with a given name')
|
||||
.action(createToken);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.outputHelp();
|
||||
util.shutdown();
|
||||
}
|
||||
-483
@@ -1,483 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
const program = require('./commander');
|
||||
const inquirer = require('inquirer');
|
||||
const UsersService = require('../services/users');
|
||||
const UserModel = require('../models/user');
|
||||
const USER_ROLES = require('../models/enum/user_roles');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const util = require('./util');
|
||||
const Table = require('cli-table');
|
||||
|
||||
const validateRequired = (msg = 'Field is required', len = 1) => (input) => {
|
||||
if (input && input.length >= len) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return msg;
|
||||
};
|
||||
|
||||
// Regeister the shutdown criteria.
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
function getUserCreateAnswers(options) {
|
||||
if (options.flag_mode) {
|
||||
|
||||
let user = {
|
||||
email: options.email,
|
||||
password: options.password,
|
||||
confirmPassword: options.password,
|
||||
username: options.name,
|
||||
roles: []
|
||||
};
|
||||
|
||||
if (options.role && USER_ROLES.indexOf(options.role) > -1) {
|
||||
user.roles = [options.role];
|
||||
}
|
||||
|
||||
return Promise.resolve(user);
|
||||
}
|
||||
|
||||
return inquirer.prompt([
|
||||
{
|
||||
name: 'email',
|
||||
message: 'Email',
|
||||
format: 'email',
|
||||
validate: validateRequired('Email is required')
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
type: 'password',
|
||||
filter: (password) => {
|
||||
return UsersService
|
||||
.isValidPassword(password)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'confirmPassword',
|
||||
message: 'Confirm Password',
|
||||
type: 'password',
|
||||
filter: (confirmPassword) => {
|
||||
return UsersService
|
||||
.isValidPassword(confirmPassword)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'username',
|
||||
message: 'Username',
|
||||
filter: (username) => {
|
||||
return UsersService
|
||||
.isValidUsername(username)
|
||||
.catch((err) => {
|
||||
throw err.message;
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'roles',
|
||||
message: 'User Role',
|
||||
type: 'checkbox',
|
||||
choices: USER_ROLES
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts for input and registers a user based on those.
|
||||
*/
|
||||
function createUser(options) {
|
||||
getUserCreateAnswers(options)
|
||||
.then((answers) => {
|
||||
if (answers.password !== answers.confirmPassword) {
|
||||
return Promise.reject(new Error('Passwords do not match'));
|
||||
}
|
||||
|
||||
return answers;
|
||||
})
|
||||
.then((answers) => {
|
||||
return UsersService
|
||||
.createLocalUser(answers.email.trim(), answers.password.trim(), answers.username.trim())
|
||||
.then((user) => {
|
||||
console.log(`Created user ${user.id}.`);
|
||||
|
||||
if (answers.roles.length > 0) {
|
||||
return Promise.all(answers.roles.map((role) => {
|
||||
return UsersService
|
||||
.addRoleToUser(user.id, role)
|
||||
.then(() => {
|
||||
console.log(`Added the role ${role} to User ${user.id}.`);
|
||||
});
|
||||
}));
|
||||
}
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a user.
|
||||
*/
|
||||
function deleteUser(userID) {
|
||||
UserModel
|
||||
.findOneAndRemove({
|
||||
id: userID
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Deleted user');
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the password for a user.
|
||||
*/
|
||||
function passwd(userID) {
|
||||
inquirer.prompt([
|
||||
{
|
||||
name: 'password',
|
||||
message: 'Password',
|
||||
type: 'password',
|
||||
validate: validateRequired('Password is required')
|
||||
},
|
||||
{
|
||||
name: 'confirmPassword',
|
||||
message: 'Confirm Password',
|
||||
type: 'password',
|
||||
validate: validateRequired('Confirm Password is required')
|
||||
}
|
||||
])
|
||||
.then((answers) => {
|
||||
if (answers.password !== answers.confirmPassword) {
|
||||
return Promise.reject(new Error('Password mismatch'));
|
||||
}
|
||||
|
||||
return UsersService.changePassword(userID, answers.password);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Password changed.');
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the user from the options array.
|
||||
*/
|
||||
function updateUser(userID, options) {
|
||||
const updates = [];
|
||||
|
||||
if (options.email && typeof options.email === 'string' && options.email.length > 0) {
|
||||
let q = UserModel.update({
|
||||
'id': userID,
|
||||
'profiles.provider': 'local'
|
||||
}, {
|
||||
$set: {
|
||||
'profiles.$.id': options.email
|
||||
}
|
||||
});
|
||||
|
||||
updates.push(q);
|
||||
}
|
||||
|
||||
if (options.name && typeof options.name === 'string' && options.name.length > 0) {
|
||||
let q = UserModel.update({
|
||||
'id': userID
|
||||
}, {
|
||||
$set: {
|
||||
username: options.name
|
||||
}
|
||||
});
|
||||
|
||||
updates.push(q);
|
||||
}
|
||||
|
||||
Promise
|
||||
.all(updates.map((q) => q.exec()))
|
||||
.then(() => {
|
||||
console.log(`User ${userID} updated.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all the users registered in the database.
|
||||
*/
|
||||
function listUsers() {
|
||||
UsersService
|
||||
.all()
|
||||
.then((users) => {
|
||||
let table = new Table({
|
||||
head: [
|
||||
'ID',
|
||||
'Username',
|
||||
'Profiles',
|
||||
'Roles',
|
||||
'Status',
|
||||
'State'
|
||||
]
|
||||
});
|
||||
|
||||
users.forEach((user) => {
|
||||
table.push([
|
||||
user.id,
|
||||
user.username,
|
||||
user.profiles.map((p) => p.provider).join(', '),
|
||||
user.roles.join(', '),
|
||||
user.status,
|
||||
user.disabled ? 'Disabled' : 'Enabled'
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges two users using the specified ID's.
|
||||
* @param {String} dstUserID id of the user to which is the target of the merge
|
||||
* @param {String} srcUserID id of the user to which is the source of the merge
|
||||
*/
|
||||
function mergeUsers(dstUserID, srcUserID) {
|
||||
UsersService
|
||||
.mergeUsers(dstUserID, srcUserID)
|
||||
.then(() => {
|
||||
console.log(`User ${srcUserID} was merged into user ${dstUserID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a role to a user
|
||||
* @param {String} userUD id of the user to add the role to
|
||||
* @param {String} role the role to add
|
||||
*/
|
||||
function addRole(userID, role) {
|
||||
|
||||
if (USER_ROLES.indexOf(role) === -1) {
|
||||
console.error(`Role '${role}' is not supported. Supported roles are ${USER_ROLES.join(', ')}.`);
|
||||
util.shutdown(1);
|
||||
return;
|
||||
}
|
||||
|
||||
UsersService
|
||||
.addRoleToUser(userID, role)
|
||||
.then(() => {
|
||||
console.log(`Added the ${role} role to User ${userID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a role from a user
|
||||
* @param {String} userUD id of the user to remove the role from
|
||||
* @param {String} role the role to remove
|
||||
*/
|
||||
function removeRole(userID, role) {
|
||||
|
||||
if (USER_ROLES.indexOf(role) === -1) {
|
||||
console.error(`Role '${role}' is not supported. Supported roles are ${USER_ROLES.join(', ')}.`);
|
||||
util.shutdown(1);
|
||||
return;
|
||||
}
|
||||
|
||||
UsersService
|
||||
.removeRoleFromUser(userID, role)
|
||||
.then(() => {
|
||||
console.log(`Removed the ${role} role from User ${userID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ban a user
|
||||
* @param {String} userID id of the user to ban
|
||||
*/
|
||||
function ban(userID) {
|
||||
UsersService
|
||||
.setStatus(userID, 'BANNED')
|
||||
.then(() => {
|
||||
console.log(`Banned the User ${userID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unban a user
|
||||
* @param {String} userUD id of the user to remove the role from
|
||||
*/
|
||||
function unban(userID) {
|
||||
UsersService
|
||||
.setStatus(userID, 'ACTIVE')
|
||||
.then(() => {
|
||||
console.log(`Unban the User ${userID}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable a given user.
|
||||
* @param {String} userID the ID of a user to disable
|
||||
*/
|
||||
function disableUser(userID) {
|
||||
UsersService
|
||||
.disableUser(userID)
|
||||
.then(() => {
|
||||
console.log(`User ${userID} was disabled.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enabled a given user.
|
||||
* @param {String} userID the ID of a user to enable
|
||||
*/
|
||||
function enableUser(userID) {
|
||||
UsersService
|
||||
.enableUser(userID)
|
||||
.then(() => {
|
||||
console.log(`User ${userID} was enabled.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
program
|
||||
.command('create')
|
||||
.option('--email [email]', 'Email to use')
|
||||
.option('--password [password]', 'Password to use')
|
||||
.option('--name [name]', 'Name to use')
|
||||
.option('--role [role]', 'Role to add')
|
||||
.option('-f, --flag_mode', 'Source from flags instead of prompting')
|
||||
.description('create a new user')
|
||||
.action(createUser);
|
||||
|
||||
program
|
||||
.command('delete <userID>')
|
||||
.description('delete a user')
|
||||
.action(deleteUser);
|
||||
|
||||
program
|
||||
.command('passwd <userID>')
|
||||
.description('change a password for a user')
|
||||
.action(passwd);
|
||||
|
||||
program
|
||||
.command('update <userID>')
|
||||
.option('--email [email]', 'Email to use')
|
||||
.option('--name [name]', 'Name to use')
|
||||
.description('update a user')
|
||||
.action(updateUser);
|
||||
|
||||
program
|
||||
.command('list')
|
||||
.description('list all the users in the database')
|
||||
.action(listUsers);
|
||||
|
||||
program
|
||||
.command('merge <dstUserID> <srcUserID>')
|
||||
.description('merge srcUser into the dstUser')
|
||||
.action(mergeUsers);
|
||||
|
||||
program
|
||||
.command('addrole <userID> <role>')
|
||||
.description('adds a role to a given user')
|
||||
.action(addRole);
|
||||
|
||||
program
|
||||
.command('removerole <userID> <role>')
|
||||
.description('removes a role from a given user')
|
||||
.action(removeRole);
|
||||
|
||||
program
|
||||
.command('ban <userID>')
|
||||
.description('ban a given user')
|
||||
.action(ban);
|
||||
|
||||
program
|
||||
.command('uban <userID>')
|
||||
.description('unban a given user')
|
||||
.action(unban);
|
||||
|
||||
program
|
||||
.command('disable <userID>')
|
||||
.description('disable a given user from logging in')
|
||||
.action(disableUser);
|
||||
|
||||
program
|
||||
.command('enable <userID>')
|
||||
.description('enable a given user from logging in')
|
||||
.action(enableUser);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
if (!process.argv.slice(2).length) {
|
||||
program.outputHelp();
|
||||
util.shutdown();
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
const pkg = require('../package.json');
|
||||
const dotenv = require('dotenv');
|
||||
const fs = require('fs');
|
||||
const program = require('commander');
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
|
||||
const parseArgs = require('minimist')(process.argv.slice(2), {
|
||||
alias: {
|
||||
'c': 'config'
|
||||
},
|
||||
string: [
|
||||
'config',
|
||||
'pid'
|
||||
],
|
||||
default: {
|
||||
'config': null,
|
||||
'pid': null
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* If the config flag is present, then we have to load the configuration from
|
||||
* the file specified. We will then load those values into the environment.
|
||||
*/
|
||||
if (parseArgs.config) {
|
||||
let envConfig = dotenv.parse(fs.readFileSync(parseArgs.config, {encoding: 'utf8'}));
|
||||
|
||||
Object.keys(envConfig).forEach((k) => {
|
||||
process.env[k] = envConfig[k];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* If the pid flag is present, then we have to create a pid file at the location
|
||||
* specified.
|
||||
*/
|
||||
if (parseArgs.pid) {
|
||||
const util = require('./util');
|
||||
|
||||
console.log('Wrote PID');
|
||||
|
||||
util.pid(parseArgs.pid);
|
||||
}
|
||||
|
||||
module.exports = program
|
||||
.version(pkg.version)
|
||||
.option('-c, --config [path]', 'Specify the configuration file to load')
|
||||
.option('--pid [path]', 'Specify a path to output the current PID to');
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
const debug = require('debug')('talk:util');
|
||||
const fs = require('fs');
|
||||
|
||||
const util = module.exports = {};
|
||||
|
||||
/**
|
||||
* Stores an array of functions that should be executed in the event that the
|
||||
* application needs to shutdown.
|
||||
* @type {Array}
|
||||
*/
|
||||
util.toshutdown = [];
|
||||
|
||||
/**
|
||||
* Calls all the shutdown functions and then ends the process.
|
||||
* @param {Number} [defaultCode=0] default return code upon sucesfull shutdown.
|
||||
*/
|
||||
util.shutdown = (defaultCode = 0, signal = null) => {
|
||||
|
||||
if (signal) {
|
||||
debug(`Reached ${signal} signal`);
|
||||
}
|
||||
|
||||
debug(`${util.toshutdown.length} jobs now being called`);
|
||||
|
||||
Promise
|
||||
.all(util.toshutdown.map((func) => func ? func(signal) : null).filter((func) => func))
|
||||
.then(() => {
|
||||
debug('Shutdown complete, now exiting');
|
||||
process.exit(defaultCode);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Waits until an event is triggered by the node runtime and elevates a series
|
||||
* of jobs to be ran in the event we need to shutdown.
|
||||
* @param {Array} jobs Array of promise capable shutdown functions that are
|
||||
* executed.
|
||||
*/
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a PID file to be maintained for the lifespan of the process.
|
||||
* @param {String} path path to the PID file to create
|
||||
*/
|
||||
util.pid = (path) => {
|
||||
if (!/\//.test(path)) {
|
||||
if (!/\.pid/.test(path)) {
|
||||
path += '.pid';
|
||||
}
|
||||
path = `/tmp/${path}`;
|
||||
}
|
||||
|
||||
const pid = `${process.pid.toString()}\n`;
|
||||
|
||||
fs.writeFile(path, pid, (err) => {
|
||||
if (err) {
|
||||
console.error(`Can't write PID file: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Add the cleanup for the fs onto the shutdown.
|
||||
util.onshutdown([
|
||||
() => new Promise((resolve, reject) => {
|
||||
|
||||
// Remove the pid file.
|
||||
fs.unlink(path, (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve();
|
||||
});
|
||||
})
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
// 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('SIGUSR2', () => util.shutdown(0, 'SIGUSR2'));
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
machine:
|
||||
node:
|
||||
version: 7.9
|
||||
services:
|
||||
- docker
|
||||
- redis
|
||||
environment:
|
||||
PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin"
|
||||
NODE_ENV: "test"
|
||||
|
||||
dependencies:
|
||||
override:
|
||||
# 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
|
||||
|
||||
# Install node dependencies.
|
||||
- yarn --version
|
||||
- yarn
|
||||
cache_directories:
|
||||
- ~/.cache/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.
|
||||
- MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml MOCHA_REPORTER=mocha-junit-reporter yarn test
|
||||
# Run the e2e test suite.
|
||||
# - E2E_REPORT_PATH=$CIRCLE_TEST_REPORTS/e2e yarn e2e
|
||||
|
||||
deployment:
|
||||
release:
|
||||
tag: /v[0-9]+(\.[0-9]+)*/
|
||||
commands:
|
||||
- bash ./scripts/docker.sh deploy
|
||||
|
||||
latest:
|
||||
branch: master
|
||||
owner: coralproject
|
||||
commands:
|
||||
- bash ./scripts/docker.sh deploy
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
"add-module-exports",
|
||||
"transform-class-properties",
|
||||
"transform-decorators-legacy",
|
||||
"transform-object-assign",
|
||||
"transform-object-rest-spread",
|
||||
"transform-async-to-generator",
|
||||
"transform-react-jsx"
|
||||
]
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error",
|
||||
"no-console": ["warn", { "allow": ["warn", "error"] }]
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Router, Route, IndexRedirect, browserHistory, Redirect} from 'react-router';
|
||||
|
||||
import Configure from 'routes/Configure';
|
||||
import Dashboard from 'routes/Dashboard';
|
||||
import Install from 'routes/Install';
|
||||
import Stories from 'routes/Stories';
|
||||
import {CommunityLayout, Community} from 'routes/Community';
|
||||
import {ModerationLayout, Moderation} from 'routes/Moderation';
|
||||
|
||||
import Layout from 'containers/Layout';
|
||||
|
||||
const routes = (
|
||||
<div>
|
||||
<Route exact path="/admin/install" component={Install}/>
|
||||
<Route path='/admin' component={Layout}>
|
||||
<IndexRedirect to='/admin/moderate/all' />
|
||||
<Route path='configure' component={Configure} />
|
||||
<Route path='stories' component={Stories} />
|
||||
<Route path='dashboard' component={Dashboard} />
|
||||
|
||||
{/* Community Routes */}
|
||||
|
||||
<Route path='community' component={CommunityLayout}>
|
||||
<Route path='flagged' components={Community}>
|
||||
<Route path=':id' components={Community} />
|
||||
</Route>
|
||||
<Route path='people' components={Community}>
|
||||
<Route path=':id' components={Community} />
|
||||
</Route>
|
||||
<IndexRedirect to='flagged' />
|
||||
</Route>
|
||||
|
||||
{/* Moderation Routes */}
|
||||
|
||||
<Route path='moderate' component={ModerationLayout}>
|
||||
<Route path='all' components={Moderation}>
|
||||
<Route path=':id' components={Moderation} />
|
||||
</Route>
|
||||
<Route path='accepted' components={Moderation}>
|
||||
<Route path=':id' components={Moderation} />
|
||||
</Route>
|
||||
<Route path='premod' components={Moderation}>
|
||||
<Route path=':id' components={Moderation} />
|
||||
</Route>
|
||||
<Route path='rejected' components={Moderation}>
|
||||
<Route path=':id' components={Moderation} />
|
||||
</Route>
|
||||
<Route path='flagged' components={Moderation}>
|
||||
<Route path=':id' components={Moderation} />
|
||||
</Route>
|
||||
<Redirect from=':id' to='all/:id' />
|
||||
<IndexRedirect to='all' />
|
||||
</Route>
|
||||
</Route>
|
||||
</div>
|
||||
);
|
||||
|
||||
const AppRouter = () => <Router history={browserHistory} routes={routes} />;
|
||||
|
||||
export default AppRouter;
|
||||
@@ -1,50 +0,0 @@
|
||||
import {
|
||||
FETCH_ASSETS_REQUEST,
|
||||
FETCH_ASSETS_SUCCESS,
|
||||
FETCH_ASSETS_FAILURE,
|
||||
UPDATE_ASSET_STATE_REQUEST,
|
||||
UPDATE_ASSET_STATE_SUCCESS,
|
||||
UPDATE_ASSET_STATE_FAILURE,
|
||||
UPDATE_ASSETS
|
||||
} from '../constants/assets';
|
||||
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
/**
|
||||
* Action disptacher related to assets
|
||||
*/
|
||||
|
||||
// Fetch a page of assets
|
||||
// Get comments to fill each of the three lists on the mod queue
|
||||
export const fetchAssets = (skip = '', limit = '', search = '', sort = '', filter = '') => (dispatch) => {
|
||||
dispatch({type: FETCH_ASSETS_REQUEST});
|
||||
return coralApi(`/assets?skip=${skip}&limit=${limit}&sort=${sort}&search=${search}&filter=${filter}`)
|
||||
.then(({result, count}) =>
|
||||
dispatch({type: FETCH_ASSETS_SUCCESS,
|
||||
assets: result,
|
||||
count
|
||||
}))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: FETCH_ASSETS_FAILURE, error: errorMessage});
|
||||
});
|
||||
};
|
||||
|
||||
// Update an asset state
|
||||
// Get comments to fill each of the three lists on the mod queue
|
||||
export const updateAssetState = (id, closedAt) => (dispatch) => {
|
||||
dispatch({type: UPDATE_ASSET_STATE_REQUEST});
|
||||
return coralApi(`/assets/${id}/status`, {method: 'PUT', body: {closedAt}})
|
||||
.then(() => dispatch({type: UPDATE_ASSET_STATE_SUCCESS}))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: UPDATE_ASSET_STATE_FAILURE, error: errorMessage});
|
||||
});
|
||||
};
|
||||
|
||||
export const updateAssets = (assets) => (dispatch) => {
|
||||
dispatch({type: UPDATE_ASSETS, assets});
|
||||
};
|
||||
@@ -1,138 +0,0 @@
|
||||
import bowser from 'bowser';
|
||||
import * as actions from '../constants/auth';
|
||||
import coralApi from 'coral-framework/helpers/request';
|
||||
import * as Storage from 'coral-framework/helpers/storage';
|
||||
import {handleAuthToken} from 'coral-framework/actions/auth';
|
||||
import {resetWebsocket} from 'coral-framework/services/client';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
//==============================================================================
|
||||
// SIGN IN
|
||||
//==============================================================================
|
||||
|
||||
export const handleLogin = (email, password, recaptchaResponse) => (dispatch) => {
|
||||
dispatch({type: actions.LOGIN_REQUEST});
|
||||
|
||||
const params = {
|
||||
method: 'POST',
|
||||
body: {
|
||||
email,
|
||||
password
|
||||
}
|
||||
};
|
||||
|
||||
if (recaptchaResponse) {
|
||||
params.headers = {
|
||||
'X-Recaptcha-Response': recaptchaResponse
|
||||
};
|
||||
}
|
||||
|
||||
return coralApi('/auth/local', params)
|
||||
.then(({user, token}) => {
|
||||
|
||||
if (!user) {
|
||||
if (!bowser.safari && !bowser.ios) {
|
||||
Storage.removeItem('token');
|
||||
}
|
||||
return dispatch(checkLoginFailure('not logged in'));
|
||||
}
|
||||
|
||||
dispatch(handleAuthToken(token));
|
||||
resetWebsocket();
|
||||
dispatch(checkLoginSuccess(user));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
|
||||
if (error.translation_key === 'NOT_AUTHORIZED') {
|
||||
|
||||
// invalid credentials
|
||||
dispatch({
|
||||
type: actions.LOGIN_FAILURE,
|
||||
message: t('error.email_password')
|
||||
});
|
||||
}
|
||||
else if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
|
||||
dispatch({
|
||||
type: actions.LOGIN_MAXIMUM_EXCEEDED,
|
||||
message: t(`error.${error.translation_key}`),
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: actions.LOGIN_FAILURE,
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// FORGOT PASSWORD
|
||||
//==============================================================================
|
||||
|
||||
const forgotPasswordRequest = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_REQUEST
|
||||
});
|
||||
|
||||
const forgotPasswordSuccess = () => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS
|
||||
});
|
||||
|
||||
const forgotPasswordFailure = (error) => ({
|
||||
type: actions.FETCH_FORGOT_PASSWORD_FAILURE,
|
||||
error,
|
||||
});
|
||||
|
||||
export const requestPasswordReset = (email) => (dispatch) => {
|
||||
dispatch(forgotPasswordRequest(email));
|
||||
const redirectUri = location.href;
|
||||
|
||||
return coralApi('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}})
|
||||
.then(() => dispatch(forgotPasswordSuccess()))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(forgotPasswordFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// CHECK LOGIN
|
||||
//==============================================================================
|
||||
|
||||
const checkLoginRequest = () => ({
|
||||
type: actions.CHECK_LOGIN_REQUEST
|
||||
});
|
||||
|
||||
const checkLoginSuccess = (user, isAdmin) => ({
|
||||
type: actions.CHECK_LOGIN_SUCCESS,
|
||||
user,
|
||||
isAdmin
|
||||
});
|
||||
|
||||
const checkLoginFailure = (error) => ({
|
||||
type: actions.CHECK_LOGIN_FAILURE,
|
||||
error
|
||||
});
|
||||
|
||||
export const checkLogin = () => (dispatch) => {
|
||||
dispatch(checkLoginRequest());
|
||||
return coralApi('/auth')
|
||||
.then(({user}) => {
|
||||
if (!user) {
|
||||
if (!bowser.safari && !bowser.ios) {
|
||||
Storage.removeItem('token');
|
||||
}
|
||||
return dispatch(checkLoginFailure('not logged in'));
|
||||
}
|
||||
|
||||
resetWebsocket();
|
||||
dispatch(checkLoginSuccess(user));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(checkLoginFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
import {SHOW_BAN_USER_DIALOG, HIDE_BAN_USER_DIALOG} from '../constants/banUserDialog';
|
||||
|
||||
export const showBanUserDialog = ({userId, username, commentId, commentStatus}) =>
|
||||
({type: SHOW_BAN_USER_DIALOG, userId, username, commentId, commentStatus});
|
||||
|
||||
export const hideBanUserDialog = () => ({type: HIDE_BAN_USER_DIALOG});
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import qs from 'qs';
|
||||
|
||||
import {
|
||||
FETCH_COMMENTERS_REQUEST,
|
||||
FETCH_COMMENTERS_SUCCESS,
|
||||
FETCH_COMMENTERS_FAILURE,
|
||||
SORT_UPDATE,
|
||||
COMMENTERS_NEW_PAGE,
|
||||
SET_ROLE,
|
||||
SET_COMMENTER_STATUS,
|
||||
SHOW_BANUSER_DIALOG,
|
||||
HIDE_BANUSER_DIALOG,
|
||||
SHOW_REJECT_USERNAME_DIALOG,
|
||||
HIDE_REJECT_USERNAME_DIALOG
|
||||
} from '../constants/community';
|
||||
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const fetchAccounts = (query = {}) => (dispatch) => {
|
||||
|
||||
dispatch(requestFetchAccounts());
|
||||
coralApi(`/users?${qs.stringify(query)}`)
|
||||
.then(({result, page, count, limit, totalPages}) =>{
|
||||
dispatch({
|
||||
type: FETCH_COMMENTERS_SUCCESS,
|
||||
accounts: result,
|
||||
page,
|
||||
count,
|
||||
limit,
|
||||
totalPages
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: FETCH_COMMENTERS_FAILURE, error: errorMessage});
|
||||
});
|
||||
};
|
||||
|
||||
const requestFetchAccounts = () => ({
|
||||
type: FETCH_COMMENTERS_REQUEST
|
||||
});
|
||||
|
||||
export const updateSorting = (sort) => ({
|
||||
type: SORT_UPDATE,
|
||||
sort
|
||||
});
|
||||
|
||||
export const newPage = () => ({
|
||||
type: COMMENTERS_NEW_PAGE
|
||||
});
|
||||
|
||||
export const setRole = (id, role) => (dispatch) => {
|
||||
return coralApi(`/users/${id}/role`, {method: 'POST', body: {role}})
|
||||
.then(() => {
|
||||
return dispatch({type: SET_ROLE, id, role});
|
||||
});
|
||||
};
|
||||
|
||||
export const setCommenterStatus = (id, status) => (dispatch) => {
|
||||
return coralApi(`/users/${id}/status`, {method: 'POST', body: {status}})
|
||||
.then(() => {
|
||||
return dispatch({type: SET_COMMENTER_STATUS, id, status});
|
||||
});
|
||||
};
|
||||
|
||||
// Ban User Dialog
|
||||
export const showBanUserDialog = (user) => ({type: SHOW_BANUSER_DIALOG, user});
|
||||
export const hideBanUserDialog = () => ({type: HIDE_BANUSER_DIALOG});
|
||||
|
||||
// Reject Username Dialog
|
||||
export const showRejectUsernameDialog = (user) => ({type: SHOW_REJECT_USERNAME_DIALOG, user});
|
||||
export const hideRejectUsernameDialog = () => ({type: HIDE_REJECT_USERNAME_DIALOG});
|
||||
@@ -1,7 +0,0 @@
|
||||
export const CONFIG_UPDATED = 'CONFIG_UPDATED';
|
||||
|
||||
export const fetchConfig = () => (dispatch) => {
|
||||
let json = document.getElementById('data');
|
||||
let data = JSON.parse(json.textContent);
|
||||
dispatch({type: CONFIG_UPDATED, data});
|
||||
};
|
||||
@@ -1,146 +0,0 @@
|
||||
import coralApi from 'coral-framework/helpers/request';
|
||||
import * as actions from '../constants/install';
|
||||
import validate from 'coral-framework/helpers/validate';
|
||||
import errorMsj from 'coral-framework/helpers/error';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const nextStep = () => ({type: actions.NEXT_STEP});
|
||||
export const previousStep = () => ({type: actions.PREVIOUS_STEP});
|
||||
export const goToStep = (step) => ({type: actions.GO_TO_STEP, step});
|
||||
|
||||
const installRequest = () => ({type: actions.INSTALL_REQUEST});
|
||||
const installSuccess = () => ({type: actions.INSTALL_SUCCESS});
|
||||
const installFailure = (error) => ({type: actions.INSTALL_FAILURE, error});
|
||||
|
||||
const addError = (name, error) => ({type: actions.ADD_ERROR, name, error});
|
||||
const hasError = (error) => ({type: actions.HAS_ERROR, error});
|
||||
const clearErrors = () => ({type: actions.CLEAR_ERRORS});
|
||||
|
||||
const validation = (formData, dispatch, next) => {
|
||||
if (!(formData != null)) {
|
||||
dispatch(hasError());
|
||||
return;
|
||||
}
|
||||
|
||||
const validKeys = Object.keys(formData)
|
||||
.filter((name) => name !== 'domains');
|
||||
|
||||
// Required Validation
|
||||
const empty = validKeys
|
||||
.filter((name) => {
|
||||
const cond = !formData[name].length;
|
||||
|
||||
if (cond) {
|
||||
|
||||
// Adding Error
|
||||
dispatch(addError(name, 'This field is required.'));
|
||||
} else {
|
||||
dispatch(addError(name, ''));
|
||||
}
|
||||
|
||||
return cond;
|
||||
});
|
||||
|
||||
if (empty.length) {
|
||||
dispatch(hasError());
|
||||
return;
|
||||
}
|
||||
|
||||
// RegExp Validation
|
||||
const validation = validKeys
|
||||
.filter((name) => {
|
||||
const cond = !validate[name](formData[name]);
|
||||
if (cond) {
|
||||
|
||||
// Adding Error
|
||||
dispatch(addError(name, errorMsj[name]));
|
||||
} else {
|
||||
dispatch(addError(name, ''));
|
||||
}
|
||||
|
||||
return cond;
|
||||
});
|
||||
|
||||
if (validation.length) {
|
||||
dispatch(hasError());
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm Validation
|
||||
const prefixLength = 'confirm'.length;
|
||||
const confirm = validKeys
|
||||
.filter((name) => {
|
||||
if (!name.startsWith('confirm')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check that 'confirmX' equals 'X'.
|
||||
const other = name.substr(prefixLength, 1).toLowerCase() + name.substr(prefixLength + 1);
|
||||
const cond = formData[other] !== formData[name];
|
||||
if (cond) {
|
||||
dispatch(addError(name, errorMsj[name]));
|
||||
}
|
||||
return cond;
|
||||
});
|
||||
|
||||
if (confirm.length) {
|
||||
dispatch(hasError());
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(clearErrors());
|
||||
next();
|
||||
};
|
||||
|
||||
export const submitSettings = () => (dispatch, getState) => {
|
||||
const settingsFormData = getState().install.toJS().data.settings;
|
||||
validation(settingsFormData, dispatch, function() {
|
||||
dispatch(nextStep());
|
||||
});
|
||||
};
|
||||
|
||||
export const submitUser = () => (dispatch, getState) => {
|
||||
const userFormData = getState().install.toJS().data.user;
|
||||
validation(userFormData, dispatch, function() {
|
||||
dispatch(nextStep());
|
||||
});
|
||||
};
|
||||
|
||||
export const finishInstall = () => (dispatch, getState) => {
|
||||
const data = getState().install.toJS().data;
|
||||
dispatch(installRequest());
|
||||
return coralApi('/setup', {method: 'POST', body: data})
|
||||
.then(() => {
|
||||
dispatch(installSuccess());
|
||||
dispatch(nextStep());
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(installFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
|
||||
export const updateSettingsFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_SETTINGS, name, value});
|
||||
export const updateUserFormData = (name, value) => ({type: actions.UPDATE_FORMDATA_USER, name, value});
|
||||
export const updatePermittedDomains = (value) => ({type: actions.UPDATE_PERMITTED_DOMAINS_SETTINGS, value});
|
||||
|
||||
const checkInstallRequest = () => ({type: actions.CHECK_INSTALL_REQUEST});
|
||||
const checkInstallSuccess = (installed) => ({type: actions.CHECK_INSTALL_SUCCESS, installed});
|
||||
const checkInstallFailure = (error) => ({type: actions.CHECK_INSTALL_FAILURE, error});
|
||||
|
||||
export const checkInstall = (next) => (dispatch) => {
|
||||
dispatch(checkInstallRequest());
|
||||
coralApi('/setup')
|
||||
.then(({installed}) => {
|
||||
dispatch(checkInstallSuccess(installed));
|
||||
if (installed) {
|
||||
next();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch(checkInstallFailure(errorMessage));
|
||||
});
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
import * as actions from 'constants/moderation';
|
||||
|
||||
export const toggleModal = (open) => ({type: actions.TOGGLE_MODAL, open});
|
||||
export const singleView = () => ({type: actions.SINGLE_VIEW});
|
||||
|
||||
// hide shortcuts note
|
||||
export const hideShortcutsNote = () => {
|
||||
try {
|
||||
window.localStorage.setItem('coral:shortcutsNote', 'hide');
|
||||
} catch (e) {
|
||||
|
||||
// above will fail in Safari private mode
|
||||
}
|
||||
|
||||
return {type: actions.HIDE_SHORTCUTS_NOTE};
|
||||
};
|
||||
|
||||
export const viewUserDetail = (userId) => ({type: actions.VIEW_USER_DETAIL, userId});
|
||||
export const hideUserDetail = () => ({type: actions.HIDE_USER_DETAIL});
|
||||
|
||||
export const setSortOrder = (order) => ({
|
||||
type: actions.SET_SORT_ORDER,
|
||||
order
|
||||
});
|
||||
|
||||
export const changeUserDetailStatuses = (tab) => {
|
||||
let statuses;
|
||||
if (tab === 'all') {
|
||||
statuses = ['NONE', 'ACCEPTED', 'REJECTED', 'PREMOD'];
|
||||
} else if (tab === 'rejected') {
|
||||
statuses = ['REJECTED'];
|
||||
}
|
||||
return {type: actions.CHANGE_USER_DETAIL_STATUSES, tab, statuses};
|
||||
};
|
||||
|
||||
export const clearUserDetailSelections = () => ({type: actions.CLEAR_USER_DETAIL_SELECTIONS});
|
||||
|
||||
export const toggleSelectCommentInUserDetail = (id, active) => {
|
||||
return {
|
||||
type: active ? actions.SELECT_USER_DETAIL_COMMENT : actions.UNSELECT_USER_DETAIL_COMMENT,
|
||||
id
|
||||
};
|
||||
};
|
||||
|
||||
export const toggleStorySearch = (active) => ({
|
||||
type: active ? actions.SHOW_STORY_SEARCH : actions.HIDE_STORY_SEARCH
|
||||
});
|
||||
|
||||
export const storySearchChange = (value) => ({
|
||||
type: actions.STORY_SEARCH_CHANGE_VALUE,
|
||||
value
|
||||
});
|
||||
|
||||
export const clearState = () => ({
|
||||
type: actions.MODERATION_CLEAR_STATE
|
||||
});
|
||||
@@ -1,59 +0,0 @@
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const SETTINGS_LOADING = 'SETTINGS_LOADING';
|
||||
export const SETTINGS_RECEIVED = 'SETTINGS_RECEIVED';
|
||||
export const SETTINGS_FETCH_ERROR = 'SETTINGS_FETCH_ERROR';
|
||||
|
||||
export const SETTINGS_UPDATED = 'SETTINGS_UPDATED';
|
||||
|
||||
export const SAVE_SETTINGS_LOADING = 'SAVE_SETTINGS_LOADING';
|
||||
export const SAVE_SETTINGS_SUCCESS = 'SAVE_SETTINGS_SUCCESS';
|
||||
export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED';
|
||||
|
||||
export const WORDLIST_UPDATED = 'WORDLIST_UPDATED';
|
||||
export const DOMAINLIST_UPDATED = 'DOMAINLIST_UPDATED';
|
||||
|
||||
export const fetchSettings = () => (dispatch) => {
|
||||
dispatch({type: SETTINGS_LOADING});
|
||||
coralApi('/settings')
|
||||
.then((settings) => {
|
||||
dispatch({type: SETTINGS_RECEIVED, settings});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: SETTINGS_FETCH_ERROR, error: errorMessage});
|
||||
});
|
||||
};
|
||||
|
||||
// for updating top-level settings
|
||||
export const updateSettings = (settings) => {
|
||||
return {type: SETTINGS_UPDATED, settings};
|
||||
};
|
||||
|
||||
// this is a nested property, so it needs a special action.
|
||||
export const updateWordlist = (listName, list) => {
|
||||
return {type: WORDLIST_UPDATED, listName, list};
|
||||
};
|
||||
|
||||
export const updateDomainlist = (listName, list) => {
|
||||
return {type: DOMAINLIST_UPDATED, listName, list};
|
||||
};
|
||||
|
||||
export const saveSettingsToServer = () => (dispatch, getState) => {
|
||||
let settings = getState().settings.toJS();
|
||||
if (settings.charCount) {
|
||||
settings.charCount = parseInt(settings.charCount);
|
||||
}
|
||||
dispatch({type: SAVE_SETTINGS_LOADING});
|
||||
coralApi('/settings', {method: 'PUT', body: settings})
|
||||
.then(() => {
|
||||
dispatch({type: SAVE_SETTINGS_SUCCESS, settings});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: SAVE_SETTINGS_FAILED, error: errorMessage});
|
||||
});
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
import {SHOW_SUSPEND_USER_DIALOG, HIDE_SUSPEND_USER_DIALOG} from '../constants/suspendUserDialog.js';
|
||||
|
||||
export const showSuspendUserDialog = ({userId, username, commentId, commentStatus}) =>
|
||||
({type: SHOW_SUSPEND_USER_DIALOG, userId, username, commentId, commentStatus});
|
||||
|
||||
export const hideSuspendUserDialog = () => ({type: HIDE_SUSPEND_USER_DIALOG});
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import coralApi from '../../../coral-framework/helpers/request';
|
||||
import * as userTypes from '../constants/users';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
/**
|
||||
* Action disptacher related to users
|
||||
*/
|
||||
// change status of a user
|
||||
export const userStatusUpdate = (status, userId, commentId) => {
|
||||
return (dispatch) => {
|
||||
dispatch({type: userTypes.UPDATE_STATUS_REQUEST});
|
||||
return coralApi(`/users/${userId}/status`, {method: 'POST', body: {status: status, comment_id: commentId}})
|
||||
.then((res) => dispatch({type: userTypes.UPDATE_STATUS_SUCCESS, res}))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: userTypes.UPDATE_STATUS_FAILURE, error: errorMessage});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// change status of a user
|
||||
export const sendNotificationEmail = (userId, subject, body) => {
|
||||
return (dispatch) => {
|
||||
return coralApi(`/users/${userId}/email`, {method: 'POST', body: {subject, body}})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: userTypes.USER_EMAIL_FAILURE, error: errorMessage});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// let a user edit their username
|
||||
export const enableUsernameEdit = (userId) => {
|
||||
return (dispatch) => {
|
||||
return coralApi(`/users/${userId}/username-enable`, {method: 'POST'})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
|
||||
dispatch({type: userTypes.USERNAME_ENABLE_FAILURE, error: errorMessage});
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from './ModerationList.css';
|
||||
import {Button} from 'coral-ui';
|
||||
import {menuActionsMap} from '../routes/Moderation/helpers/moderationQueueActionsMap';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const ActionButton = ({type = '', active, ...props}) => {
|
||||
const typeName = type.toLowerCase();
|
||||
let text = menuActionsMap[type].text;
|
||||
|
||||
if (text === 'approve' && active) {
|
||||
text = 'approved';
|
||||
} else if (text === 'reject' && active) {
|
||||
text = 'rejected';
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={`${typeName} ${styles.actionButton} ${props.minimal ? styles.minimal : ''} ${active ? styles[`${typeName}__active`] : ''}`}
|
||||
cStyle={typeName}
|
||||
icon={menuActionsMap[type].icon}
|
||||
onClick={type === 'APPROVE' ? props.acceptComment : props.rejectComment}
|
||||
>{props.minimal ? '' : t(`modqueue.${text}`)}</Button>
|
||||
);
|
||||
};
|
||||
|
||||
ActionButton.propTypes = {
|
||||
active: PropTypes.bool
|
||||
};
|
||||
|
||||
export default ActionButton;
|
||||
@@ -1,53 +0,0 @@
|
||||
.button {
|
||||
-webkit-transform: scale(.8);
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.root {
|
||||
color: black;
|
||||
> :global(.mdl-menu__container) {
|
||||
margin-left: 10px;
|
||||
> :global(.mdl-menu__outline) {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttonOpen {
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
background-color: #616161;
|
||||
}
|
||||
|
||||
.arrowIcon {
|
||||
margin-left: 6px;
|
||||
margin-right: 0;
|
||||
vertical-align: middle;
|
||||
margin-right: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.menuItem {
|
||||
background-color: #2a2a2a;
|
||||
color: white;
|
||||
&:first-child {
|
||||
margin-bottom: 1px;
|
||||
border-radius: 2px 2px 0px 0px;
|
||||
}
|
||||
&:last-child {
|
||||
border-radius: 0px 0px 2px 2px;
|
||||
}
|
||||
&:hover, &:active, &:focus {
|
||||
background-color: #767676;
|
||||
}
|
||||
&[disabled], &[disabled]:hover, &[disabled]:focus, &[disabled]:active {
|
||||
background-color: #262626;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Button, Icon} from 'coral-ui';
|
||||
import {Menu} from 'react-mdl';
|
||||
import cn from 'classnames';
|
||||
import {findDOMNode} from 'react-dom';
|
||||
import styles from './ActionsMenu.css';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
let count = 0;
|
||||
|
||||
class ActionsMenu extends React.Component {
|
||||
id = `actions-dropdown-${count++}`;
|
||||
menu = null;
|
||||
state = {open: false};
|
||||
timeout = null;
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
|
||||
handleRef = (ref) => {
|
||||
this.menu = ref ? findDOMNode(ref).parentNode : null;
|
||||
}
|
||||
|
||||
syncOpenState = () => {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = setTimeout(() => {
|
||||
this.setState({open: this.menu.className.indexOf('is-visible') >= 0});
|
||||
}, 150);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.root} onBlur={this.syncOpenState} >
|
||||
<Button
|
||||
cStyle='actions'
|
||||
className={cn(styles.button, {[styles.buttonOpen]: this.state.open})}
|
||||
disabled={false}
|
||||
id={this.id}
|
||||
onClick={this.syncOpenState}
|
||||
icon={this.props.icon}
|
||||
raised>
|
||||
{t('modqueue.actions')}
|
||||
<Icon
|
||||
name={this.state.open ? 'keyboard_arrow_up' : 'keyboard_arrow_down'}
|
||||
className={styles.arrowIcon}
|
||||
/>
|
||||
</Button>
|
||||
<Menu target={this.id} className={styles.menu} ref={this.handleRef}>
|
||||
{this.props.children}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ActionsMenu.propTypes = {
|
||||
icon: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ActionsMenu;
|
||||
@@ -1,9 +0,0 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import {MenuItem} from 'react-mdl';
|
||||
import styles from './ActionsMenu.css';
|
||||
|
||||
const ActionsMenuItem = (props) =>
|
||||
<MenuItem className={cn(styles.menuItem, props.className)} {...props} />;
|
||||
|
||||
export default ActionsMenuItem;
|
||||
@@ -1,110 +0,0 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import Layout from 'coral-admin/src/components/ui/Layout';
|
||||
import styles from './NotFound.css';
|
||||
import {Button, TextField, Alert, Success} from 'coral-ui';
|
||||
import Recaptcha from 'react-recaptcha';
|
||||
|
||||
class AdminLogin extends React.Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {email: '', password: '', requestPassword: false};
|
||||
}
|
||||
|
||||
handleSignIn = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.handleLogin(this.state.email, this.state.password);
|
||||
}
|
||||
|
||||
onRecaptchaLoad = () => {
|
||||
|
||||
// do something?
|
||||
}
|
||||
|
||||
onRecaptchaVerify = (recaptchaResponse) => {
|
||||
this.props.handleLogin(this.state.email, this.state.password, recaptchaResponse);
|
||||
}
|
||||
|
||||
handleRequestPassword = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.requestPasswordReset(this.state.email);
|
||||
}
|
||||
|
||||
render () {
|
||||
const {errorMessage, loginMaxExceeded, recaptchaPublic} = this.props;
|
||||
const signInForm = (
|
||||
<form onSubmit={this.handleSignIn}>
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
label='Email Address'
|
||||
value={this.state.email}
|
||||
onChange={(e) => this.setState({email: e.target.value})} />
|
||||
<TextField
|
||||
label='Password'
|
||||
value={this.state.password}
|
||||
onChange={(e) => this.setState({password: e.target.value})}
|
||||
type='password' />
|
||||
<div style={{height: 10}}></div>
|
||||
<Button
|
||||
type='submit'
|
||||
cStyle='black'
|
||||
full
|
||||
onClick={this.handleSignIn}>Sign In</Button>
|
||||
<p className={styles.forgotPasswordCTA}>
|
||||
Forgot your password? <a href="#" className={styles.forgotPasswordLink} onClick={(e) => {
|
||||
e.preventDefault();
|
||||
this.setState({requestPassword: true});
|
||||
}}>Request a new one.</a>
|
||||
</p>
|
||||
{
|
||||
loginMaxExceeded &&
|
||||
<Recaptcha
|
||||
sitekey={recaptchaPublic}
|
||||
render='explicit'
|
||||
theme='dark'
|
||||
onloadCallback={this.onRecaptchaLoad}
|
||||
verifyCallback={this.onRecaptchaVerify} />
|
||||
}
|
||||
</form>
|
||||
);
|
||||
const requestPasswordForm = (
|
||||
this.props.passwordRequestSuccess
|
||||
? <p className={styles.passwordRequestSuccess} onClick={() => {
|
||||
location.href = location.href;
|
||||
}}>
|
||||
{this.props.passwordRequestSuccess} <a className={styles.signInLink} href="#">Sign in</a>
|
||||
<Success />
|
||||
</p>
|
||||
: <form onSubmit={this.handleRequestPassword}>
|
||||
<TextField
|
||||
label='Email Address'
|
||||
value={this.state.email}
|
||||
onChange={(e) => this.setState({email: e.target.value})} />
|
||||
<Button
|
||||
type='submit'
|
||||
cStyle='black'
|
||||
full
|
||||
onClick={this.handleRequestPassword}>Reset Password</Button>
|
||||
</form>
|
||||
);
|
||||
return (
|
||||
<Layout fixedDrawer restricted={true}>
|
||||
<div className={styles.loginLayout}>
|
||||
<h1 className={styles.loginHeader}>Team sign in</h1>
|
||||
<p className={styles.loginCTA}>Sign in to interact with your community.</p>
|
||||
{ this.state.requestPassword ? requestPasswordForm : signInForm }
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AdminLogin.propTypes = {
|
||||
loginMaxExceeded: PropTypes.bool.isRequired,
|
||||
handleLogin: PropTypes.func.isRequired,
|
||||
passwordRequestSuccess: PropTypes.string,
|
||||
loginError: PropTypes.string,
|
||||
recaptchaPublic: PropTypes.string
|
||||
};
|
||||
|
||||
export default AdminLogin;
|
||||
@@ -1,16 +0,0 @@
|
||||
import React from 'react';
|
||||
import ToastContainer from './ToastContainer';
|
||||
import 'material-design-lite';
|
||||
|
||||
import AppRouter from '../AppRouter';
|
||||
|
||||
export default class App extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<ToastContainer />
|
||||
<AppRouter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
.dialog {
|
||||
border: none;
|
||||
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
|
||||
width: 500px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 184px;
|
||||
padding: 20px;
|
||||
|
||||
h2 {
|
||||
color: black;
|
||||
font-size: 1.76em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: black;
|
||||
font-size: 1.4em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.textField {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.textField label {
|
||||
font-size: 1.08em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.textField input {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border: none;
|
||||
outline: none;
|
||||
border: 1px solid rgba(0,0,0,.12);
|
||||
padding: 10px 6px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
margin: 5px auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin: 20px auto 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer span {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.socialConnections {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.signInButton {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.close {
|
||||
font-size: 20px;
|
||||
line-height: 14px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
position: absolute;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #363636;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: #6b6b6b;
|
||||
}
|
||||
|
||||
input.error{
|
||||
border: solid 2px #f44336;
|
||||
}
|
||||
|
||||
.errorMsg, .hint {
|
||||
color: grey;
|
||||
font-weight: 600;
|
||||
padding: 3px 0 16px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.alert--success {
|
||||
border: solid 1px #1ec00e;
|
||||
background: #cbf1b8;
|
||||
color: #006900;
|
||||
}
|
||||
|
||||
.alert--error {
|
||||
background: #FFEBEE;
|
||||
color: #B71C1C;
|
||||
}
|
||||
|
||||
.userBox a {
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.attention {
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background: #B71C1C;
|
||||
color: #FFEBEE;
|
||||
font-weight: bolder;
|
||||
padding: 4px;
|
||||
vertical-align: middle;
|
||||
border-radius: 20px;
|
||||
box-sizing: border-box;
|
||||
font-size: 9px;
|
||||
line-height: 7px;
|
||||
text-align: center;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.action {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.passwordRequestSuccess {
|
||||
border: 1px solid green;
|
||||
background-color: lightgreen;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.passwordRequestFailure {
|
||||
border: 1px solid orange;
|
||||
background-color: 1px solid coral;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
margin-right: 10px;
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.ban {
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Dialog} from 'coral-ui';
|
||||
import styles from './BanUserDialog.css';
|
||||
|
||||
import Button from 'coral-ui/components/Button';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const BanUserDialog = ({open, onCancel, onPerform, username, info}) => (
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
id="banUserDialog"
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
title={t('bandialog.ban_user')}>
|
||||
<span className={styles.close} onClick={onCancel}>×</span>
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<h2>{t('bandialog.ban_user')}</h2>
|
||||
</div>
|
||||
<div className={styles.separator}>
|
||||
<h3>{t('bandialog.are_you_sure', username)}</h3>
|
||||
<i>{info}</i>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<Button cStyle="cancel" className={styles.cancel} onClick={onCancel} raised>
|
||||
{t('bandialog.cancel')}
|
||||
</Button>
|
||||
<Button cStyle="black" className={styles.ban} onClick={onPerform} raised>
|
||||
{t('bandialog.yes_ban_user')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
BanUserDialog.propTypes = {
|
||||
open: PropTypes.bool,
|
||||
onPerform: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
username: PropTypes.string,
|
||||
info: PropTypes.string,
|
||||
};
|
||||
|
||||
export default BanUserDialog;
|
||||
@@ -1,4 +0,0 @@
|
||||
|
||||
.textareaContainer {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
|
||||
import React from 'react';
|
||||
import styles from './CommentBox.css';
|
||||
import {Button} from 'react-mdl';
|
||||
|
||||
// Renders a comment box for creating a new comment
|
||||
export default class CommentBox extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {name: '', body: ''};
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
}
|
||||
|
||||
onSubmit () {
|
||||
const {name, body} = this.state;
|
||||
this.props.onSubmit({name, body});
|
||||
this.setState({body: '', name: ''});
|
||||
}
|
||||
|
||||
render (props, {name, body}) {
|
||||
return (
|
||||
<div>
|
||||
<div class={`${styles.textareaContainer} mdl-textfield mdl-js-textfield`}>
|
||||
<input type='text' value={name} onInput={this.linkState('name')} class='mdl-textfield__input' id='name' />
|
||||
<label class='mdl-textfield__label' for='name'>Your name</label>
|
||||
</div>
|
||||
<div class={`${styles.textareaContainer} mdl-textfield mdl-js-textfield`}>
|
||||
<textarea value={body} onInput={this.linkState('body')} class='mdl-textfield__input' type='text' rows='5' id='comment' />
|
||||
<label class='mdl-textfield__label' for='comment'>Write your comment</label>
|
||||
</div>
|
||||
<Button onClick={this.onSubmit} raised>Post</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Card} from 'coral-ui';
|
||||
|
||||
const EmptyCard = (props) => (
|
||||
<Card style={{textAlign: 'center', maxWidth: 400, margin: '0 auto'}}>
|
||||
{props.children}
|
||||
</Card>
|
||||
);
|
||||
|
||||
EmptyCard.propTypes = {
|
||||
children: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
export default EmptyCard;
|
||||
@@ -1,34 +0,0 @@
|
||||
.heading {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.widgetTable {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.widgetTable thead th {
|
||||
border-bottom: 1px solid #f47e6b;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.widgetTable tbody tr {
|
||||
border-bottom: 1px solid lightgrey;
|
||||
}
|
||||
|
||||
.widgetTable tbody tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.widgetTable tbody td {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.lede {
|
||||
font-size: 0.9em;
|
||||
color: grey;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
.layout {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.layout h1 {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.layout img {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Layout} from 'react-mdl';
|
||||
import styles from './FullLoading.css';
|
||||
import {CoralLogo} from 'coral-ui';
|
||||
|
||||
export const FullLoading = () => (
|
||||
<Layout fixedDrawer>
|
||||
<div className={styles.layout} >
|
||||
<h1>Loading</h1>
|
||||
<CoralLogo />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
@@ -1,147 +0,0 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import SimpleMDE from 'simplemde';
|
||||
import cn from 'classnames';
|
||||
import noop from 'lodash/noop';
|
||||
import styles from './MarkdownEditor.css';
|
||||
|
||||
const config = {
|
||||
status: false,
|
||||
|
||||
// Do not download fontAwesome icons as we replace them with
|
||||
// material icons.
|
||||
autoDownloadFontAwesome: false,
|
||||
|
||||
// Disable built-in spell checker as it is very rudimentary.
|
||||
spellChecker: false,
|
||||
|
||||
toolbar: [
|
||||
{
|
||||
name: 'bold',
|
||||
action: SimpleMDE.toggleBold,
|
||||
className: styles.iconBold,
|
||||
title: 'Bold',
|
||||
},
|
||||
{
|
||||
name: 'italic',
|
||||
action: SimpleMDE.toggleItalic,
|
||||
className: styles.iconItalic,
|
||||
title: 'Italic',
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
action: SimpleMDE.toggleHeadingSmaller,
|
||||
className: styles.iconTitle,
|
||||
title: 'Title, Subtitle, Heading',
|
||||
},
|
||||
'|',
|
||||
{
|
||||
name: 'quote',
|
||||
action: SimpleMDE.toggleBlockquote,
|
||||
className: styles.iconQuote,
|
||||
title: 'Quote',
|
||||
},
|
||||
{
|
||||
name: 'unordered-list',
|
||||
action: SimpleMDE.toggleUnorderedList,
|
||||
className: styles.iconUnorderedList,
|
||||
title: 'Generic List',
|
||||
},
|
||||
{
|
||||
name: 'ordered-list',
|
||||
action: SimpleMDE.toggleOrderedList,
|
||||
className: styles.iconOrderedList,
|
||||
title: 'Numbered List',
|
||||
},
|
||||
'|',
|
||||
{
|
||||
name: 'link',
|
||||
action: SimpleMDE.drawLink,
|
||||
className: styles.iconLink,
|
||||
title: 'Create Link',
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
action: SimpleMDE.drawImage,
|
||||
className: styles.iconImage,
|
||||
title: 'Insert Image',
|
||||
},
|
||||
'|',
|
||||
{
|
||||
name: 'preview',
|
||||
action: SimpleMDE.togglePreview,
|
||||
className: cn(styles.iconPreview, 'no-disable'),
|
||||
title: 'Toggle Preview',
|
||||
},
|
||||
{
|
||||
name: 'side-by-side',
|
||||
action: SimpleMDE.toggleSideBySide,
|
||||
className: cn(styles.iconSideBySide, 'no-disable'),
|
||||
title: 'Toggle Side by Side',
|
||||
},
|
||||
{
|
||||
name: 'fullscreen',
|
||||
action: SimpleMDE.toggleFullScreen,
|
||||
className: cn(styles.iconFullscreen, 'no-disable'),
|
||||
title: 'Toggle Fullscreen',
|
||||
},
|
||||
'|',
|
||||
{
|
||||
name: 'guide',
|
||||
action: 'https://simplemde.com/markdown-guide',
|
||||
className: styles.iconGuide,
|
||||
title: 'Markdown Guide',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default class MarkdownEditor extends Component {
|
||||
|
||||
textarea = null
|
||||
editor = null
|
||||
|
||||
onRef = (ref) => this.textarea = ref
|
||||
|
||||
componentDidMount() {
|
||||
this.editor = new SimpleMDE({
|
||||
...config,
|
||||
element: this.textarea,
|
||||
});
|
||||
this.editor.codemirror.on('change', this.onChange);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.value !== nextProps.value && nextProps.value !== this.editor.value()) {
|
||||
this.editor.value(nextProps.value);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
|
||||
// Workaround empty render issue.
|
||||
// https://github.com/NextStepWebs/simplemde-markdown-editor/issues/313
|
||||
this.editor.codemirror.refresh();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.editor.toTextArea();
|
||||
}
|
||||
|
||||
onChange = () => {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(this.editor.value());
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<textarea ref={this.onRef} {...this.props} onChange={noop} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MarkdownEditor.propTypes = {
|
||||
onChange: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
};
|
||||
@@ -1,41 +0,0 @@
|
||||
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
|
||||
.container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 999;
|
||||
background-color: rgba(0, 0, 0, .4);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.inner {
|
||||
width: 100vw;
|
||||
background-color: #fff;
|
||||
box-shadow: 2px 2px 5px 0px rgba(153,153,153,1);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.close {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@media (--big-viewport) {
|
||||
.inner {
|
||||
width: 600px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Button, Icon} from 'react-mdl';
|
||||
import styles from './Modal.css';
|
||||
|
||||
export default ({open, children, onClose}) => (
|
||||
<div className={`${styles.container} ${!open ? styles.hide : ''}`}>
|
||||
<div className={styles.inner}>
|
||||
<Button className={styles.close} onClick={onClose}><Icon name='close' /></Button>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1,64 +0,0 @@
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.key {
|
||||
background: #eee;
|
||||
padding: 3px;
|
||||
min-width: 15px;
|
||||
min-height: 8px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
.callToAction {
|
||||
position: fixed;
|
||||
left: 10px;
|
||||
bottom: 10px;
|
||||
width: 280px;
|
||||
height: 200px;
|
||||
background: white;
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.15);
|
||||
|
||||
.ctaHeader {
|
||||
font-size: 16px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
p, li {
|
||||
font-size: 12px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
li span:first-child, p:last-child span:first-child {
|
||||
display: inline-block;
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
float: right;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.smallKey {
|
||||
padding: 5px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import Modal from 'components/Modal';
|
||||
import styles from './ModerationKeysModal.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const shortcuts = [
|
||||
{
|
||||
title: 'modqueue.navigation',
|
||||
shortcuts: {
|
||||
'j': 'modqueue.next_comment',
|
||||
'k': 'modqueue.prev_comment',
|
||||
's': 'modqueue.singleview',
|
||||
'?': 'modqueue.thismenu'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'modqueue.actions',
|
||||
shortcuts: {
|
||||
'd': 'modqueue.approve',
|
||||
'f': 'modqueue.reject'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default class ModerationKeysModal extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
open: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
hideShortcutsNote: PropTypes.func.isRequired,
|
||||
shortcutsNoteVisible: PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
render () {
|
||||
const {open, onClose, hideShortcutsNote, shortcutsNoteVisible} = this.props;
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.callToAction} style={{display: shortcutsNoteVisible === 'show' ? 'block' : 'none'}}>
|
||||
<div onClick={hideShortcutsNote} className={styles.closeButton}>×</div>
|
||||
<p className={styles.ctaHeader}>{t('modqueue.mod_faster')}</p>
|
||||
<p><strong>{t('modqueue.try_these')}:</strong></p>
|
||||
<ul>
|
||||
<li><span>{t('modqueue.approve')}</span> <span className={styles.smallKey}>d</span></li>
|
||||
<li><span>{t('modqueue.reject')}</span> <span className={styles.smallKey}>f</span></li>
|
||||
</ul>
|
||||
<p><span>{t('modqueue.view_more_shortcuts')}</span> <span className={styles.smallKey}>{t('modqueue.shift_key')}</span> + <span className={styles.smallKey}>/</span></p>
|
||||
</div>
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<h3>{t('modqueue.shortcuts')}</h3>
|
||||
<div className={styles.container}>
|
||||
{shortcuts.map((shortcut, i) => (
|
||||
<table className={styles.table} key={i}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t(shortcut.title)}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.keys(shortcut.shortcuts).map((key) => (
|
||||
<tr key={`${key}tr`}>
|
||||
<td className={styles.shortcut}><span className={styles.key}>{key}</span></td>
|
||||
<td>{t(shortcut.shortcuts[key])}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.list {
|
||||
padding: 8px 0;
|
||||
list-style: none;
|
||||
display: block;
|
||||
|
||||
&.singleView .listItem {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.singleView .listItem.activeItem {
|
||||
display: block;
|
||||
height: 100%;
|
||||
font-size: 1.5em;
|
||||
line-height: 1.5em;
|
||||
border: none;
|
||||
|
||||
.actions {
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
left: 25%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 50%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.listItem {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
max-width: 660px;
|
||||
min-width: 400px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 14px;
|
||||
position: relative;
|
||||
transition: box-shadow 200ms;
|
||||
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.sideActions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
padding: 40px 18px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.itemHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.author {
|
||||
min-width: 230px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.itemBody {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-right: 16px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: #757575;
|
||||
font-size: 40px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.created {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-top: 20px;
|
||||
flex: 1;
|
||||
font-size: 0.88em;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.flagged {
|
||||
color: rgba(255, 0, 0, .5);
|
||||
padding-top: 15px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.flagCount{
|
||||
font-size: 12px;
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: #444;
|
||||
margin-top: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@media (--big-viewport) {
|
||||
.listItem {
|
||||
border: 1px solid #e0e0e0;
|
||||
margin-bottom: 30px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
&.activeItem {
|
||||
border: 2px solid #333;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.hasLinks {
|
||||
color: #f00;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.banned {
|
||||
color: #f00;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.ban {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.banButton {
|
||||
width: 114px;
|
||||
letter-spacing: 1px;
|
||||
|
||||
i {
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.selected {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
|
||||
.actionButton {
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.minimal {
|
||||
width: 45px;
|
||||
min-width: 0;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.approve__active {
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
background-color: #519954;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.reject__active, .rejected__active {
|
||||
color: white;
|
||||
background-color: #D03235;
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import styles from './ModerationList.css';
|
||||
import key from 'keymaster';
|
||||
import Hammer from 'hammerjs';
|
||||
import Comment from './Comment';
|
||||
import User from './User';
|
||||
import SuspendUserModal from './SuspendUserModal';
|
||||
|
||||
// Each action has different meaning and configuration
|
||||
const menuOptionsMap = {
|
||||
'reject': {status: 'REJECTED', icon: 'close', key: 'f'},
|
||||
'approve': {status: 'ACCEPTED', icon: 'done', key: 'd'},
|
||||
'flag': {status: 'FLAGGED', icon: 'flag', filter: 'Untouched'},
|
||||
'ban': {status: 'BANNED', icon: 'not interested'}
|
||||
};
|
||||
|
||||
// Renders a comment list and allow performing actions
|
||||
export default class ModerationList extends React.Component {
|
||||
static propTypes = {
|
||||
isActive: PropTypes.bool,
|
||||
singleView: PropTypes.bool,
|
||||
commentIds: PropTypes.arrayOf(PropTypes.string),
|
||||
actionIds: PropTypes.arrayOf(PropTypes.string),
|
||||
comments: PropTypes.object,
|
||||
users: PropTypes.object.isRequired,
|
||||
actions: PropTypes.object,
|
||||
userStatusUpdate: PropTypes.func.isRequired,
|
||||
suspendUser: PropTypes.func.isRequired,
|
||||
|
||||
// list of actions (flags, etc) associated with the comments
|
||||
modActions: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
loading: PropTypes.bool,
|
||||
|
||||
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired
|
||||
}
|
||||
|
||||
state = {active: null, suspendUserModal: null, email: null};
|
||||
|
||||
// remove key handlers before leaving
|
||||
componentWillUnmount () {
|
||||
this.unbindKeyHandlers();
|
||||
}
|
||||
|
||||
// add key handlers and gestures
|
||||
componentDidMount () {
|
||||
this.bindKeyHandlers();
|
||||
|
||||
// this.bindGestures() // need to check whether we're on a mobile device or this throws an Error
|
||||
}
|
||||
|
||||
// If entering to singleview and no active, active is the first eleement
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.singleView && !this.state.active) {
|
||||
this.setState({active: nextProps.commentIds[0]});
|
||||
}
|
||||
}
|
||||
|
||||
// Add swipe to approve or reject
|
||||
bindGestures () {
|
||||
const {modActions} = this.props;
|
||||
this._hammer = new Hammer(this.base);
|
||||
this._hammer.get('swipe').set({direction: Hammer.DIRECTION_HORIZONTAL});
|
||||
|
||||
if (modActions.indexOf('reject') !== -1) {
|
||||
this._hammer.on('swipeleft', () => this.props.singleView && this.actionKeyHandler('Rejected'));
|
||||
}
|
||||
if (modActions.indexOf('approve') !== -1) {
|
||||
this._hammer.on('swiperight', () => this.props.singleView && this.actionKeyHandler('Approved'));
|
||||
}
|
||||
}
|
||||
|
||||
// Add key handlers. Each action has one and added j/k for moving around
|
||||
bindKeyHandlers () {
|
||||
const {modActions, isActive} = this.props;
|
||||
modActions.filter((action) => menuOptionsMap[action].key).forEach((action) => {
|
||||
key(menuOptionsMap[action].key, 'moderationList', () => isActive && this.actionKeyHandler(menuOptionsMap[action].status));
|
||||
});
|
||||
key('j', 'moderationList', () => isActive && this.moveKeyHandler('down'));
|
||||
key('k', 'moderationList', () => isActive && this.moveKeyHandler('up'));
|
||||
key.setScope('moderationList');
|
||||
}
|
||||
|
||||
// Perform an action using the keys only if the comment is active
|
||||
actionKeyHandler (action) {
|
||||
if (this.props.isActive && this.state.active) {
|
||||
this.onClickAction(action, this.state.active);
|
||||
}
|
||||
}
|
||||
|
||||
// move around with j/k
|
||||
moveKeyHandler (direction) {
|
||||
if (!this.props.isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {commentIds} = this.props;
|
||||
const {active} = this.state;
|
||||
|
||||
// check boundaries
|
||||
if (active === null || !commentIds.length) {
|
||||
this.setState({active: commentIds[0]});
|
||||
} else if (direction === 'up' && active !== commentIds[0]) {
|
||||
this.setState({active: commentIds[commentIds.indexOf(active) - 1]});
|
||||
} else if (direction === 'down' && active !== commentIds[commentIds.length - 1]) {
|
||||
this.setState({active: commentIds[commentIds.indexOf(active) + 1]});
|
||||
}
|
||||
|
||||
// scroll to the position
|
||||
const index = Math.max(commentIds.indexOf(this.state.active), 0);
|
||||
this.base.childNodes[index] && this.base.childNodes[index].focus();
|
||||
}
|
||||
|
||||
unbindKeyHandlers () {
|
||||
key.deleteScope('moderationList');
|
||||
}
|
||||
|
||||
// If we are performing an action over a comment (aka removing from the list) we need to select a new active.
|
||||
// TODO: In the future this can be improved and look at the actual state to
|
||||
// resolve since the content of the list could change externally. For now it works as expected
|
||||
onClickAction = (menuOption, id, action) => {
|
||||
|
||||
// activate the next comment
|
||||
if (id === this.state.active) {
|
||||
const moderationIds = this.getModerationIds();
|
||||
if (moderationIds[moderationIds.length - 1] === this.state.active) {
|
||||
this.setState({active: moderationIds[moderationIds.length - 2]});
|
||||
} else {
|
||||
this.setState({active: moderationIds[Math.min(moderationIds.indexOf(this.state.active) + 1, moderationIds.length - 1)]});
|
||||
}
|
||||
}
|
||||
|
||||
// Update the status right away if this is a comment
|
||||
if (action.item_type === 'COMMENTS') {
|
||||
this.props.updateCommentStatus(menuOption, id);
|
||||
} else if (action.item_type === 'USERS') {
|
||||
|
||||
// If a user bio or name is rejected, bring up a dialog before suspending them.
|
||||
if (menuOption === 'REJECTED') {
|
||||
this.setState({suspendUserModal: action});
|
||||
} else if (menuOption === 'ACCEPTED') {
|
||||
this.props.userStatusUpdate('APPROVED', action.item_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClickShowBanDialog = (userId, userName, commentId) => {
|
||||
this.props.onClickShowBanDialog(userId, userName, commentId);
|
||||
}
|
||||
|
||||
mapModItems = (itemId, index) => {
|
||||
|
||||
const {comments = {}, users, actions = {}, modActions, suspectWords, hideActive} = this.props;
|
||||
const {active} = this.state;
|
||||
|
||||
// Because ids are unique, the id will either appear as an action or as a comment.
|
||||
|
||||
const item = comments[itemId] || actions[itemId];
|
||||
let modItem;
|
||||
|
||||
if (item.body) {
|
||||
|
||||
// If the item is a comment...
|
||||
const author = users[item.author_id];
|
||||
modItem = <Comment
|
||||
suspectWords={suspectWords}
|
||||
comment={item}
|
||||
author={author}
|
||||
key={index}
|
||||
index={index}
|
||||
onClickAction={this.onClickAction}
|
||||
onClickShowBanDialog={this.onClickShowBanDialog}
|
||||
modActions={modActions}
|
||||
menuOptionsMap={menuOptionsMap}
|
||||
isActive={itemId === active}
|
||||
hideActive={hideActive} />;
|
||||
} else {
|
||||
|
||||
// If the item is an action...
|
||||
const user = users[item.item_id];
|
||||
modItem = user && <User
|
||||
suspectWords={suspectWords}
|
||||
action={item}
|
||||
user={user}
|
||||
key={index}
|
||||
index={index}
|
||||
onClickAction={this.onClickAction}
|
||||
onClickShowBanDialog={this.onClickShowBanDialog}
|
||||
modActions={modActions}
|
||||
menuOptionsMap={menuOptionsMap}
|
||||
isActive={itemId === active}
|
||||
hideActive={hideActive} />;
|
||||
}
|
||||
return modItem;
|
||||
}
|
||||
|
||||
getModerationIds = () => {
|
||||
const {commentIds = [], actionIds = [], comments, actions} = this.props;
|
||||
if (comments && actions) {
|
||||
return [ ...commentIds, ...actionIds ].sort((a, b) => {
|
||||
const itemA = comments[a] || actions[a];
|
||||
const itemB = comments[b] || actions[b];
|
||||
return itemB.updated_at - itemA.updated_at;
|
||||
});
|
||||
} else {
|
||||
return comments ? commentIds : actionIds;
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {singleView, key, suspendUser} = this.props;
|
||||
|
||||
// Combine moderations and actions into a single stream and sort by most recently updated.
|
||||
const moderationIds = this.getModerationIds();
|
||||
|
||||
return (
|
||||
<ul
|
||||
className={`${styles.list} ${singleView ? styles.singleView : ''}`} {...key}
|
||||
id='moderationList'>
|
||||
{moderationIds.map(this.mapModItems)}
|
||||
<SuspendUserModal
|
||||
action = {this.state.suspendUserModal}
|
||||
onClose={() => this.setState({suspendUserModal:null})}
|
||||
suspendUser={suspendUser} />
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
.layout {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loginLayout {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loginHeader, .loginCTA, .forgotPasswordCTA, .passwordRequestSuccess {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.forgotPasswordLink, .signInLink {
|
||||
color: blue;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.forgotPasswordLink:hover, .signInLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.layout h1 {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.loginHeader {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.passwordRequestSuccess {
|
||||
cursor: pointer;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Layout} from 'react-mdl';
|
||||
import styles from './NotFound.css';
|
||||
|
||||
export const NotFound = () => (
|
||||
<Layout fixedDrawer>
|
||||
<div className={styles.layout} >
|
||||
<h1>Page Not Found</h1>
|
||||
<p>The communicorn feels your pain.</p>
|
||||
<img src="https://coralproject.net/images/communicorn.jpg" alt="Communicorn"/>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
@@ -1,90 +0,0 @@
|
||||
.dialog {
|
||||
border: none;
|
||||
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
|
||||
width: 400px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.header {
|
||||
color: black;
|
||||
font-size: 1.5em;
|
||||
font-weight: 500;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.close {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
userSelect: none;
|
||||
outline: none;
|
||||
border: none;
|
||||
touchAction: manipulation;
|
||||
&::-moz-focus-inner: {
|
||||
border: 0;
|
||||
}
|
||||
background: 0;
|
||||
padding: 0;
|
||||
font-size: 24px;
|
||||
line-height: 14px;
|
||||
cursor: pointer;
|
||||
color: #363636;
|
||||
&:hover {
|
||||
color: #6b6b6b;
|
||||
}
|
||||
}
|
||||
|
||||
.legend {
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.radioGroup {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
label.radioGroup {
|
||||
|
||||
&:global(.is-checked) > :global(.mdl-radio__outer-circle),
|
||||
> :global(.mdl-radio__outer-circle) {
|
||||
border-color: #212121;
|
||||
}
|
||||
|
||||
> :global(.mdl-radio__inner-circle) {
|
||||
background: #212121;
|
||||
}
|
||||
|
||||
> :global(.mdl-radio__label) {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.messageInput {
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.perform {
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 6px;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Dialog} from 'coral-ui';
|
||||
import {RadioGroup, Radio} from 'react-mdl';
|
||||
import styles from './SuspendUserDialog.css';
|
||||
|
||||
import Button from 'coral-ui/components/Button';
|
||||
|
||||
import t, {timeago} from 'coral-framework/services/i18n';
|
||||
import {dateAdd} from 'coral-framework/utils';
|
||||
|
||||
const initialState = {step: 0, duration: '3'};
|
||||
|
||||
function durationsToDate(hours) {
|
||||
|
||||
// Add 1 minute more to help `timeago.js` to display the correct duration.
|
||||
return dateAdd(new Date(), 'minute', hours * 60 + 1);
|
||||
}
|
||||
|
||||
class SuspendUserDialog extends React.Component {
|
||||
|
||||
state = initialState;
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
if (this.props.open && !next.open) {
|
||||
this.setState(initialState);
|
||||
}
|
||||
}
|
||||
|
||||
handleDurationChange = (event) => {
|
||||
this.setState({duration: event.target.value});
|
||||
}
|
||||
|
||||
handleMessageChange = (event) => {
|
||||
this.setState({message: event.target.value});
|
||||
}
|
||||
|
||||
goToStep1 = () => {
|
||||
this.setState({
|
||||
step: 1,
|
||||
message: t(
|
||||
'suspenduser.email_message_suspend',
|
||||
this.props.username,
|
||||
this.props.organizationName,
|
||||
timeago(durationsToDate(this.state.duration)),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
handlePerform = () => {
|
||||
|
||||
this.props.onPerform({
|
||||
message: this.state.message,
|
||||
|
||||
// Add 1 minute more to help `timeago.js` to display the correct duration.
|
||||
until: durationsToDate(this.state.duration),
|
||||
});
|
||||
};
|
||||
|
||||
renderStep0() {
|
||||
const {onCancel, username} = this.props;
|
||||
const {duration} = this.state;
|
||||
return (
|
||||
<section>
|
||||
<h1 className={styles.header}>
|
||||
{t('suspenduser.title_suspend')}
|
||||
</h1>
|
||||
<p className={styles.description}>
|
||||
{t('suspenduser.description_suspend', username)}
|
||||
</p>
|
||||
<fieldset>
|
||||
<legend className={styles.legend}>{t('suspenduser.select_duration')}</legend>
|
||||
<RadioGroup
|
||||
name='status filter'
|
||||
value={duration}
|
||||
childContainer='div'
|
||||
onChange={this.handleDurationChange}
|
||||
className={styles.radioGroup}
|
||||
>
|
||||
<Radio value='1'>{t('suspenduser.one_hour')}</Radio>
|
||||
<Radio value='3'>{t('suspenduser.hours', 3)}</Radio>
|
||||
<Radio value='24'>{t('suspenduser.hours', 24)}</Radio>
|
||||
<Radio value='168'>{t('suspenduser.days', 7)}</Radio>
|
||||
</RadioGroup>
|
||||
</fieldset>
|
||||
<div className={styles.buttons}>
|
||||
<Button cStyle="white" className={styles.cancel} onClick={onCancel} raised>
|
||||
{t('suspenduser.cancel')}
|
||||
</Button>
|
||||
<Button cStyle="black" className={styles.perform} onClick={this.goToStep1} raised>
|
||||
{t('suspenduser.suspend_user')}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
renderStep1() {
|
||||
const {onCancel, username} = this.props;
|
||||
const {message} = this.state;
|
||||
return (
|
||||
<section>
|
||||
<h1 className={styles.header}>
|
||||
{t('suspenduser.title_notify')}
|
||||
</h1>
|
||||
<p className={styles.description}>
|
||||
{t('suspenduser.description_notify', username)}
|
||||
</p>
|
||||
<fieldset>
|
||||
<legend className={styles.legend}>{t('suspenduser.write_message')}</legend>
|
||||
<textarea
|
||||
rows={5}
|
||||
className={styles.messageInput}
|
||||
value={message}
|
||||
onChange={this.handleMessageChange} />
|
||||
</fieldset>
|
||||
<div className={styles.buttons}>
|
||||
<Button cStyle="white" className={styles.cancel} onClick={onCancel} raised>
|
||||
{t('suspenduser.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
cStyle="black"
|
||||
className={styles.perform}
|
||||
onClick={this.handlePerform}
|
||||
disabled={this.state.message.length === 0}
|
||||
raised
|
||||
>
|
||||
{t('suspenduser.send')}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {open, onCancel} = this.props;
|
||||
const {step} = this.state;
|
||||
return (
|
||||
<Dialog
|
||||
className={styles.dialog}
|
||||
onCancel={onCancel}
|
||||
open={open}
|
||||
>
|
||||
<div className={styles.close}>
|
||||
<button aria-label="Close" onClick={onCancel} className={styles.closeButton}>×</button>
|
||||
</div>
|
||||
{step === 0 && this.renderStep0()}
|
||||
{step === 1 && this.renderStep1()}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SuspendUserDialog.propTypes = {
|
||||
open: PropTypes.bool.isRequired,
|
||||
onPerform: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
organizationName: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
};
|
||||
|
||||
export default SuspendUserDialog;
|
||||
@@ -1,51 +0,0 @@
|
||||
.root {
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
overflow: hidden;
|
||||
padding-left: 5px;
|
||||
padding-top: 5px;
|
||||
|
||||
}
|
||||
|
||||
.rootFocuesd {
|
||||
border-color: rgb(142, 76, 65);
|
||||
}
|
||||
|
||||
.tag {
|
||||
background-color: rgb(255, 220, 214);
|
||||
border-radius: 2px;
|
||||
border: 1px solid rgb(244, 126, 107);
|
||||
color: rgb(244, 126, 107);
|
||||
display: inline-block;
|
||||
font-family: sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
margin-bottom: 5px;
|
||||
margin-right: 5px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.tagRemove {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
color: rgb(101, 24, 23);
|
||||
|
||||
&::before {
|
||||
content: " ×";
|
||||
}
|
||||
}
|
||||
|
||||
.input > input {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: #777;
|
||||
font-family: sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
margin-bottom: 6px;
|
||||
margin-top: 1px;
|
||||
outline: none;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import React from 'react';
|
||||
import TagsInput from 'react-tagsinput';
|
||||
import styles from './TagsInput.css';
|
||||
import AutosizeInput from 'react-input-autosize';
|
||||
|
||||
const autosizingRenderInput = ({onChange, value, addTag: _, ...other}) =>
|
||||
<AutosizeInput type='text' onChange={onChange} value={value} {...other} />;
|
||||
|
||||
export default (props) => {
|
||||
return (
|
||||
<TagsInput
|
||||
addOnBlur={true}
|
||||
addOnPaste={true}
|
||||
pasteSplit={(data) => data.split(',').map((d) => d.trim())}
|
||||
className={styles.root}
|
||||
focusedClassName={styles.rootFocus}
|
||||
renderInput={autosizingRenderInput}
|
||||
{...props}
|
||||
|
||||
tagProps={{
|
||||
className: styles.tag,
|
||||
classNameRemove: styles.tagRemove,
|
||||
...props.tagProps,
|
||||
}}
|
||||
inputProps={{
|
||||
className: styles.input,
|
||||
...props.inputProps,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,227 +0,0 @@
|
||||
@keyframes :global(bounceInRight) {
|
||||
from, 60%, 75%, 90%, to {
|
||||
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate3d(3000px, 0, 0); }
|
||||
60% {
|
||||
opacity: 1;
|
||||
transform: translate3d(-25px, 0, 0); }
|
||||
75% {
|
||||
transform: translate3d(10px, 0, 0); }
|
||||
90% {
|
||||
transform: translate3d(-5px, 0, 0); }
|
||||
to {
|
||||
transform: none; } }
|
||||
|
||||
@keyframes :global(bounceOutRight) {
|
||||
20% {
|
||||
opacity: 1;
|
||||
transform: translate3d(-20px, 0, 0); }
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translate3d(2000px, 0, 0); } }
|
||||
|
||||
@keyframes :global(bounceInLeft) {
|
||||
from, 60%, 75%, 90%, to {
|
||||
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); }
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(-3000px, 0, 0); }
|
||||
60% {
|
||||
opacity: 1;
|
||||
transform: translate3d(25px, 0, 0); }
|
||||
75% {
|
||||
transform: translate3d(-10px, 0, 0); }
|
||||
90% {
|
||||
transform: translate3d(5px, 0, 0); }
|
||||
to {
|
||||
transform: none; } }
|
||||
|
||||
@keyframes :global(bounceOutLeft) {
|
||||
20% {
|
||||
opacity: 1;
|
||||
transform: translate3d(20px, 0, 0); }
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translate3d(-2000px, 0, 0); } }
|
||||
|
||||
@keyframes :global(bounceInUp) {
|
||||
from, 60%, 75%, 90%, to {
|
||||
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 3000px, 0); }
|
||||
60% {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, -20px, 0); }
|
||||
75% {
|
||||
transform: translate3d(0, 10px, 0); }
|
||||
90% {
|
||||
transform: translate3d(0, -5px, 0); }
|
||||
to {
|
||||
transform: translate3d(0, 0, 0); } }
|
||||
|
||||
@keyframes :global(bounceOutUp) {
|
||||
20% {
|
||||
transform: translate3d(0, -10px, 0); }
|
||||
40%, 45% {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 20px, 0); }
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, -2000px, 0); } }
|
||||
|
||||
@keyframes :global(bounceInDown) {
|
||||
from, 60%, 75%, 90%, to {
|
||||
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); }
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, -3000px, 0); }
|
||||
60% {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 25px, 0); }
|
||||
75% {
|
||||
transform: translate3d(0, -10px, 0); }
|
||||
90% {
|
||||
transform: translate3d(0, 5px, 0); }
|
||||
to {
|
||||
transform: none; } }
|
||||
|
||||
@keyframes :global(bounceOutDown) {
|
||||
20% {
|
||||
transform: translate3d(0, 10px, 0); }
|
||||
40%, 45% {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, -20px, 0); }
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 2000px, 0); } }
|
||||
|
||||
@keyframes :global(track-progress) {
|
||||
0% {
|
||||
width: 100%; }
|
||||
100% {
|
||||
width: 0; } }
|
||||
|
||||
:global {
|
||||
.bounceOutRight, .toast-exit--top-right, .toast-exit--bottom-right {
|
||||
animation-name: bounceOutRight; }
|
||||
|
||||
.bounceInRight, .toast-enter--top-right, .toast-enter--bottom-right {
|
||||
animation-name: bounceInRight; }
|
||||
|
||||
.bounceInLeft, .toast-enter--top-left, .toast-enter--bottom-left {
|
||||
animation-name: bounceInLeft; }
|
||||
|
||||
.bounceOutLeft, .toast-exit--top-left, .toast-exit--bottom-left {
|
||||
animation-name: bounceOutLeft; }
|
||||
|
||||
.bounceInUp, .toast-enter--bottom-center {
|
||||
animation-name: bounceInUp; }
|
||||
.bounceOutUp, .toast-exit--top-center {
|
||||
animation-name: bounceOutUp; }
|
||||
|
||||
.bounceInDown, .toast-enter--top-center {
|
||||
animation-name: bounceInDown; }
|
||||
|
||||
.bounceOutDown, .toast-exit--bottom-center {
|
||||
animation-name: bounceOutDown; }
|
||||
|
||||
.animated {
|
||||
animation-duration: 0.75s;
|
||||
animation-fill-mode: both; }
|
||||
|
||||
.toastify {
|
||||
z-index: 99999;
|
||||
position: fixed;
|
||||
padding: 4px;
|
||||
width: 350px;
|
||||
max-width: 98%;
|
||||
color: #999;
|
||||
box-sizing: border-box; }
|
||||
.toastify--top-left {
|
||||
top: 1em;
|
||||
left: 1em; }
|
||||
.toastify--top-center {
|
||||
top: 1em;
|
||||
left: 50%;
|
||||
margin-left: -175px; }
|
||||
.toastify--top-right {
|
||||
top: 1em;
|
||||
right: 2em; }
|
||||
.toastify--bottom-left {
|
||||
bottom: 1em;
|
||||
left: 1em; }
|
||||
.toastify--bottom-center {
|
||||
bottom: 1em;
|
||||
left: 50%;
|
||||
margin-left: -175px; }
|
||||
.toastify--bottom-right {
|
||||
bottom: 1em;
|
||||
right: 2em; }
|
||||
.toastify__img {
|
||||
float: left;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle; }
|
||||
|
||||
.toastify__close {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
left: 12px;
|
||||
width: 20px;
|
||||
height: 16px;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
transition: .3s ease; }
|
||||
.toastify__close:hover, .toastify__close:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.toastify-content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
padding: 18px 24px 20px 48px;
|
||||
box-sizing: border-box;
|
||||
background: #404040;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 15px 0 rgba(0, 0, 0, 0.1), 0 3px 20px 0 rgba(0, 0, 0, 0.05); }
|
||||
.toastify-content--info {
|
||||
background: #404040; }
|
||||
.toastify-content--success {
|
||||
background: #008577; }
|
||||
.toastify-content--warning {
|
||||
background: #ef6c2b; }
|
||||
.toastify-content--error {
|
||||
background: #ef342b; }
|
||||
|
||||
.toastify__body {
|
||||
color: white;
|
||||
overflow-x: scroll;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.toastify__progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 4px;
|
||||
z-index: 999;
|
||||
opacity: 0.8;
|
||||
border-radius: 2px;
|
||||
animation: track-progress linear 1;
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import './ToastContainer.css';
|
||||
import {defaultProps} from 'recompose';
|
||||
import {ToastContainer} from 'react-toastify';
|
||||
|
||||
export default defaultProps({
|
||||
autoClose: 5000,
|
||||
})(ToastContainer);
|
||||
@@ -1,62 +0,0 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Navigation, Drawer} from 'react-mdl';
|
||||
import {IndexLink, Link} from 'react-router';
|
||||
import styles from './Drawer.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
|
||||
const CoralDrawer = ({handleLogout, auth}) => (
|
||||
<Drawer className={styles.header}>
|
||||
{ auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
|
||||
<div>
|
||||
<Navigation className={styles.nav}>
|
||||
<IndexLink
|
||||
className={styles.navLink}
|
||||
to="/admin/dashboard"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.dashboard')}
|
||||
</IndexLink>
|
||||
{
|
||||
can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
<Link
|
||||
className={styles.navLink}
|
||||
to="/admin/moderate"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.moderate')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
<Link className={styles.navLink}
|
||||
to="/admin/stories"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.stories')}
|
||||
</Link>
|
||||
<Link className={styles.navLink}
|
||||
to="/admin/community"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.community')}
|
||||
</Link>
|
||||
{
|
||||
can(auth.user, 'UPDATE_CONFIG') &&
|
||||
(
|
||||
<Link
|
||||
className={styles.navLink}
|
||||
to="/admin/configure"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.configure')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
<a onClick={handleLogout}>Sign Out</a>
|
||||
<span>{`v${process.env.VERSION}`}</span>
|
||||
</Navigation>
|
||||
</div> : null }
|
||||
</Drawer>
|
||||
);
|
||||
|
||||
CoralDrawer.propTypes = {
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
restricted: PropTypes.bool // hide app elements from a logged out user
|
||||
};
|
||||
|
||||
export default CoralDrawer;
|
||||
@@ -1,103 +0,0 @@
|
||||
@custom-media --table-viewport (max-width: 1024px);
|
||||
|
||||
@media (--table-viewport) {
|
||||
.logo {
|
||||
margin-left: 58px;
|
||||
h1 {
|
||||
background: #696969;
|
||||
}
|
||||
span {
|
||||
color: #fcfcfc;
|
||||
}
|
||||
}
|
||||
.nav {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
min-height: 58px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header > div {
|
||||
background-color: #696969;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
min-width: 1280px;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
|
||||
height: 58px;
|
||||
}
|
||||
|
||||
.rightPanel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 170px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.rightPanel > ul {
|
||||
list-style: none;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.rightPanel > ul > li {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
margin-left: 15px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
line-height: 33px;
|
||||
}
|
||||
|
||||
.rightPanel a {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.rightPanel .settings {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.rightPanel .settings > div {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rightPanel .settings:hover {
|
||||
background: rgba(158, 158, 158, 0.69);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.navLink {
|
||||
padding: 0 20px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
background-color: transparent;
|
||||
transition: background-color 200ms;
|
||||
font-weight: 100;
|
||||
letter-spacing: .8;
|
||||
|
||||
&:hover {
|
||||
background-color: #404040;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #232323;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.nav {
|
||||
overflow: hidden;
|
||||
height: 58px !important;
|
||||
}
|
||||
|
||||
.nav .navLink {
|
||||
padding: 0 20px;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Navigation, Header, IconButton, MenuItem, Menu} from 'react-mdl';
|
||||
import {Link, IndexLink} from 'react-router';
|
||||
import styles from './Header.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {Logo} from './Logo';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
|
||||
const CoralHeader = ({
|
||||
handleLogout,
|
||||
showShortcuts = () => {},
|
||||
auth
|
||||
}) => (
|
||||
<Header className={styles.header}>
|
||||
<Logo className={styles.logo} />
|
||||
<div>
|
||||
{
|
||||
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
|
||||
<Navigation className={styles.nav}>
|
||||
<IndexLink
|
||||
id='dashboardNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/dashboard"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.dashboard')}
|
||||
</IndexLink>
|
||||
{
|
||||
can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
<Link
|
||||
id='moderateNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/moderate"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.moderate')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
<Link
|
||||
id='streamsNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/stories"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.stories')}
|
||||
</Link>
|
||||
<Link
|
||||
id='communityNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/community"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.community')}
|
||||
</Link>
|
||||
{
|
||||
can(auth.user, 'UPDATE_CONFIG') && (
|
||||
<Link
|
||||
id='configureNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/configure"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.configure')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
</Navigation>
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className={styles.rightPanel}>
|
||||
<ul>
|
||||
<li className={styles.settings}>
|
||||
<div>
|
||||
<IconButton name="settings" id="menu-settings"/>
|
||||
<Menu target="menu-settings" align="right">
|
||||
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://github.com/coralproject/talk/releases" target="_blank">
|
||||
View latest version
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://coralproject.net/contribute.html#other-ideas-and-bug-reports" target="_blank">
|
||||
Report a bug or give feedback
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleLogout}>
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
{`v${process.env.VERSION}`}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Header>
|
||||
);
|
||||
|
||||
CoralHeader.propTypes = {
|
||||
auth: PropTypes.object,
|
||||
showShortcuts: PropTypes.func,
|
||||
handleLogout: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default CoralHeader;
|
||||
@@ -1,5 +0,0 @@
|
||||
.layout {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {Layout as LayoutMDL} from 'react-mdl';
|
||||
import Header from './Header';
|
||||
import Drawer from './Drawer';
|
||||
import styles from './Layout.css';
|
||||
|
||||
const Layout = ({
|
||||
children,
|
||||
handleLogout = () => {},
|
||||
toggleShortcutModal,
|
||||
restricted = false,
|
||||
...props}) => (
|
||||
<LayoutMDL className={styles.layout} fixedDrawer>
|
||||
<Header
|
||||
handleLogout={handleLogout}
|
||||
showShortcuts={toggleShortcutModal}
|
||||
{...props} />
|
||||
<Drawer handleLogout={handleLogout} restricted={restricted} {...props} />
|
||||
<div className={styles.layout}>
|
||||
{children}
|
||||
</div>
|
||||
</LayoutMDL>
|
||||
);
|
||||
|
||||
Layout.propTypes = {
|
||||
handleLogout: PropTypes.func,
|
||||
toggleShortcutModal: PropTypes.func,
|
||||
restricted: PropTypes.bool // hide elements from a user that's logged out
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@@ -1,29 +0,0 @@
|
||||
.logo h1 {
|
||||
color: #272727;
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
line-height: 60px;
|
||||
padding-left: 13px;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.logo span {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
font-size: 18px;
|
||||
vertical-align: middle;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.logo {
|
||||
background: #E5E5E5;
|
||||
height: 100%;
|
||||
width: 128px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.base {
|
||||
stroke: #E5E5E5;
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import React from 'react';
|
||||
import styles from './Logo.css';
|
||||
import {CoralLogo} from 'coral-ui';
|
||||
|
||||
export const Logo = ({className = ''}) => (
|
||||
<div className={`${styles.logo} ${className}`}>
|
||||
<h1>
|
||||
<CoralLogo className={styles.base} />
|
||||
<span>Talk</span>
|
||||
</h1>
|
||||
</div>
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
export const ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS = 'ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS';
|
||||
@@ -1,9 +0,0 @@
|
||||
export const FETCH_ASSETS_REQUEST = 'FETCH_ASSETS_REQUEST';
|
||||
export const FETCH_ASSETS_SUCCESS = 'FETCH_ASSETS_SUCCESS';
|
||||
export const FETCH_ASSETS_FAILURE = 'FETCH_ASSETS_FAILURE';
|
||||
|
||||
export const UPDATE_ASSET_STATE_REQUEST = 'UPDATE_ASSET_STATE_REQUEST';
|
||||
export const UPDATE_ASSET_STATE_SUCCESS = 'UPDATE_ASSET_STATE_SUCCESS';
|
||||
export const UPDATE_ASSET_STATE_FAILURE = 'UPDATE_ASSET_STATE_FAILURE';
|
||||
|
||||
export const UPDATE_ASSETS = 'UPDATE_ASSETS';
|
||||
@@ -1,14 +0,0 @@
|
||||
export const CHECK_LOGIN_REQUEST = 'CHECK_LOGIN_REQUEST';
|
||||
export const CHECK_LOGIN_SUCCESS = 'CHECK_LOGIN_SUCCESS';
|
||||
export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE';
|
||||
|
||||
export const LOGOUT = 'LOGOUT';
|
||||
|
||||
export const LOGIN_REQUEST = 'LOGIN_REQUEST';
|
||||
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
|
||||
export const LOGIN_FAILURE = 'LOGIN_FAILURE';
|
||||
export const LOGIN_MAXIMUM_EXCEEDED = 'LOGIN_MAXIMUM_EXCEEDED';
|
||||
|
||||
export const FETCH_FORGOT_PASSWORD_REQUEST = 'FETCH_FORGOT_PASSWORD_REQUEST';
|
||||
export const FETCH_FORGOT_PASSWORD_SUCCESS = 'FETCH_FORGOT_PASSWORD_SUCCESS';
|
||||
export const FETCH_FORGOT_PASSWORD_FAILURE = 'FETCH_FORGOT_PASSWORD_FAILURE';
|
||||
@@ -1,2 +0,0 @@
|
||||
export const SHOW_BAN_USER_DIALOG = 'SHOW_BAN_USER_DIALOG';
|
||||
export const HIDE_BAN_USER_DIALOG = 'HIDE_BAN_USER_DIALOG';
|
||||
@@ -1,13 +0,0 @@
|
||||
export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG';
|
||||
export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG';
|
||||
export const SHOW_SUSPENDUSER_DIALOG = 'SHOW_SUSPENDUSER_DIALOG';
|
||||
export const HIDE_SUSPENDUSER_DIALOG = 'HIDE_SUSPENDUSER_DIALOG';
|
||||
export const COMMENTS_MODERATION_QUEUE_FETCH_REQUEST = 'COMMENTS_MODERATION_QUEUE_FETCH_REQUEST';
|
||||
export const COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS = 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS';
|
||||
export const COMMENT_CREATE_SUCCESS = 'COMMENT_CREATE_SUCCESS';
|
||||
export const COMMENT_CREATE_FAILED = 'COMMENT_CREATE_FAILED';
|
||||
export const COMMENT_STREAM_FETCH_SUCCESS = 'COMMENT_STREAM_FETCH_SUCCESS';
|
||||
export const COMMENT_STATUS_UPDATE_SUCCESS = 'COMMENT_STATUS_UPDATE_SUCCESS';
|
||||
export const COMMENT_STATUS_UPDATE_FAILURE = 'COMMENT_STATUS_UPDATE_FAILURE';
|
||||
export const COMMENT_STATUS_UPDATE_REQUEST = 'COMMENT_STATUS_UPDATE_REQUEST';
|
||||
export const COMMENT_FLAG = 'COMMENT_FLAG';
|
||||
@@ -1,17 +0,0 @@
|
||||
export const FETCH_COMMENTERS_REQUEST = 'FETCH_COMMENTERS_REQUEST';
|
||||
export const FETCH_COMMENTERS_SUCCESS = 'FETCH_COMMENTERS_SUCCESS';
|
||||
export const FETCH_COMMENTERS_FAILURE = 'FETCH_COMMENTERS_FAILURE';
|
||||
export const SORT_UPDATE = 'SORT_UPDATE';
|
||||
export const COMMENTERS_NEW_PAGE = 'COMMENTERS_NEW_PAGE';
|
||||
export const SET_ROLE = 'SET_ROLE';
|
||||
export const SET_COMMENTER_STATUS = 'SET_COMMENTER_STATUS';
|
||||
|
||||
export const FETCH_FLAGGED_COMMENTERS_REQUEST = 'FETCH_FLAGGED_COMMENTERS_REQUEST';
|
||||
export const FETCH_FLAGGED_COMMENTERS_SUCCESS = 'FETCH_FLAGGED_COMMENTERS_SUCCESS';
|
||||
export const FETCH_FLAGGED_COMMENTERS_FAILURE = 'FETCH_FLAGGED_COMMENTERS_FAILURE';
|
||||
|
||||
export const SHOW_BANUSER_DIALOG = 'SHOW_BANUSER_DIALOG';
|
||||
export const HIDE_BANUSER_DIALOG = 'HIDE_BANUSER_DIALOG';
|
||||
|
||||
export const SHOW_REJECT_USERNAME_DIALOG = 'SHOW_REJECT_USERNAME_DIALOG';
|
||||
export const HIDE_REJECT_USERNAME_DIALOG = 'HIDE_REJECT_USERNAME_DIALOG';
|
||||
@@ -1,17 +0,0 @@
|
||||
export const NEXT_STEP = 'NEXT_STEP';
|
||||
export const GO_TO_STEP = 'GO_TO_STEP';
|
||||
export const PREVIOUS_STEP = 'PREVIOUS_STEP';
|
||||
export const ADD_ERROR = 'ADD_ERROR';
|
||||
export const HAS_ERROR = 'HAS_ERROR';
|
||||
export const SHOW_ERRORS = 'SHOW_ERRORS';
|
||||
export const CLEAR_ERRORS = 'CLEAR_ERRORS';
|
||||
export const INSTALL_REQUEST = 'INSTALL_REQUEST';
|
||||
export const INSTALL_SUCCESS = 'INSTALL_SUCCESS';
|
||||
export const INSTALL_FAILURE = 'INSTALL_FAILURE';
|
||||
export const UPDATE_FORMDATA_USER = 'UPDATE_FORMDATA_USER';
|
||||
export const UPDATE_FORMDATA_SETTINGS = 'UPDATE_FORMDATA_SETTINGS';
|
||||
export const UPDATE_PERMITTED_DOMAINS_SETTINGS = 'UPDATE_PERMITTED_DOMAINS_SETTINGS';
|
||||
|
||||
export const CHECK_INSTALL_REQUEST = 'CHECK_INSTALL_REQUEST';
|
||||
export const CHECK_INSTALL_SUCCESS = 'CHECK_INSTALL_SUCCESS';
|
||||
export const CHECK_INSTALL_FAILURE = 'CHECK_INSTALL_FAILURE';
|
||||
@@ -1,14 +0,0 @@
|
||||
export const TOGGLE_MODAL = 'TOGGLE_MODAL';
|
||||
export const SINGLE_VIEW = 'SINGLE_VIEW';
|
||||
export const HIDE_SHORTCUTS_NOTE = 'HIDE_SHORTCUTS_NOTE';
|
||||
export const VIEW_USER_DETAIL = 'VIEW_USER_DETAIL';
|
||||
export const HIDE_USER_DETAIL = 'HIDE_USER_DETAIL';
|
||||
export const SET_SORT_ORDER = 'MODERATION_SET_SORT_ORDER';
|
||||
export const CHANGE_USER_DETAIL_STATUSES = 'CHANGE_USER_DETAIL_STATUSES';
|
||||
export const SELECT_USER_DETAIL_COMMENT = 'SELECT_USER_DETAIL_COMMENT';
|
||||
export const UNSELECT_USER_DETAIL_COMMENT = 'UNSELECT_USER_DETAIL_COMMENT';
|
||||
export const CLEAR_USER_DETAIL_SELECTIONS = 'CLEAR_USER_DETAIL_SELECTIONS';
|
||||
export const SHOW_STORY_SEARCH = 'SHOW_STORY_SEARCH';
|
||||
export const HIDE_STORY_SEARCH = 'HIDE_STORY_SEARCH';
|
||||
export const STORY_SEARCH_CHANGE_VALUE = 'STORY_SEARCH_CHANGE_VALUE';
|
||||
export const MODERATION_CLEAR_STATE = 'MODERATION_CLEAR_STATE';
|
||||
@@ -1,2 +0,0 @@
|
||||
export const SHOW_SUSPEND_USER_DIALOG = 'SHOW_SUSPEND_USER_DIALOG';
|
||||
export const HIDE_SUSPEND_USER_DIALOG = 'HIDE_SUSPEND_USER_DIALOG';
|
||||
@@ -1,5 +0,0 @@
|
||||
export const UPDATE_STATUS_REQUEST = 'UPDATE_STATUS_REQUEST';
|
||||
export const UPDATE_STATUS_SUCCESS = 'UPDATE_STATUS_SUCCESS';
|
||||
export const UPDATE_STATUS_FAILURE = 'UPDATE_STATUS_FAILURE';
|
||||
export const USER_EMAIL_FAILURE = 'USER_EMAIL_FAILURE';
|
||||
export const USERNAME_ENABLE_FAILURE = 'USERNAME_ENABLE_FAILURE';
|
||||
@@ -1,63 +0,0 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import BanUserDialog from '../components/BanUserDialog';
|
||||
import {hideBanUserDialog} from '../actions/banUserDialog';
|
||||
import {withSetUserStatus, withSetCommentStatus} from 'coral-framework/graphql/mutations';
|
||||
import {compose} from 'react-apollo';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class BanUserDialogContainer extends Component {
|
||||
|
||||
banUser = async () => {
|
||||
const {userId, commentId, commentStatus, setUserStatus, setCommentStatus, hideBanUserDialog} = this.props;
|
||||
await setUserStatus({userId, status: 'BANNED'});
|
||||
hideBanUserDialog();
|
||||
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
|
||||
await setCommentStatus({commentId, status: 'REJECTED'});
|
||||
}
|
||||
}
|
||||
|
||||
getInfo() {
|
||||
let note = t('bandialog.note_ban_user');
|
||||
if (this.props.commentStatus && this.props.commentStatus !== 'REJECTED') {
|
||||
note = t('bandialog.note_reject_comment');
|
||||
}
|
||||
return t('bandialog.note', note);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<BanUserDialog
|
||||
open={this.props.open}
|
||||
onPerform={this.banUser}
|
||||
onCancel={this.props.hideBanUserDialog}
|
||||
username={this.props.username}
|
||||
info={this.getInfo()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({banUserDialog: {open, userId, username, commentId, commentStatus}}) => ({
|
||||
open,
|
||||
userId,
|
||||
username,
|
||||
commentId,
|
||||
commentStatus,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
...bindActionCreators({
|
||||
hideBanUserDialog,
|
||||
}, dispatch),
|
||||
});
|
||||
|
||||
export default compose(
|
||||
withSetUserStatus,
|
||||
withSetCommentStatus,
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
),
|
||||
)(BanUserDialogContainer);
|
||||
@@ -1,91 +0,0 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import Layout from '../components/ui/Layout';
|
||||
import {fetchConfig} from '../actions/config';
|
||||
import AdminLogin from '../components/AdminLogin';
|
||||
import {logout} from 'coral-framework/actions/auth';
|
||||
import {FullLoading} from '../components/FullLoading';
|
||||
import BanUserDialog from './BanUserDialog';
|
||||
import SuspendUserDialog from './SuspendUserDialog';
|
||||
import {toggleModal as toggleShortcutModal} from '../actions/moderation';
|
||||
import {checkLogin, handleLogin, requestPasswordReset} from '../actions/auth';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
|
||||
class LayoutContainer extends Component {
|
||||
componentWillMount() {
|
||||
const {checkLogin, fetchConfig} = this.props;
|
||||
|
||||
checkLogin();
|
||||
fetchConfig();
|
||||
}
|
||||
render() {
|
||||
const {
|
||||
user,
|
||||
loggedIn,
|
||||
loadingUser,
|
||||
loginError,
|
||||
loginMaxExceeded,
|
||||
passwordRequestSuccess
|
||||
} = this.props.auth;
|
||||
|
||||
const {
|
||||
handleLogout,
|
||||
toggleShortcutModal,
|
||||
TALK_RECAPTCHA_PUBLIC
|
||||
} = this.props;
|
||||
if (loadingUser) {
|
||||
return <FullLoading />;
|
||||
}
|
||||
if (!loggedIn) {
|
||||
return (
|
||||
<AdminLogin
|
||||
loginMaxExceeded={loginMaxExceeded}
|
||||
handleLogin={this.props.handleLogin}
|
||||
requestPasswordReset={this.props.requestPasswordReset}
|
||||
passwordRequestSuccess={passwordRequestSuccess}
|
||||
recaptchaPublic={TALK_RECAPTCHA_PUBLIC}
|
||||
errorMessage={loginError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (can(user, 'ACCESS_ADMIN') && loggedIn) {
|
||||
return (
|
||||
<Layout
|
||||
handleLogout={handleLogout}
|
||||
toggleShortcutModal={toggleShortcutModal}
|
||||
{...this.props}
|
||||
>
|
||||
<BanUserDialog />
|
||||
<SuspendUserDialog />
|
||||
{this.props.children}
|
||||
</Layout>
|
||||
);
|
||||
} else if (loggedIn) {
|
||||
return (
|
||||
<Layout {...this.props}>
|
||||
<p>This page is for team use only. Please contact an administrator if you want to join this team.</p>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
return <FullLoading />;
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
auth: state.auth.toJS(),
|
||||
TALK_RECAPTCHA_PUBLIC: state.config
|
||||
.get('data')
|
||||
.get('TALK_RECAPTCHA_PUBLIC', null)
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
checkLogin: () => dispatch(checkLogin()),
|
||||
fetchConfig: () => dispatch(fetchConfig()),
|
||||
handleLogin: (username, password, recaptchaResponse) =>
|
||||
dispatch(handleLogin(username, password, recaptchaResponse)),
|
||||
requestPasswordReset: (email) => dispatch(requestPasswordReset(email)),
|
||||
toggleShortcutModal: (toggle) => dispatch(toggleShortcutModal(toggle)),
|
||||
handleLogout: () => dispatch(logout())
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user