mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1a3a5ca28 | ||
|
|
8ef44dc6f5 | ||
|
|
362f29f77e | ||
|
|
08d342ea2e | ||
|
|
5d32c4ec03 | ||
|
|
50d4e2bb9b | ||
|
|
debdd3eae4 | ||
|
|
286d9baddf | ||
|
|
9c06a63afd | ||
|
|
f38e72c4df | ||
|
|
e2fac4a2dc | ||
|
|
ba0513fd6c | ||
|
|
8e29f0094c | ||
|
|
f60c7e3ca6 | ||
|
|
b3c034d89d | ||
|
|
be2657eb30 | ||
|
|
fd32d2aa24 | ||
|
|
809b39ae2a | ||
|
|
6197981480 | ||
|
|
eb58dd52f8 | ||
|
|
f5c011f44b | ||
|
|
396ccbd6c2 | ||
|
|
4fd61b3731 | ||
|
|
cbd795dc95 | ||
|
|
65a6b484a9 | ||
|
|
45c193b9bf | ||
|
|
ed037f8461 | ||
|
|
7bb0c73ee4 | ||
|
|
f13654b297 | ||
|
|
4a165c0972 | ||
|
|
bbf034a595 | ||
|
|
9f6ddd3801 | ||
|
|
34184d07eb | ||
|
|
057d083341 | ||
|
|
c5f107a168 | ||
|
|
17a801d6d2 | ||
|
|
888a61f895 | ||
|
|
9edf1d7c41 | ||
|
|
b802fdd5eb | ||
|
|
1a7b3fe903 | ||
|
|
cadbbd0b5c | ||
|
|
b6101b880d | ||
|
|
afb9fdcdc1 | ||
|
|
53abb65da1 | ||
|
|
7b5192d40c | ||
|
|
8fdea8734a | ||
|
|
588fbb0eb9 | ||
|
|
a28024dfed | ||
|
|
343eb3e8b6 | ||
|
|
80319f9aca | ||
|
|
33e1e5e729 | ||
|
|
ea2b2bc2e3 | ||
|
|
6ca2f21bc2 | ||
|
|
0d9b795740 | ||
|
|
73482d68ac | ||
|
|
e837dfc3c1 | ||
|
|
2cd9a71d62 | ||
|
|
ad73fcd94b | ||
|
|
66145d6fd5 | ||
|
|
7d246bba76 | ||
|
|
0ce0d57a96 | ||
|
|
792bc713f2 | ||
|
|
399f5ef7ba | ||
|
|
14c0945687 | ||
|
|
52f2f48385 | ||
|
|
e52605e26b | ||
|
|
ae4d6e2424 | ||
|
|
968d20e744 | ||
|
|
49d2416172 | ||
|
|
aac7cae6d6 | ||
|
|
f4fd843efe | ||
|
|
c4de69218c | ||
|
|
94ae029a4a | ||
|
|
1dc1f91b72 | ||
|
|
c98216cf3c | ||
|
|
2c9f8586d3 | ||
|
|
4ce5d97e3b | ||
|
|
3ea2632187 | ||
|
|
c9b709ad43 | ||
|
|
dcd7145510 | ||
|
|
d3896ed97d | ||
|
|
47a3cba5f5 | ||
|
|
0f1cabe126 | ||
|
|
5f62083c2f | ||
|
|
3cabe50a7b | ||
|
|
c0efc33044 | ||
|
|
9344c1bc80 | ||
|
|
1bfe0fbac1 | ||
|
|
b3d704be76 | ||
|
|
3c100160fb | ||
|
|
49c7d2eeda | ||
|
|
cff9b29a36 | ||
|
|
5a1f93975c | ||
|
|
cd6da751a6 | ||
|
|
41610ccf57 | ||
|
|
607a59ad63 | ||
|
|
c0fea4821b | ||
|
|
8432df15a1 | ||
|
|
61486caa9e | ||
|
|
7f45f7e2e2 | ||
|
|
9e511817a8 | ||
|
|
4015e19040 | ||
|
|
c1fcc82b62 | ||
|
|
893c8d32b2 | ||
|
|
2dfadebd42 | ||
|
|
a2d5af1372 | ||
|
|
7a5416a807 | ||
|
|
8c2c632f52 | ||
|
|
fabec2c89b | ||
|
|
7f18603fd5 | ||
|
|
3a69e0cf96 | ||
|
|
7ad3d247b5 | ||
|
|
130dc578b3 | ||
|
|
fd7114ab2a | ||
|
|
5e4bdf1f0c | ||
|
|
71e1b69afe | ||
|
|
6fa18f6c7c | ||
|
|
bda9b55562 | ||
|
|
d843320c4a | ||
|
|
8a88228d9c | ||
|
|
0a9664d464 | ||
|
|
b3749b9332 | ||
|
|
aecb7de019 | ||
|
|
ade3cf6252 | ||
|
|
1837ae4a0f | ||
|
|
334074a7ca | ||
|
|
f93c4adc97 | ||
|
|
bb54009567 | ||
|
|
c126d217ad | ||
|
|
8b2dc3a46d | ||
|
|
d0eca26d5b | ||
|
|
b742923897 | ||
|
|
1781b926d9 | ||
|
|
9222ee57a5 | ||
|
|
67cca7d85d | ||
|
|
7b97a8fca2 | ||
|
|
6bff2de371 | ||
|
|
05283b08f7 | ||
|
|
838687a3ae | ||
|
|
7b0213299a | ||
|
|
f4dbc7a24d | ||
|
|
161e8b4117 | ||
|
|
6f3535984c | ||
|
|
e3557c1f7a | ||
|
|
692b6af09d | ||
|
|
25932e10d6 | ||
|
|
ec13a001a2 | ||
|
|
702a616e6c | ||
|
|
a3dd208fbd | ||
|
|
55c3406990 | ||
|
|
197fbc33e0 | ||
|
|
008a4fd7d5 | ||
|
|
8c5665095f | ||
|
|
647b2ea0c8 | ||
|
|
e61f88b98d | ||
|
|
b024421bbb | ||
|
|
ce95467145 | ||
|
|
1564b380b1 | ||
|
|
593b8959c5 | ||
|
|
3fda3b4b7c | ||
|
|
7e680038d6 | ||
|
|
71b8bb5d60 | ||
|
|
cfbafdeb5c | ||
|
|
c19d4a433a | ||
|
|
13cc80bd35 | ||
|
|
1eea2daeae | ||
|
|
5b29e7d351 | ||
|
|
3af39d99e4 | ||
|
|
1abf988366 | ||
|
|
205f943ab5 | ||
|
|
879683da4f | ||
|
|
974e884432 | ||
|
|
846319f1a9 | ||
|
|
e5237ae805 | ||
|
|
b9d88251e6 | ||
|
|
0d720626d4 | ||
|
|
50b2c6aa88 | ||
|
|
382019d907 | ||
|
|
97103739c2 | ||
|
|
9cf13691ad | ||
|
|
e6107173fb | ||
|
|
1d9ec7a779 | ||
|
|
0c345fe913 | ||
|
|
b6f06f1e89 | ||
|
|
aea3a7449e | ||
|
|
4db73ae73a | ||
|
|
2af7acd3db | ||
|
|
da9847b88f | ||
|
|
5b9f9f1b0d | ||
|
|
5427f295f6 | ||
|
|
620273769f | ||
|
|
dff3d79569 | ||
|
|
0904453e41 | ||
|
|
90eafb557c | ||
|
|
bcb294c625 | ||
|
|
b070a7cc52 | ||
|
|
e48d89a607 | ||
|
|
cb57e5c1e1 | ||
|
|
bf363621d3 | ||
|
|
4dbd1e0bb3 | ||
|
|
cbda970437 | ||
|
|
eecd814e53 | ||
|
|
fb4b6330da | ||
|
|
eb24b5c027 | ||
|
|
3346f5de77 | ||
|
|
1a69cea9f4 | ||
|
|
d763f0b8f6 | ||
|
|
6bb98a00dc | ||
|
|
2edb8e8d0a | ||
|
|
c3e162dd56 | ||
|
|
3cdd061afd | ||
|
|
aaaf37b8db | ||
|
|
7af5f04e45 | ||
|
|
65a166b860 | ||
|
|
eb4bcb71c4 | ||
|
|
6a04b66d84 | ||
|
|
30ae9a82c1 | ||
|
|
ec06f2a392 | ||
|
|
f461f0884b | ||
|
|
d55c329f76 | ||
|
|
0f63e076a6 | ||
|
|
1012b6b28b | ||
|
|
421352c75c | ||
|
|
a170c107e2 | ||
|
|
104617d644 | ||
|
|
5b1773ba26 | ||
|
|
cd0aeca87c | ||
|
|
5c65702032 | ||
|
|
66445aae59 | ||
|
|
23156f3d5e | ||
|
|
76bb6df084 | ||
|
|
54c99c7fcc | ||
|
|
80fc5e86ff | ||
|
|
eddd63ae1a | ||
|
|
e55ed489f2 | ||
|
|
95fb33e41b | ||
|
|
404c900de7 | ||
|
|
dc773f486b | ||
|
|
db679fb6ca | ||
|
|
a2458e0dd0 | ||
|
|
f22507e313 | ||
|
|
8cfc0caad3 | ||
|
|
7d8b36e00c | ||
|
|
50b920b799 | ||
|
|
b4f5121382 | ||
|
|
b9828bcf11 | ||
|
|
0ad0577ba3 | ||
|
|
46d6c3a8c6 | ||
|
|
b40ed1d217 | ||
|
|
ab3ceb151a | ||
|
|
d393c6fc4d | ||
|
|
e6c6391958 | ||
|
|
f2857a8085 | ||
|
|
30a5a08769 | ||
|
|
16a884d778 | ||
|
|
f403281978 | ||
|
|
c1d547e1b1 | ||
|
|
d94003475c | ||
|
|
dd8a11e241 | ||
|
|
7699e440e4 | ||
|
|
0ad2da9baa | ||
|
|
c287b842f3 | ||
|
|
b6d2092048 | ||
|
|
ae9f5d4e8e | ||
|
|
eab0b40e2c | ||
|
|
0aee78ac5f | ||
|
|
56cb8962bd | ||
|
|
5747fd84ad | ||
|
|
d3666a9388 | ||
|
|
d836e8203f | ||
|
|
458927a6e3 | ||
|
|
0267782386 | ||
|
|
32bdf1c1af | ||
|
|
a977ce4ac9 | ||
|
|
18f6121797 | ||
|
|
59531f32dc | ||
|
|
03ae85f5ac | ||
|
|
8ff27d5e8c | ||
|
|
7ecf51a159 | ||
|
|
f4513912d4 | ||
|
|
a7f7e6fa9e | ||
|
|
73464361e4 | ||
|
|
56c6633ef6 | ||
|
|
143d23bf9d | ||
|
|
111b1b744f | ||
|
|
29f8762ed7 | ||
|
|
959e5beb3c | ||
|
|
7417a972b6 | ||
|
|
3da62f3471 | ||
|
|
cc668ef439 | ||
|
|
18fa98d8ea | ||
|
|
a245c2e7ea | ||
|
|
a1745b26f7 | ||
|
|
35ca970650 | ||
|
|
6e8ab4f141 | ||
|
|
e48c2b9fd5 | ||
|
|
8c20376c95 | ||
|
|
93813adf97 | ||
|
|
ee81a3d280 | ||
|
|
41fab3e11c | ||
|
|
dae4eb123c | ||
|
|
6b6e59791a | ||
|
|
ef97ef7217 | ||
|
|
d407ac0c4a | ||
|
|
82b580d3a3 | ||
|
|
c2a9e0290c | ||
|
|
01609a51f5 | ||
|
|
ccff6ed63b | ||
|
|
d9a1d739dc | ||
|
|
05088a7744 | ||
|
|
2387b46ba1 | ||
|
|
375c8789a4 | ||
|
|
7a13b64fbe | ||
|
|
b8a12aef2f | ||
|
|
8feb67f2ea | ||
|
|
773eb34c3e | ||
|
|
5835845092 | ||
|
|
7627d37e22 | ||
|
|
6674b657ab | ||
|
|
9c3d9e5943 | ||
|
|
636bc9efa4 | ||
|
|
6f66ff6ccf | ||
|
|
bdf5b0c6a1 | ||
|
|
ef86938a0e | ||
|
|
01f7aa52a0 | ||
|
|
6c73e5b818 | ||
|
|
3ac392e358 | ||
|
|
18d58e4ea8 | ||
|
|
2d343029a1 | ||
|
|
56d9fee486 | ||
|
|
cf98232109 | ||
|
|
01c19a9995 | ||
|
|
f142150b3e | ||
|
|
b783ee8c0b | ||
|
|
2f4dfa080b | ||
|
|
54b0ce27fb | ||
|
|
0e8d410c98 | ||
|
|
d3158f5c7e | ||
|
|
f580608a31 | ||
|
|
fefd4d281a | ||
|
|
ed539d2ee1 | ||
|
|
25852d68bd | ||
|
|
795389c0c8 | ||
|
|
66f2d41033 | ||
|
|
01a0d0d587 | ||
|
|
7262a9ab50 | ||
|
|
189d27c13f | ||
|
|
eb7f2abd97 | ||
|
|
250a8ec2f1 | ||
|
|
375ef09d78 | ||
|
|
135f0e300f | ||
|
|
9d752685cd | ||
|
|
c5a6f8a0b3 | ||
|
|
b7768ccaf6 | ||
|
|
e68d5c8a14 | ||
|
|
f7899bc670 | ||
|
|
5f74c827f5 | ||
|
|
09ddd82800 | ||
|
|
87aa1af654 | ||
|
|
c3ffb13a79 | ||
|
|
ba8466bacf | ||
|
|
595e19231e | ||
|
|
4f76163541 | ||
|
|
6f06ac760c | ||
|
|
25947cb278 | ||
|
|
b0d76f2f6a | ||
|
|
6d35c648d6 | ||
|
|
b4e7dd54cf | ||
|
|
6411c1cf0d | ||
|
|
0f16d0a7a3 | ||
|
|
1a29f374a5 | ||
|
|
5911f3e28e | ||
|
|
040447833c | ||
|
|
c60ed59a75 | ||
|
|
976f92cb11 | ||
|
|
ded8367b07 | ||
|
|
1071e86f96 | ||
|
|
da68b222b7 | ||
|
|
42ca5743aa | ||
|
|
c2b14fca10 | ||
|
|
9e2b1c5af3 | ||
|
|
8f0d05da2e | ||
|
|
3d3a46070c | ||
|
|
fbc551a76e | ||
|
|
8563ddffd3 | ||
|
|
7955b28a75 |
@@ -2,5 +2,10 @@
|
||||
"env": {
|
||||
"jest": true
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "15.0"
|
||||
}
|
||||
},
|
||||
"extends": "@coralproject/eslint-config-talk"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
overrides:
|
||||
- files: "bin/cli*"
|
||||
options:
|
||||
parser: babylon
|
||||
+4
-76
@@ -1,78 +1,6 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
# Mozilla Community Participation Guidelines
|
||||
|
||||
## Our Pledge
|
||||
## Version 2.3 - Updated May 16, 2017
|
||||
|
||||
We expect everyone contributing to The Coral Project to follow this code of conduct. That means the team, contractors we employ, contributors, as well as anyone posting to our public or internal-facing channels.
|
||||
We created it not because we anticipate any unacceptable behavior, but because we believe that articulating our values and obligations to one another reinforces the already exceptional level of respect among the team, and because having a code provides us with clear avenues to correct our culture should it ever stray from that course.
|
||||
|
||||
We commit to enforce and evolve this code over the duration of the project.
|
||||
|
||||
## Expected behavior
|
||||
|
||||
* Be supportive of each other.
|
||||
* Be collaborative. Involve others in brainstorms, sketching sessions, code reviews, planning documents, and the like. It’s not only okay to ask for help or feedback often, it’s unacceptable not to do so.
|
||||
* Be generous and kind in both giving and accepting critique. Critique is a natural and important part of our culture. Good critiques are kind, respectful, clear, and constructive, focused on goals and requirements rather than personal preferences. You are expected to give and receive criticism with grace.
|
||||
* Be humane. Be polite and friendly in all forms of communication, especially remote communication, where opportunities for misunderstanding are greater. Use sarcasm carefully. Tone is hard to decipher online; make judicious use of emoji to aid in communication.
|
||||
* Be considerate.
|
||||
* Be tolerant.
|
||||
* Respect people’s boundaries.
|
||||
* Do not make it personal.
|
||||
* Use welcoming and inclusive language.
|
||||
* Offer to help if you see someone struggling or otherwise in need of assistance (taking care not to be patronizing or disrespectful).
|
||||
* If someone approaches you looking for help, be generous with your time; if you’re under a deadline, direct them to someone else who may be of assistance.
|
||||
* Go out of your way to include people in jokes or memes, recognizing that we want to build an environment free of cliques.
|
||||
* Show empathy towards other community members
|
||||
|
||||
|
||||
## Unacceptable behavior
|
||||
|
||||
We are committed to providing a welcoming and safe environment for people of all races, gender identities, gender expressions, sexual orientations, physical abilities, physical appearances, socioeconomic backgrounds, nationalities, ages, religions, and beliefs.
|
||||
We expect that you will refrain from demeaning, discriminatory, or harassing behavior and speech.
|
||||
|
||||
Harassment includes, but is not limited to: deliberate intimidation; stalking; unwanted photography or recording; sustained or willful disruption of talks or other events; inappropriate physical contact; use of sexual or discriminatory imagery, comments, or jokes; and unwelcome sexual attention.
|
||||
Furthermore, any behavior or language which is unwelcoming—whether or not it rises to the level of harassment—is also strongly discouraged. Much exclusionary behavior takes the form of microaggressions—subtle put-downs which may be unconsciously delivered. Regardless of intent, microaggressions can have a significant negative impact on victims and have no place on our team.
|
||||
|
||||
Other inappropriate behavior:
|
||||
|
||||
* Threats
|
||||
* Slurs
|
||||
* Pornography
|
||||
* Spam
|
||||
* Bullying
|
||||
* Copyright infringement
|
||||
* Impersonation of someone else
|
||||
* Violating someone’s privacy
|
||||
|
||||
If you feel that someone has harassed you or otherwise treated you or someone else inappropriately, please alert the project lead at [andrewl@mozillafoundation.org](mailto:andrewl@mozillafoundation.org).
|
||||
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
These guidelines are ambitious, and we’re not always going to succeed in meeting them. When something goes wrong—whether it’s a microaggression or an instance of harassment — there are a number of things you can do to address the situation. Depending on your comfort level and the severity of the situation, here are some suggestions:
|
||||
|
||||
* Address it directly. If you’re comfortable bringing up the incident with the person who instigated it, pull them aside to discuss how it affected you. Be sure to approach these conversations in a forgiving spirit: an angry or tense conversation will not do either of you any good. If you’re unsure how to go about that, try discussing with your manager or with the people and culture team first—they might have some advice about how to make this conversation happen.
|
||||
|
||||
If you’re too frustrated to have a direct conversation, there are a number of alternate routes you can take.
|
||||
|
||||
* Talk to a peer or mentor. Your colleagues are likely to have personal and professional experience on which to draw that could be of use to you. If you have someone you’re comfortable approaching, reach out and discuss the situation with them. They may be able to advise on how they would handle it, or direct you to someone who can. The flip side of this, of course, is that you should also be available when your colleagues reach out to you.
|
||||
|
||||
* Contact the project lead, Andrew Losowsky, [andrewl@mozillafoundation.org](mailto:andrewl@mozillafoundation.org), or the technical lead. We will work with you to help you figure out how to ensure that any conflict doesn’t interfere with your work, in confidence if you would prefer.
|
||||
|
||||
* Talk to Chris Lawrence. Chris oversees the project. He can be contacted at [clawrence@mozillafoundation.org](mailto:clawrence@mozillafoundation.org).
|
||||
|
||||
If you feel you have been unfairly accused of violating this code of conduct, you should contact Chris with a concise description of your grievance.
|
||||
|
||||
## Conclusion
|
||||
|
||||
We welcome your feedback on this and every other aspect of what we do as The Coral Project, and we thank you for working with us to make it a safe, enjoyable, and friendly experience for everyone involved in the project and what we do.
|
||||
Above text is licensed CC BY-SA 4.0, adapted from the SRCCON code of conduct, FreeBSD’s code of conduct, Vox Media’s product team code of conduct, Medium’s code of conduct, as well as adapted from the Contributor Covenant.
|
||||
The heart of Mozilla is people. We put people first and do our best to recognize, appreciate and respect the diversity of our global contributors. The Mozilla Project welcomes contributions from everyone who shares our goals and wants to contribute in a healthy and constructive manner within our community. As such, we have adopted this code of conduct and require all those who participate to agree and adhere to these Community Participation Guidelines in order to help us create a safe and positive community experience for all.
|
||||
https://www.mozilla.org/en-US/about/governance/policies/participation/
|
||||
|
||||
@@ -6,10 +6,12 @@ ONBUILD ARG TALK_ASSET_COMMENTS_LOAD_DEPTH=10
|
||||
ONBUILD ARG TALK_REPLY_COMMENTS_LOAD_DEPTH=3
|
||||
ONBUILD ARG TALK_THREADING_LEVEL=3
|
||||
ONBUILD ARG TALK_DEFAULT_STREAM_TAB=all
|
||||
ONBUILD ARG TALK_DISABLE_EMBED_POLYFILL=FALSE
|
||||
ONBUILD ARG TALK_DEFAULT_LANG=en
|
||||
ONBUILD ARG TALK_WHITELISTED_LANGUAGES
|
||||
ONBUILD ARG TALK_PLUGINS_JSON
|
||||
ONBUILD ARG TALK_WEBPACK_SOURCE_MAP
|
||||
ONBUILD ARG TALK_DEFAULT_LAZY_RENDER
|
||||
|
||||
# Bundle app source
|
||||
ONBUILD COPY . /usr/src/app
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
# Talk · [](https://circleci.com/gh/coralproject/talk) · [](https://nodesecurity.io/orgs/coralproject/projects/7bd7d26c-47ed-4a5f-8c4a-b919bf1c2946) · [](CONTRIBUTING.md#pull-requests)
|
||||
# Talk · [](https://circleci.com/gh/coralproject/talk) · [](CONTRIBUTING.md#pull-requests)
|
||||
|
||||
Online comments are broken. Our open-source commenting platform, Talk, rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here](https://coralproject.net/products/talk.html).
|
||||
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).
|
||||
|
||||
Built with <3 by The Coral Project & Mozilla.
|
||||
Built with <3 by The Coral Project.
|
||||
|
||||
## Try Talk!
|
||||
## Getting Started
|
||||
|
||||
You're just one click away from trying Talk - all you need is a Heroku account and a few minutes of your time.
|
||||
|
||||
[](https://dashboard.heroku.com/new?template=https%3A%2F%2Fgithub.com%2Fcoralproject%2Ftalk&env[TALK_FACEBOOK_APP_ID]=ignore&env[TALK_FACEBOOK_APP_SECRET]=ignore)
|
||||
|
||||
## Technical Documentation
|
||||
|
||||
From getting up and running, to advanced configuration, to how to scale Talk, our [Talk Technical Docs](https://docs.coralproject.net/talk/) have everything you need to know.
|
||||
Check out our Quickstart and Install guides to get started with Talk in our [Technical Docs](https://docs.coralproject.net/talk/).
|
||||
|
||||
## Product Guide
|
||||
|
||||
@@ -20,7 +14,15 @@ Learn more about Talk, including a deep dive into features for commenters and mo
|
||||
|
||||
## Pre-Launch Guide
|
||||
|
||||
You’ve installed Talk on your server, and you’re preparing to launch it on your site. The real community work starts now, before you go live. You have a unique opportunity pre-launch to set your community up for success. Read our [Talk Community Guide](https://blog.coralproject.net/youve-installed-talk-now-what/).
|
||||
You’ve installed Talk on your server, and you’re preparing to launch it on your site. The real community work starts now, before you go live. You have a unique opportunity pre-launch to set your community up for success. Read our [Talk Community Guide](https://coralproject.net/blog/youve-installed-talk-now-what/).
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
For advanced configuration and usage of Talk, check out our [Configuration](https://docs.coralproject.net/talk/advanced-configuration/) and [Integration](https://docs.coralproject.net/talk/integrating/authentication/) how-tos. This covers topics in which you will need dev support to fully customize and integrate Talk, such as SSO/authentication, creating and managing assets and articles, styling Talk with custom CSS, and setting up Notifications and SMTP support.
|
||||
|
||||
## Versions & Upgrading
|
||||
|
||||
The current recommended release version is v4.5.0. ^4.5 (and all future even-numbered versions) are considered stable LTS versions. We recommend ^4.5 for use in production environments.
|
||||
|
||||
## More Resources
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
"value": "",
|
||||
"required": true
|
||||
},
|
||||
"MAILGUN_SMTP_PASSWORD": {
|
||||
"value": "",
|
||||
"required": true
|
||||
},
|
||||
"NODE_ENV": "production",
|
||||
"REWRITE_ENV": "TALK_MONGO_URL:MONGO_URI,TALK_REDIS_URL:REDIS_URL,TALK_SMTP_HOST:MAILGUN_SMTP_SERVER,TALK_SMTP_PORT:MAILGUN_SMTP_PORT,TALK_SMTP_USERNAME:MAILGUN_SMTP_LOGIN,TALK_SMTP_PASSWORD:MAILGUN_SMTP_PASSWORD",
|
||||
"NPM_CONFIG_PRODUCTION": "false"
|
||||
|
||||
@@ -219,6 +219,28 @@ async function rewrite(search, replace, options) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* debugAssetURL will scrape the given URL and print it's scraped values.
|
||||
*/
|
||||
async function debugAssetURL(url) {
|
||||
try {
|
||||
const meta = await scraper.scrape(url);
|
||||
|
||||
const table = new Table({ head: ['Property', 'Value'] });
|
||||
|
||||
for (const [property, value] of Object.entries(meta)) {
|
||||
table.push([property, value]);
|
||||
}
|
||||
|
||||
console.log(table.toString());
|
||||
|
||||
util.shutdown(0);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// Setting up the program command line arguments.
|
||||
//==============================================================================
|
||||
@@ -234,6 +256,13 @@ program
|
||||
.description('list all the assets in the database')
|
||||
.action(listAssets);
|
||||
|
||||
program
|
||||
.command('debug [url]')
|
||||
.description(
|
||||
'prints the scraped metadata that would be added to the given asset'
|
||||
)
|
||||
.action(debugAssetURL);
|
||||
|
||||
program
|
||||
.command('refresh [age]')
|
||||
.description('queues the assets that exceed the age requested')
|
||||
|
||||
+37
-24
@@ -143,9 +143,9 @@ async function reconcileRemotePlugins({ dryRun, upgradeRemote }) {
|
||||
|
||||
if (fetchable.length > 0) {
|
||||
console.log(
|
||||
`$ yarn add --ignore-scripts --ignore-workspace-root-check ${fetchable.map(
|
||||
({ name, version }) => `${name}@${version}`.cyan
|
||||
)}`
|
||||
`$ yarn add --ignore-scripts --ignore-workspace-root-check ${fetchable
|
||||
.map(({ name, version }) => `${name}@${version}`.cyan)
|
||||
.join(' ')}`
|
||||
);
|
||||
|
||||
if (!dryRun) {
|
||||
@@ -162,7 +162,7 @@ async function reconcileRemotePlugins({ dryRun, upgradeRemote }) {
|
||||
|
||||
if (output.status) {
|
||||
throw new Error(
|
||||
'Could not install external plugins, errors occured during install'
|
||||
'Could not install external plugins, errors occurred during install'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ async function reconcileRemotePlugins({ dryRun, upgradeRemote }) {
|
||||
|
||||
if (output.status) {
|
||||
throw new Error(
|
||||
'Could not install external plugins, errors occured during install'
|
||||
'Could not install external plugins, errors occurred during install'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -329,28 +329,41 @@ async function createSeedPlugin() {
|
||||
if (answers.addPluginsJson) {
|
||||
const pluginsJson = path.resolve(__dirname, '..', 'plugins.json');
|
||||
|
||||
fs
|
||||
.readJson(pluginsJson)
|
||||
.then(j => {
|
||||
// This is a client-side plugin, let's push this.
|
||||
if (answers.client) {
|
||||
j.client.push(answers.pluginName);
|
||||
let j;
|
||||
try {
|
||||
j = await fs.readJson(pluginsJson);
|
||||
} catch (err) {
|
||||
// Fallback to plugins.default.json if the plugins.json file does not
|
||||
// exist.
|
||||
const defaultPluginsJson = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'plugins.default.json'
|
||||
);
|
||||
|
||||
const output = JSON.stringify(j, null, 2);
|
||||
fs.writeFileSync(pluginsJson, output);
|
||||
}
|
||||
try {
|
||||
j = await fs.readJson(defaultPluginsJson);
|
||||
} catch (err) {
|
||||
// Fallback to an empty one if that also, doesn't exist.
|
||||
j = { client: [], server: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// This is a server-side plugin, let's push this.
|
||||
if (answers.server) {
|
||||
j.server.push(answers.pluginName);
|
||||
// This is a client-side plugin, let's push this.
|
||||
if (answers.client) {
|
||||
j.client.push(answers.pluginName);
|
||||
|
||||
const output = JSON.stringify(j, null, 2);
|
||||
fs.writeFileSync(pluginsJson, output);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
const output = JSON.stringify(j, null, 2);
|
||||
fs.writeFileSync(pluginsJson, output);
|
||||
}
|
||||
|
||||
// This is a server-side plugin, let's push this.
|
||||
if (answers.server) {
|
||||
j.server.push(answers.pluginName);
|
||||
|
||||
const output = JSON.stringify(j, null, 2);
|
||||
fs.writeFileSync(pluginsJson, output);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
|
||||
+4
-1
@@ -49,7 +49,10 @@ async function revokeToken(tokenID) {
|
||||
|
||||
async function createToken(userID, tokenName) {
|
||||
try {
|
||||
let { pat: { id }, jwt } = await TokensService.create(userID, tokenName);
|
||||
let {
|
||||
pat: { id },
|
||||
jwt,
|
||||
} = await TokensService.create(userID, tokenName);
|
||||
|
||||
console.log(`Created Token[${id}] for User[${userID}] = ${jwt}`);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import queryString from 'query-string';
|
||||
import queryString from 'querystringify';
|
||||
|
||||
import {
|
||||
FETCH_USERS_REQUEST,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import {
|
||||
SHOW_REJECT_USERNAME_DIALOG,
|
||||
HIDE_REJECT_USERNAME_DIALOG,
|
||||
} from '../constants/rejectUsernameDialog';
|
||||
|
||||
export const showRejectUsernameDialog = ({ userId, username }) => ({
|
||||
type: SHOW_REJECT_USERNAME_DIALOG,
|
||||
userId,
|
||||
username,
|
||||
});
|
||||
|
||||
export const hideRejectUsernameDialog = () => ({
|
||||
type: HIDE_REJECT_USERNAME_DIALOG,
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import queryString from 'query-string';
|
||||
import queryString from 'querystringify';
|
||||
|
||||
import {
|
||||
FETCH_ASSETS_REQUEST,
|
||||
|
||||
@@ -76,7 +76,7 @@ ActionsMenu.propTypes = {
|
||||
icon: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
label: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
|
||||
buttonClassNames: PropTypes.string,
|
||||
};
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ const ActionsMenuItem = props => (
|
||||
|
||||
ActionsMenuItem.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.string,
|
||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
|
||||
};
|
||||
|
||||
export default ActionsMenuItem;
|
||||
|
||||
@@ -5,6 +5,7 @@ import styles from './NotFound.css';
|
||||
import { Button, TextField, Alert, Success } from 'coral-ui';
|
||||
import Recaptcha from 'react-recaptcha';
|
||||
import cn from 'classnames';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class AdminLogin extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -41,13 +42,13 @@ class AdminLogin extends React.Component {
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
id="email"
|
||||
label="Email Address"
|
||||
label={t('login.email_address')}
|
||||
value={this.state.email}
|
||||
onChange={e => this.setState({ email: e.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
id="password"
|
||||
label="Password"
|
||||
label={t('login.password')}
|
||||
value={this.state.password}
|
||||
onChange={e => this.setState({ password: e.target.value })}
|
||||
type="password"
|
||||
@@ -60,10 +61,10 @@ class AdminLogin extends React.Component {
|
||||
full
|
||||
onClick={this.handleSignIn}
|
||||
>
|
||||
Sign In
|
||||
{t('login.sign_in_button')}
|
||||
</Button>
|
||||
<p className={styles.forgotPasswordCTA}>
|
||||
Forgot your password?{' '}
|
||||
{t('login.forgot_password')}{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.forgotPasswordLink}
|
||||
@@ -72,7 +73,7 @@ class AdminLogin extends React.Component {
|
||||
this.setState({ requestPassword: true });
|
||||
}}
|
||||
>
|
||||
Request a new one.
|
||||
{t('login.request_passowrd')}
|
||||
</a>
|
||||
</p>
|
||||
{loginMaxExceeded && (
|
||||
@@ -95,14 +96,14 @@ class AdminLogin extends React.Component {
|
||||
>
|
||||
{this.props.passwordRequestSuccess}{' '}
|
||||
<a className={styles.signInLink} href="#">
|
||||
Sign in
|
||||
{t('login.sign_in')}
|
||||
</a>
|
||||
<Success />
|
||||
</p>
|
||||
) : (
|
||||
<form onSubmit={this.handleRequestPassword}>
|
||||
<TextField
|
||||
label="Email Address"
|
||||
label={t('login.email_address')}
|
||||
value={this.state.email}
|
||||
onChange={e => this.setState({ email: e.target.value })}
|
||||
/>
|
||||
@@ -112,17 +113,15 @@ class AdminLogin extends React.Component {
|
||||
full
|
||||
onClick={this.handleRequestPassword}
|
||||
>
|
||||
Reset Password
|
||||
{t('login.reset_password')}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
return (
|
||||
<Layout fixedDrawer restricted={true}>
|
||||
<div className={cn(styles.loginLayout, 'talk-admin-login')}>
|
||||
<h1 className={styles.loginHeader}>Team sign in</h1>
|
||||
<p className={styles.loginCTA}>
|
||||
Sign in to interact with your community.
|
||||
</p>
|
||||
<h1 className={styles.loginHeader}>{t('login.team_sign_in')}</h1>
|
||||
<p className={styles.loginCTA}>{t('login.sign_in_message')}</p>
|
||||
{this.state.requestPassword ? requestPasswordForm : signInForm}
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
@@ -19,7 +19,9 @@ class BanUserDialog extends React.Component {
|
||||
}
|
||||
|
||||
handleMessageChange = e => {
|
||||
const { target: { value: message } } = e;
|
||||
const {
|
||||
target: { value: message },
|
||||
} = e;
|
||||
this.setState({ message });
|
||||
};
|
||||
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './CountBadge.css';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { humanizeNumber } from 'coral-framework/helpers/numbers';
|
||||
|
||||
const CountBadge = ({ count }) => {
|
||||
let number = count;
|
||||
|
||||
// shorten large counts to abbreviations
|
||||
if (number / 1e9 > 1) {
|
||||
number = `${(number / 1e9).toFixed(1)}${t('modqueue.billion')}`;
|
||||
} else if (number / 1e6 > 1) {
|
||||
number = `${(number / 1e6).toFixed(1)}${t('modqueue.million')}`;
|
||||
} else if (number / 1e3 > 1) {
|
||||
number = `${(number / 1e3).toFixed(1)}${t('modqueue.thousand')}`;
|
||||
}
|
||||
number = humanizeNumber(number);
|
||||
|
||||
return <span className={styles.count}>{number}</span>;
|
||||
};
|
||||
|
||||
@@ -7,9 +7,9 @@ import t from 'coral-framework/services/i18n';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import cn from 'classnames';
|
||||
|
||||
const CoralDrawer = ({ handleLogout, currentUser }) => (
|
||||
<Drawer className={cn('talk-admin-drawer-nav', styles.drawer)}>
|
||||
{currentUser && can(currentUser, 'ACCESS_ADMIN') ? (
|
||||
const CoralDrawer = ({ handleLogout, currentUser }) =>
|
||||
currentUser && can(currentUser, 'ACCESS_ADMIN') ? (
|
||||
<Drawer className={cn('talk-admin-drawer-nav', styles.drawer)}>
|
||||
<div>
|
||||
<Navigation className={styles.nav}>
|
||||
{can(currentUser, 'MODERATE_COMMENTS') && (
|
||||
@@ -48,9 +48,8 @@ const CoralDrawer = ({ handleLogout, currentUser }) => (
|
||||
<span>{`v${process.env.VERSION}`}</span>
|
||||
</Navigation>
|
||||
</div>
|
||||
) : null}
|
||||
</Drawer>
|
||||
);
|
||||
</Drawer>
|
||||
) : null;
|
||||
|
||||
CoralDrawer.propTypes = {
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import React from 'react';
|
||||
import styles from './Forbidden.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const Forbidden = () => (
|
||||
<div className={styles.container}>
|
||||
<p className={styles.copy}>
|
||||
This page is for team use only. Please contact an administrator if you
|
||||
want to join this team.
|
||||
</p>
|
||||
<p className={styles.copy}>{t('error.PAGE_NOT_AVAILABLE_ROLE')}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class ForgotPassword extends React.Component {
|
||||
href="#"
|
||||
onClick={this.handleSignInLink}
|
||||
>
|
||||
Sign in
|
||||
{t('login.sign_in')}
|
||||
</a>
|
||||
<Success />
|
||||
</div>
|
||||
@@ -43,21 +43,21 @@ class ForgotPassword extends React.Component {
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
label="Email Address"
|
||||
label={t('login.email_address')}
|
||||
value={email}
|
||||
onChange={this.handleEmailChange}
|
||||
/>
|
||||
<Button type="submit" cStyle="black" full>
|
||||
Reset Password
|
||||
{t('login.reset_password_send_button')}
|
||||
</Button>
|
||||
<p className={styles.cta}>
|
||||
Go back to{' '}
|
||||
{t('login.go_back')}{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.signInLink}
|
||||
onClick={this.handleSignInLink}
|
||||
>
|
||||
Sign In
|
||||
{t('login.sign_in')}
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
|
||||
@@ -77,13 +77,22 @@ const CoralHeader = ({
|
||||
<MenuItem onClick={() => showShortcuts(true)}>
|
||||
{t('configure.shortcuts')}
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a
|
||||
href="https://docs.coralproject.net/talk/how-talk-works/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('configure.product_guide_link')}
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a
|
||||
href="https://github.com/coralproject/talk/releases"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
View latest version
|
||||
{t('configure.view_last_version')}
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
@@ -92,7 +101,7 @@ const CoralHeader = ({
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Report a bug or give feedback
|
||||
{t('configure.report_bug_or_feedback')}
|
||||
</a>
|
||||
</MenuItem>
|
||||
{currentUser && (
|
||||
|
||||
@@ -27,7 +27,9 @@ class KarmaTooltip extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { thresholds: { unreliable } } = this.props;
|
||||
const {
|
||||
thresholds: { unreliable },
|
||||
} = this.props;
|
||||
const { menuVisible } = this.state;
|
||||
|
||||
return (
|
||||
@@ -68,6 +70,7 @@ class KarmaTooltip extends React.Component {
|
||||
className={styles.link}
|
||||
href={t('user_detail.karma_docs_link')}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('user_detail.learn_more')}
|
||||
</a>
|
||||
|
||||
@@ -5,6 +5,7 @@ import PropTypes from 'prop-types';
|
||||
import styles from './Login.css';
|
||||
import Layout from 'coral-admin/src/components/Layout';
|
||||
import cn from 'classnames';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class LoginContainer extends Component {
|
||||
renderForm() {
|
||||
@@ -19,8 +20,8 @@ class LoginContainer extends Component {
|
||||
return (
|
||||
<Layout fixedDrawer restricted={true}>
|
||||
<div className={cn(styles.layout, 'talk-admin-login')}>
|
||||
<h1 className={styles.header}>Team sign in</h1>
|
||||
<p className={styles.cta}>Sign in to interact with your community.</p>
|
||||
<h1 className={styles.header}>{t('login.team_sign_in')}</h1>
|
||||
<p className={styles.cta}>{t('login.sign_in_message')}</p>
|
||||
{this.renderForm()}
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
.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 {
|
||||
font-size: 24px;
|
||||
line-height: 14px;
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Dialog, BareButton } from 'coral-ui';
|
||||
import styles from './RejectUsernameDialog.css';
|
||||
import cn from 'classnames';
|
||||
import { RadioGroup, Radio } from 'react-mdl';
|
||||
import Button from 'coral-ui/components/Button';
|
||||
import { username as flagReason } from 'coral-framework/graphql/flagReasons';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const initialState = { reason: flagReason.offensive, message: '' };
|
||||
|
||||
class RejectUsernameDialog extends React.Component {
|
||||
state = initialState;
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
if (this.props.open && !next.open) {
|
||||
this.setState(initialState);
|
||||
}
|
||||
}
|
||||
|
||||
handleReasonChange = event => {
|
||||
this.setState({ reason: event.target.value });
|
||||
};
|
||||
|
||||
handleMessageChange = event => {
|
||||
this.setState({ message: event.target.value });
|
||||
};
|
||||
|
||||
handlePerform = () => {
|
||||
this.props.onPerform({
|
||||
reason: this.state.reason,
|
||||
message: this.state.message,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { open, onCancel } = this.props;
|
||||
const { reason, message } = this.state;
|
||||
return (
|
||||
<Dialog
|
||||
className={cn(styles.dialog, 'talk-admin-reject-username-dialog')}
|
||||
id="rejectUsernameDialog"
|
||||
onCancel={onCancel}
|
||||
open={open}
|
||||
>
|
||||
<div className={styles.close}>
|
||||
<BareButton
|
||||
aria-label="Close"
|
||||
onClick={onCancel}
|
||||
className={styles.closeButton}
|
||||
>
|
||||
×
|
||||
</BareButton>
|
||||
</div>
|
||||
<section className="talk-admin-reject-username-dialog-section">
|
||||
<h1 className={styles.header}>
|
||||
{t('reject_username_dialog.title')}: {this.props.username}
|
||||
</h1>
|
||||
<p className={styles.description}>
|
||||
{t('reject_username_dialog.description')}
|
||||
</p>
|
||||
<fieldset>
|
||||
<legend className={styles.legend}>
|
||||
{t('reject_username_dialog.reason')}
|
||||
</legend>
|
||||
<RadioGroup
|
||||
name="reason"
|
||||
value={reason}
|
||||
childContainer="div"
|
||||
onChange={this.handleReasonChange}
|
||||
className={styles.radioGroup}
|
||||
>
|
||||
<Radio value={flagReason.offensive}>
|
||||
{t('flag_reasons.username.offensive')}
|
||||
</Radio>
|
||||
<Radio value={flagReason.nolike}>
|
||||
{t('flag_reasons.username.nolike')}
|
||||
</Radio>
|
||||
<Radio value={flagReason.impersonating}>
|
||||
{t('flag_reasons.username.impersonating')}
|
||||
</Radio>
|
||||
<Radio value={flagReason.spam}>
|
||||
{t('flag_reasons.username.spam')}
|
||||
</Radio>
|
||||
<Radio value={flagReason.other}>
|
||||
{t('flag_reasons.username.other')}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
{reason === flagReason.other && (
|
||||
<fieldset>
|
||||
<legend className={styles.legend}>
|
||||
{t('reject_username_dialog.message')}
|
||||
</legend>
|
||||
<textarea
|
||||
rows={5}
|
||||
className={styles.messageInput}
|
||||
value={message}
|
||||
onChange={this.handleMessageChange}
|
||||
/>
|
||||
</fieldset>
|
||||
)}
|
||||
</fieldset>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
cStyle="white"
|
||||
className={styles.cancel}
|
||||
onClick={onCancel}
|
||||
raised
|
||||
>
|
||||
{t('reject_username_dialog.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
cStyle="black"
|
||||
className={cn(
|
||||
styles.perform,
|
||||
'talk-admin-reject-username-dialog-continue'
|
||||
)}
|
||||
onClick={this.handlePerform}
|
||||
raised
|
||||
>
|
||||
{t('reject_username_dialog.reject_username')}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RejectUsernameDialog.propTypes = {
|
||||
open: PropTypes.bool.isRequired,
|
||||
onPerform: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
username: PropTypes.string,
|
||||
};
|
||||
|
||||
export default RejectUsernameDialog;
|
||||
@@ -5,6 +5,7 @@ import { Button, TextField, Alert } from 'coral-ui';
|
||||
import cn from 'classnames';
|
||||
import Recaptcha from 'coral-framework/components/Recaptcha';
|
||||
import External from './External';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class SignIn extends React.Component {
|
||||
recaptcha = null;
|
||||
@@ -40,13 +41,13 @@ class SignIn extends React.Component {
|
||||
{errorMessage && <Alert>{errorMessage}</Alert>}
|
||||
<TextField
|
||||
id="email"
|
||||
label="Email Address"
|
||||
label={t('login.email_address')}
|
||||
value={email}
|
||||
onChange={this.handleEmailChange}
|
||||
/>
|
||||
<TextField
|
||||
id="password"
|
||||
label="Password"
|
||||
label={t('login.password')}
|
||||
value={password}
|
||||
onChange={this.handlePasswordChange}
|
||||
type="password"
|
||||
@@ -68,17 +69,17 @@ class SignIn extends React.Component {
|
||||
cStyle="black"
|
||||
full
|
||||
>
|
||||
Sign In
|
||||
{t('login.sign_in')}
|
||||
</Button>
|
||||
<p className={styles.forgotPasswordCTA}>
|
||||
{/* TODO: translate */}
|
||||
Forgot your password?{' '}
|
||||
{t('login.forgot_password')}{' '}
|
||||
<a
|
||||
href="#"
|
||||
className={styles.forgotPasswordLink}
|
||||
onClick={this.handleForgotPasswordLink}
|
||||
>
|
||||
Request a new one.
|
||||
{t('login.request_passowrd')}
|
||||
</a>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
@@ -6,7 +6,15 @@ import styles from './UserDetail.css';
|
||||
import UserHistory from './UserHistory';
|
||||
import { Slot } from 'coral-framework/components';
|
||||
import UserDetailCommentList from '../components/UserDetailCommentList';
|
||||
import { isSuspended, isBanned, getKarma } from 'coral-framework/utils/user';
|
||||
|
||||
import {
|
||||
isSuspended,
|
||||
isUsernameRejected,
|
||||
isUsernameChanged,
|
||||
isBanned,
|
||||
getKarma,
|
||||
} from 'coral-framework/utils/user';
|
||||
|
||||
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
import {
|
||||
@@ -23,6 +31,25 @@ import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
import UserInfoTooltip from './UserInfoTooltip';
|
||||
import KarmaTooltip from './KarmaTooltip';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { humanizeNumber } from 'coral-framework/helpers/numbers';
|
||||
|
||||
const filterOutLocalProfiles = ({ provider }) => provider !== 'local';
|
||||
|
||||
/**
|
||||
* getUserStatusArray
|
||||
* returns an array of active status(es)
|
||||
* i.e if suspension is active, it returns suspension
|
||||
*/
|
||||
|
||||
function getUserStatusArray(user) {
|
||||
const statusMap = {
|
||||
suspended: isSuspended,
|
||||
banned: isBanned,
|
||||
usernameRejected: isUsernameRejected,
|
||||
usernameChanged: isUsernameChanged,
|
||||
};
|
||||
return Object.keys(statusMap).filter(k => statusMap[k](user));
|
||||
}
|
||||
|
||||
class UserDetail extends React.Component {
|
||||
changeTab = tab => {
|
||||
@@ -41,6 +68,12 @@ class UserDetail extends React.Component {
|
||||
username: this.props.root.user.username,
|
||||
});
|
||||
|
||||
showRejectUsernameDialog = () =>
|
||||
this.props.showRejectUsernameDialog({
|
||||
userId: this.props.root.user.id,
|
||||
username: this.props.root.user.username,
|
||||
});
|
||||
|
||||
renderLoading() {
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.props.hideUserDetail}>
|
||||
@@ -61,18 +94,46 @@ class UserDetail extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
getActionMenuLabel() {
|
||||
const { root: { user } } = this.props;
|
||||
getActionMenuLabel(user) {
|
||||
const userStatusArr = getUserStatusArray(user);
|
||||
const count = userStatusArr.length;
|
||||
|
||||
if (isBanned(user)) {
|
||||
return 'Banned';
|
||||
} else if (isSuspended(user)) {
|
||||
return 'Suspended';
|
||||
if (count > 1) {
|
||||
return `Status (${count})`;
|
||||
} else {
|
||||
const activeStatus = userStatusArr[0];
|
||||
switch (activeStatus) {
|
||||
case 'suspended':
|
||||
return t('user_detail.suspended');
|
||||
case 'banned':
|
||||
return t('user_detail.banned');
|
||||
case 'usernameRejected':
|
||||
return (
|
||||
<span>
|
||||
{t('user_detail.username')}
|
||||
{` `}
|
||||
<Icon name="cancel" />
|
||||
</span>
|
||||
);
|
||||
case 'usernameChanged':
|
||||
return (
|
||||
<span>
|
||||
{t('user_detail.username')}
|
||||
{` `}
|
||||
<Icon name="access_time" />
|
||||
</span>
|
||||
);
|
||||
default:
|
||||
return activeStatus;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
goToReportedUsernames = () => {
|
||||
const { router } = this.props;
|
||||
router.push('/admin/community/flagged');
|
||||
};
|
||||
|
||||
renderLoaded() {
|
||||
const {
|
||||
root,
|
||||
@@ -100,7 +161,7 @@ class UserDetail extends React.Component {
|
||||
} = this.props;
|
||||
|
||||
// if totalComments is 0, you're dividing by zero
|
||||
let rejectedPercent = rejectedComments / totalComments * 100;
|
||||
let rejectedPercent = (rejectedComments / totalComments) * 100;
|
||||
|
||||
if (rejectedPercent === Infinity || isNaN(rejectedPercent)) {
|
||||
rejectedPercent = 0;
|
||||
@@ -108,6 +169,8 @@ class UserDetail extends React.Component {
|
||||
|
||||
const banned = isBanned(user);
|
||||
const suspended = isSuspended(user);
|
||||
const usernameRejected = isUsernameRejected(user);
|
||||
const usernameChanged = isUsernameChanged(user);
|
||||
|
||||
const slotPassthrough = {
|
||||
root,
|
||||
@@ -140,7 +203,7 @@ class UserDetail extends React.Component {
|
||||
},
|
||||
'talk-admin-user-detail-actions-button'
|
||||
)}
|
||||
label={this.getActionMenuLabel()}
|
||||
label={this.getActionMenuLabel(user)}
|
||||
>
|
||||
{suspended ? (
|
||||
<ActionsMenuItem onClick={() => unsuspendUser({ id: user.id })}>
|
||||
@@ -167,6 +230,27 @@ class UserDetail extends React.Component {
|
||||
{t('user_detail.ban')}
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
|
||||
{usernameChanged && (
|
||||
<ActionsMenuItem onClick={this.goToReportedUsernames}>
|
||||
{t('user_detail.username_needs_approval')}
|
||||
{` `}
|
||||
<Icon name="launch" />
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
|
||||
{usernameRejected && !usernameChanged ? (
|
||||
<ActionsMenuItem disabled>
|
||||
{t('user_detail.username_rejected')}
|
||||
</ActionsMenuItem>
|
||||
) : (
|
||||
<ActionsMenuItem
|
||||
onClick={this.showRejectUsernameDialog}
|
||||
disabled={me.id === user.id || usernameChanged}
|
||||
>
|
||||
{t('user_detail.reject_username')}
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
</ActionsMenu>
|
||||
)}
|
||||
|
||||
@@ -180,6 +264,18 @@ class UserDetail extends React.Component {
|
||||
|
||||
<div>
|
||||
<ul className={styles.userDetailList}>
|
||||
<li className={styles.userDetailItem}>
|
||||
<Icon name="perm_identity" />
|
||||
<span className={styles.userDetailItem}>
|
||||
{t('user_detail.id')}:
|
||||
</span>
|
||||
{user.id}{' '}
|
||||
<ButtonCopyToClipboard
|
||||
className={styles.copyButton}
|
||||
icon="content_copy"
|
||||
copyText={user.id}
|
||||
/>
|
||||
</li>
|
||||
<li className={styles.userDetailItem}>
|
||||
<Icon name="assignment_ind" />
|
||||
<span className={styles.userDetailItem}>
|
||||
@@ -201,20 +297,22 @@ class UserDetail extends React.Component {
|
||||
/>
|
||||
</li>
|
||||
|
||||
{user.profiles.map(({ provider, id }) => (
|
||||
<li key={id} className={styles.userDetailItem}>
|
||||
<Icon name="device_hub" />
|
||||
<span className={styles.userDetailItem}>
|
||||
{capitalize(provider)} {t('user_detail.id')}:
|
||||
</span>
|
||||
{id}{' '}
|
||||
<ButtonCopyToClipboard
|
||||
className={styles.copyButton}
|
||||
icon="content_copy"
|
||||
copyText={id}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
{user.profiles
|
||||
.filter(filterOutLocalProfiles)
|
||||
.map(({ provider, id }) => (
|
||||
<li key={id} className={styles.userDetailItem}>
|
||||
<Icon name="device_hub" />
|
||||
<span className={styles.userDetailItem}>
|
||||
{capitalize(provider)} {t('user_detail.id')}:
|
||||
</span>
|
||||
{id}{' '}
|
||||
<ButtonCopyToClipboard
|
||||
className={styles.copyButton}
|
||||
icon="content_copy"
|
||||
copyText={id}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<ul className={styles.stats}>
|
||||
@@ -243,7 +341,7 @@ class UserDetail extends React.Component {
|
||||
styles[getKarma(user.reliable.commenter)]
|
||||
)}
|
||||
>
|
||||
{user.reliable.commenterKarma}
|
||||
{humanizeNumber(user.reliable.commenterKarma)}
|
||||
</span>
|
||||
</div>
|
||||
<KarmaTooltip thresholds={karmaThresholds.comment} />
|
||||
@@ -358,6 +456,7 @@ class UserDetail extends React.Component {
|
||||
}
|
||||
|
||||
UserDetail.propTypes = {
|
||||
router: PropTypes.object.isRequired,
|
||||
userId: PropTypes.string.isRequired,
|
||||
hideUserDetail: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
@@ -374,11 +473,13 @@ UserDetail.propTypes = {
|
||||
selectedCommentIds: PropTypes.array.isRequired,
|
||||
viewUserDetail: PropTypes.any.isRequired,
|
||||
loadMore: PropTypes.any.isRequired,
|
||||
showRejectUsernameDialog: PropTypes.func,
|
||||
showSuspendUserDialog: PropTypes.func,
|
||||
showBanUserDialog: PropTypes.func,
|
||||
unbanUser: PropTypes.func.isRequired,
|
||||
unsuspendUser: PropTypes.func.isRequired,
|
||||
modal: PropTypes.bool,
|
||||
rejectUsername: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default UserDetail;
|
||||
|
||||
@@ -14,7 +14,9 @@ import ApproveButton from './ApproveButton';
|
||||
import RejectButton from 'coral-admin/src/components/RejectButton';
|
||||
import CommentDeletedTombstone from './CommentDeletedTombstone';
|
||||
|
||||
import t, { timeago } from 'coral-framework/services/i18n';
|
||||
import { buildCommentURL } from 'coral-framework/utils/url';
|
||||
import TimeAgo from 'coral-framework/components/TimeAgo';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class UserDetailComment extends React.Component {
|
||||
approve = () =>
|
||||
@@ -73,9 +75,7 @@ class UserDetailComment extends React.Component {
|
||||
checked={selected}
|
||||
onChange={e => toggleSelect(e.target.value, e.target.checked)}
|
||||
/>
|
||||
<span className={styles.created}>
|
||||
{timeago(comment.created_at)}
|
||||
</span>
|
||||
<TimeAgo className={styles.created} datetime={comment.created_at} />
|
||||
{comment.editing && comment.editing.edited ? (
|
||||
<span>
|
||||
<span className={styles.editedMarker}>
|
||||
@@ -112,8 +112,9 @@ class UserDetailComment extends React.Component {
|
||||
/>
|
||||
<a
|
||||
className={styles.external}
|
||||
href={`${comment.asset.url}?commentId=${comment.id}`}
|
||||
href={buildCommentURL(comment.asset.url, comment.id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon name="open_in_new" /> {t('comment.view_context')}
|
||||
</a>
|
||||
@@ -122,7 +123,7 @@ class UserDetailComment extends React.Component {
|
||||
<IfHasLink text={comment.body}>
|
||||
<span className={styles.hasLinks}>
|
||||
{/* TODO: translate string */}
|
||||
<Icon name="error_outline" /> Contains Link
|
||||
<Icon name="error_outline" /> {t('common.contains_link')}
|
||||
</span>
|
||||
</IfHasLink>
|
||||
<div className={styles.actions}>
|
||||
|
||||
@@ -10,7 +10,10 @@ import ApproveButton from './ApproveButton';
|
||||
const UserDetailCommentList = props => {
|
||||
const {
|
||||
root,
|
||||
root: { user, comments: { nodes, hasNextPage } },
|
||||
root: {
|
||||
user,
|
||||
comments: { nodes, hasNextPage },
|
||||
},
|
||||
acceptComment,
|
||||
rejectComment,
|
||||
selectedCommentIds,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const SHOW_REJECT_USERNAME_DIALOG = 'SHOW_REJECT_USERNAME_DIALOG';
|
||||
export const HIDE_REJECT_USERNAME_DIALOG = 'HIDE_REJECT_USERNAME_DIALOG';
|
||||
@@ -77,7 +77,10 @@ const mapDispatchToProps = dispatch => ({
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withBanUser,
|
||||
withSetCommentStatus
|
||||
)(BanUserDialogContainer);
|
||||
|
||||
@@ -6,6 +6,7 @@ import Login from '../containers/Login';
|
||||
import { FullLoading } from '../components/FullLoading';
|
||||
import BanUserDialog from './BanUserDialog';
|
||||
import SuspendUserDialog from './SuspendUserDialog';
|
||||
import RejectUsernameDialog from './RejectUsernameDialog';
|
||||
import { toggleModal as toggleShortcutModal } from '../actions/moderation';
|
||||
import { logout } from 'coral-framework/actions/auth';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
@@ -41,6 +42,7 @@ class LayoutContainer extends React.Component {
|
||||
>
|
||||
<BanUserDialog />
|
||||
<SuspendUserDialog />
|
||||
<RejectUsernameDialog />
|
||||
<UserDetail />
|
||||
{children}
|
||||
</Layout>
|
||||
@@ -79,4 +81,7 @@ const mapDispatchToProps = dispatch =>
|
||||
dispatch
|
||||
);
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(LayoutContainer);
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import RejectUsernameDialog from '../components/RejectUsernameDialog';
|
||||
import { hideRejectUsernameDialog } from '../actions/rejectUsernameDialog';
|
||||
import {
|
||||
withRejectUsername,
|
||||
withPostFlag,
|
||||
} from 'coral-framework/graphql/mutations';
|
||||
import { notify } from 'coral-framework/actions/notification';
|
||||
import { compose } from 'react-apollo';
|
||||
import { getErrorMessages } from 'coral-framework/utils';
|
||||
|
||||
class RejectUsernameDialogContainer extends Component {
|
||||
rejectUsername = async ({ reason, message }) => {
|
||||
const {
|
||||
postFlag,
|
||||
rejectUsername,
|
||||
hideRejectUsernameDialog,
|
||||
userId,
|
||||
} = this.props;
|
||||
|
||||
// First flag the user.
|
||||
try {
|
||||
await postFlag({
|
||||
item_id: userId,
|
||||
item_type: 'USERS',
|
||||
reason,
|
||||
message,
|
||||
});
|
||||
} catch (error) {
|
||||
// Ignore already exists error, otherwise show error.
|
||||
if (
|
||||
error.errors &&
|
||||
(error.errors.length !== 1 ||
|
||||
error.errors[0].translation_key !== 'ALREADY_EXISTS')
|
||||
) {
|
||||
notify('error', getErrorMessages(error));
|
||||
}
|
||||
}
|
||||
|
||||
await rejectUsername(userId);
|
||||
hideRejectUsernameDialog();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<RejectUsernameDialog
|
||||
open={this.props.open}
|
||||
onPerform={this.rejectUsername}
|
||||
onCancel={this.props.hideRejectUsernameDialog}
|
||||
username={this.props.username}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RejectUsernameDialogContainer.propTypes = {
|
||||
rejectUsername: PropTypes.func.isRequired,
|
||||
hideRejectUsernameDialog: PropTypes.func,
|
||||
open: PropTypes.bool,
|
||||
userId: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
};
|
||||
|
||||
const mapStateToProps = ({
|
||||
rejectUsernameDialog: { open, userId, username },
|
||||
}) => ({
|
||||
open,
|
||||
userId,
|
||||
username,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
...bindActionCreators(
|
||||
{
|
||||
hideRejectUsernameDialog,
|
||||
notify,
|
||||
},
|
||||
dispatch
|
||||
),
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withRejectUsername,
|
||||
withPostFlag({ notifyOnError: false })
|
||||
)(RejectUsernameDialogContainer);
|
||||
@@ -55,4 +55,7 @@ SignInContainer.propTypes = {
|
||||
requireRecaptcha: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default compose(withSignIn, withPopupAuthHandler)(SignInContainer);
|
||||
export default compose(
|
||||
withSignIn,
|
||||
withPopupAuthHandler
|
||||
)(SignInContainer);
|
||||
|
||||
@@ -86,7 +86,10 @@ const mapDispatchToProps = dispatch => ({
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withSuspendUser,
|
||||
withSetCommentStatus,
|
||||
withOrganizationName
|
||||
|
||||
@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import UserDetail from '../components/UserDetail';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import { withRouter } from 'react-router';
|
||||
import {
|
||||
getDefinitionName,
|
||||
getSlotFragmentSpreads,
|
||||
@@ -21,11 +22,14 @@ import {
|
||||
withSetCommentStatus,
|
||||
withUnbanUser,
|
||||
withUnsuspendUser,
|
||||
withRejectUsername,
|
||||
withPostFlag,
|
||||
} from 'coral-framework/graphql/mutations';
|
||||
import UserDetailComment from './UserDetailComment';
|
||||
import update from 'immutability-helper';
|
||||
import { showBanUserDialog } from 'actions/banUserDialog';
|
||||
import { showSuspendUserDialog } from 'actions/suspendUserDialog';
|
||||
import { showRejectUsernameDialog } from 'actions/rejectUsernameDialog';
|
||||
|
||||
const commentConnectionFragment = gql`
|
||||
fragment CoralAdmin_UserDetail_CommentConnection on CommentConnection {
|
||||
@@ -131,6 +135,7 @@ class UserDetailContainer extends React.Component {
|
||||
loading={loading}
|
||||
error={this.props.data && this.props.data.error}
|
||||
loadMore={this.loadMore}
|
||||
rejectUsername={this.props.rejectUsername}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
@@ -148,6 +153,7 @@ UserDetailContainer.propTypes = {
|
||||
selectedCommentIds: PropTypes.array,
|
||||
unbanUser: PropTypes.func.isRequired,
|
||||
unsuspendUser: PropTypes.func.isRequired,
|
||||
rejectUsername: PropTypes.func.isRequired,
|
||||
userId: PropTypes.string,
|
||||
};
|
||||
|
||||
@@ -275,6 +281,7 @@ const mapDispatchToProps = dispatch => ({
|
||||
{
|
||||
showBanUserDialog,
|
||||
showSuspendUserDialog,
|
||||
showRejectUsernameDialog,
|
||||
changeTab,
|
||||
clearUserDetailSelections,
|
||||
toggleSelectCommentInUserDetail,
|
||||
@@ -287,9 +294,15 @@ const mapDispatchToProps = dispatch => ({
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withUserDetailQuery,
|
||||
withSetCommentStatus,
|
||||
withUnbanUser,
|
||||
withUnsuspendUser
|
||||
withUnsuspendUser,
|
||||
withRejectUsername,
|
||||
withPostFlag,
|
||||
withRouter
|
||||
)(UserDetailContainer);
|
||||
|
||||
@@ -66,7 +66,11 @@ export default {
|
||||
});
|
||||
},
|
||||
}),
|
||||
SuspendUser: ({ variables: { input: { id, until } } }) => ({
|
||||
SuspendUser: ({
|
||||
variables: {
|
||||
input: { id, until },
|
||||
},
|
||||
}) => ({
|
||||
update: proxy => {
|
||||
const fragmentId = `User_${id}`;
|
||||
|
||||
@@ -92,7 +96,11 @@ export default {
|
||||
});
|
||||
},
|
||||
}),
|
||||
UnsuspendUser: ({ variables: { input: { id } } }) => ({
|
||||
UnsuspendUser: ({
|
||||
variables: {
|
||||
input: { id },
|
||||
},
|
||||
}) => ({
|
||||
update: proxy => {
|
||||
const fragmentId = `User_${id}`;
|
||||
const data = proxy.readFragment({
|
||||
@@ -117,7 +125,11 @@ export default {
|
||||
});
|
||||
},
|
||||
}),
|
||||
BanUser: ({ variables: { input: { id } } }) => ({
|
||||
BanUser: ({
|
||||
variables: {
|
||||
input: { id },
|
||||
},
|
||||
}) => ({
|
||||
update: proxy => {
|
||||
const fragmentId = `User_${id}`;
|
||||
const data = proxy.readFragment({
|
||||
@@ -142,7 +154,11 @@ export default {
|
||||
});
|
||||
},
|
||||
}),
|
||||
UnbanUser: ({ variables: { input: { id } } }) => ({
|
||||
UnbanUser: ({
|
||||
variables: {
|
||||
input: { id },
|
||||
},
|
||||
}) => ({
|
||||
update: proxy => {
|
||||
const fragmentId = `User_${id}`;
|
||||
const data = proxy.readFragment({
|
||||
@@ -194,6 +210,12 @@ export default {
|
||||
},
|
||||
updateQueries: {
|
||||
TalkAdmin_Community_FlaggedAccounts: (prev, { mutationResult }) => {
|
||||
// No need to update, when user was not in the flagged users queue.
|
||||
// TODO: this should be more generic, e.g. looking at the history.
|
||||
if (!prev.flaggedUsers.nodes.find(node => node.id === id)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const decrement = {
|
||||
flaggedUsernamesCount: { $apply: count => count - 1 },
|
||||
};
|
||||
@@ -214,35 +236,6 @@ export default {
|
||||
return updated;
|
||||
},
|
||||
},
|
||||
update: proxy => {
|
||||
proxy.writeFragment({
|
||||
fragment: gql`
|
||||
fragment Talk_ApproveUsername on User {
|
||||
state {
|
||||
status {
|
||||
username {
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
id: `User_${id}`,
|
||||
data: {
|
||||
__typename: 'User',
|
||||
state: {
|
||||
__typename: 'UserState',
|
||||
status: {
|
||||
__typename: 'UserStatus',
|
||||
username: {
|
||||
__typename: 'UsernameStatus',
|
||||
status: 'APPROVED',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
RejectUsername: ({ variables: { id } }) => ({
|
||||
optimisticResponse: {
|
||||
@@ -254,6 +247,12 @@ export default {
|
||||
},
|
||||
updateQueries: {
|
||||
TalkAdmin_Community_FlaggedAccounts: (prev, { mutationResult }) => {
|
||||
// No need to update, when user was not in the flagged users queue.
|
||||
// TODO: this should be more generic, e.g. looking at the history.
|
||||
if (!prev.flaggedUsers.nodes.find(node => node.id === id)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const decrement = {
|
||||
flaggedUsernamesCount: { $apply: count => count - 1 },
|
||||
};
|
||||
@@ -274,35 +273,6 @@ export default {
|
||||
return updated;
|
||||
},
|
||||
},
|
||||
update: proxy => {
|
||||
proxy.writeFragment({
|
||||
fragment: gql`
|
||||
fragment Talk_RejectUsername on User {
|
||||
state {
|
||||
status {
|
||||
username {
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
id: `User_${id}`,
|
||||
data: {
|
||||
__typename: 'User',
|
||||
state: {
|
||||
__typename: 'UserState',
|
||||
status: {
|
||||
__typename: 'UserStatus',
|
||||
username: {
|
||||
__typename: 'UsernameStatus',
|
||||
status: 'REJECTED',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
UpdateSettings: ({ variables: { input } }) => ({
|
||||
updateQueries: {
|
||||
|
||||
@@ -14,6 +14,11 @@ import { hideShortcutsNote } from './actions/moderation';
|
||||
|
||||
smoothscroll.polyfill();
|
||||
|
||||
if (!NodeList.prototype.forEach) {
|
||||
// Polyfill IE11 missing forEach in NodeList.
|
||||
NodeList.prototype.forEach = Array.prototype.forEach;
|
||||
}
|
||||
|
||||
function init({ store, localStorage }) {
|
||||
const shouldHide = localStorage.getItem('coral:shortcutsNote') === 'hide';
|
||||
if (shouldHide) {
|
||||
|
||||
@@ -5,10 +5,12 @@ import moderation from './moderation';
|
||||
import install from './install';
|
||||
import banUserDialog from './banUserDialog';
|
||||
import suspendUserDialog from './suspendUserDialog';
|
||||
import rejectUsernameDialog from './rejectUsernameDialog';
|
||||
import userDetail from './userDetail';
|
||||
import ui from './ui';
|
||||
|
||||
export default {
|
||||
rejectUsernameDialog,
|
||||
banUserDialog,
|
||||
configure,
|
||||
suspendUserDialog,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
SHOW_REJECT_USERNAME_DIALOG,
|
||||
HIDE_REJECT_USERNAME_DIALOG,
|
||||
} from '../constants/rejectUsernameDialog';
|
||||
|
||||
const initialState = {
|
||||
open: false,
|
||||
userId: null,
|
||||
username: '',
|
||||
};
|
||||
|
||||
export default function rejectUsernameDialog(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case SHOW_REJECT_USERNAME_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
open: true,
|
||||
userId: action.userId,
|
||||
username: action.username,
|
||||
};
|
||||
case HIDE_REJECT_USERNAME_DIALOG:
|
||||
return {
|
||||
...state,
|
||||
open: false,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
.filterHeader {
|
||||
margin-top: 30px;
|
||||
font-size: 16px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.filterDetail {
|
||||
margin-top: 15px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.radioGroup {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
width: calc(100% - 300px);
|
||||
padding: 34px 14px;
|
||||
|
||||
@@ -9,6 +9,7 @@ import PropTypes from 'prop-types';
|
||||
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
|
||||
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
import { isSuspended, isBanned } from 'coral-framework/utils/user';
|
||||
import { RadioGroup, Radio } from 'react-mdl';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
ADMIN,
|
||||
@@ -64,11 +65,12 @@ class People extends React.Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
onSearchChange,
|
||||
onFilterChange,
|
||||
users = [],
|
||||
setUserRole,
|
||||
viewUserDetail,
|
||||
loadMore,
|
||||
filters,
|
||||
} = this.props;
|
||||
|
||||
const hasResults = !!users.nodes.length;
|
||||
@@ -87,11 +89,43 @@ class People extends React.Component {
|
||||
id="commenters-search"
|
||||
type="text"
|
||||
className={styles.searchBoxInput}
|
||||
defaultValue=""
|
||||
onChange={onSearchChange}
|
||||
value={filters.search}
|
||||
onChange={onFilterChange('search')}
|
||||
placeholder={t('streams.search')}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterHeader}>
|
||||
{t('community.filter_users')}
|
||||
</div>
|
||||
<div className={styles.filterDetail}>{t('community.status')}</div>
|
||||
<RadioGroup
|
||||
name="statusFilter"
|
||||
value={filters.status}
|
||||
childContainer="div"
|
||||
onChange={onFilterChange('status')}
|
||||
className={styles.radioGroup}
|
||||
>
|
||||
<Radio value="">{t('community.all')}</Radio>
|
||||
<Radio value="active">{t('community.active')}</Radio>
|
||||
<Radio value="suspended">{t('community.suspended')}</Radio>
|
||||
<Radio value="banned">{t('community.banned')}</Radio>
|
||||
</RadioGroup>
|
||||
<div className={styles.filterDetail}>
|
||||
{t('community.filter_role')}
|
||||
</div>
|
||||
<RadioGroup
|
||||
name="roleFilter"
|
||||
value={filters.role}
|
||||
childContainer="div"
|
||||
onChange={onFilterChange('role')}
|
||||
className={styles.radioGroup}
|
||||
>
|
||||
<Radio value="">{t('community.all')}</Radio>
|
||||
<Radio value={ADMIN}>{t('community.admin')}</Radio>
|
||||
<Radio value={STAFF}>{t('community.staff')}</Radio>
|
||||
<Radio value={MODERATOR}>{t('community.moderator')}</Radio>
|
||||
<Radio value={COMMENTER}>{t('community.commenter')}</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div className={styles.mainContent}>
|
||||
{hasResults ? (
|
||||
@@ -176,7 +210,7 @@ class People extends React.Component {
|
||||
})
|
||||
}
|
||||
>
|
||||
Suspend User
|
||||
{t('modqueue.suspend')}
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
|
||||
@@ -195,7 +229,7 @@ class People extends React.Component {
|
||||
})
|
||||
}
|
||||
>
|
||||
Ban User
|
||||
{t('modqueue.ban_user_actions')}
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
</ActionsMenu>
|
||||
@@ -254,7 +288,8 @@ class People extends React.Component {
|
||||
|
||||
People.propTypes = {
|
||||
users: PropTypes.object.isRequired,
|
||||
onSearchChange: PropTypes.func.isRequired,
|
||||
filters: PropTypes.object.isRequired,
|
||||
onFilterChange: PropTypes.func.isRequired,
|
||||
setUserRole: PropTypes.func.isRequired,
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
unbanUser: PropTypes.func.isRequired,
|
||||
|
||||
@@ -71,7 +71,7 @@ class RejectUsernameDialog extends Component {
|
||||
<Dialog
|
||||
className={cn(
|
||||
styles.suspendDialog,
|
||||
'talk-admin-reject-username-dialog'
|
||||
'talk-admin-reject-reported-username-dialog'
|
||||
)}
|
||||
id="rejectUsernameDialog"
|
||||
open={open}
|
||||
@@ -85,7 +85,7 @@ class RejectUsernameDialog extends Component {
|
||||
<div
|
||||
className={cn(
|
||||
styles.container,
|
||||
`talk-admin-reject-username-dialog-step-${stage}`
|
||||
`talk-admin-reject-reported-username-dialog-step-${stage}`
|
||||
)}
|
||||
>
|
||||
<div className={styles.description}>
|
||||
@@ -101,7 +101,7 @@ class RejectUsernameDialog extends Component {
|
||||
<div className={styles.emailContainer}>
|
||||
<textarea
|
||||
rows={5}
|
||||
className={cn(styles.emailInput, 'talk-admin-reject-username-dialog-suspension-message')}
|
||||
className={cn(styles.emailInput, 'talk-admin-reject-reported-username-dialog-suspension-message')}
|
||||
value={this.state.email}
|
||||
onChange={this.onEmailChange}/>
|
||||
</div>
|
||||
@@ -110,7 +110,7 @@ class RejectUsernameDialog extends Component {
|
||||
<div
|
||||
className={cn(
|
||||
styles.modalButtons,
|
||||
'talk-admin-reject-username-dialog-buttons'
|
||||
'talk-admin-reject-reported-username-dialog-buttons'
|
||||
)}
|
||||
>
|
||||
{Object.keys(stages[stage].options).map((key, i) => (
|
||||
@@ -118,7 +118,7 @@ class RejectUsernameDialog extends Component {
|
||||
key={i}
|
||||
className={cn(
|
||||
'talk-admin-username-dialog-button',
|
||||
`talk-admin-reject-username-dialog-button-${key}`
|
||||
`talk-admin-reject-reported-username-dialog-button-${key}`
|
||||
)}
|
||||
onClick={this.onActionClick(stage, i)}
|
||||
>
|
||||
|
||||
@@ -46,7 +46,11 @@ class FlaggedAccountsContainer extends Component {
|
||||
document: USERNAME_FLAGGED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameFlagged: user } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { usernameFlagged: user },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return handleFlaggedAccountsChange(prev, user, () => {
|
||||
const msg = t(
|
||||
@@ -62,7 +66,11 @@ class FlaggedAccountsContainer extends Component {
|
||||
document: USERNAME_APPROVED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameApproved: user } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { usernameApproved: user },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return handleFlaggedAccountsChange(prev, user, () => {
|
||||
const msg = t(
|
||||
@@ -78,7 +86,11 @@ class FlaggedAccountsContainer extends Component {
|
||||
document: USERNAME_REJECTED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameRejected: user } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { usernameRejected: user },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return handleFlaggedAccountsChange(prev, user, () => {
|
||||
const msg = t(
|
||||
@@ -96,7 +108,9 @@ class FlaggedAccountsContainer extends Component {
|
||||
prev,
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { usernameChanged: { previousUsername, user } },
|
||||
data: {
|
||||
usernameChanged: { previousUsername, user },
|
||||
},
|
||||
},
|
||||
}
|
||||
) => {
|
||||
@@ -297,7 +311,10 @@ const mapDispatchToProps = dispatch =>
|
||||
);
|
||||
|
||||
export default compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withApproveUsername,
|
||||
withQuery(
|
||||
gql`
|
||||
|
||||
@@ -15,7 +15,11 @@ class IndicatorContainer extends Component {
|
||||
document: USERNAME_FLAGGED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameFlagged: user } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { usernameFlagged: user },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return handleIndicatorChange(prev, user);
|
||||
},
|
||||
@@ -24,7 +28,11 @@ class IndicatorContainer extends Component {
|
||||
document: USERNAME_APPROVED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameApproved: user } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { usernameApproved: user },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return handleIndicatorChange(prev, user);
|
||||
},
|
||||
@@ -33,7 +41,11 @@ class IndicatorContainer extends Component {
|
||||
document: USERNAME_REJECTED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameRejected: user } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { usernameRejected: user },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return handleIndicatorChange(prev, user);
|
||||
},
|
||||
@@ -42,7 +54,13 @@ class IndicatorContainer extends Component {
|
||||
document: USERNAME_CHANGED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { usernameChanged: { user } } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: {
|
||||
usernameChanged: { user },
|
||||
},
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return handleIndicatorChange(prev, user);
|
||||
},
|
||||
@@ -98,9 +116,15 @@ const fields = `
|
||||
status {
|
||||
username {
|
||||
status
|
||||
history {
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
action_summaries {
|
||||
count
|
||||
}
|
||||
`;
|
||||
|
||||
const USERNAME_FLAGGED_SUBSCRIPTION = gql`
|
||||
|
||||
@@ -17,59 +17,65 @@ import update from 'immutability-helper';
|
||||
import { Spinner } from 'coral-ui';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
|
||||
class PeopleContainer extends React.Component {
|
||||
class PeopleContainer extends React.PureComponent {
|
||||
timer = null;
|
||||
|
||||
state = {
|
||||
searchValue: '',
|
||||
search: '',
|
||||
role: '',
|
||||
status: '',
|
||||
};
|
||||
|
||||
onSearchChange = e => {
|
||||
const { value } = e.target;
|
||||
this.setState({ searchValue: value }, () => {
|
||||
statusToQuery = {
|
||||
active: { suspended: false, banned: false },
|
||||
suspended: { suspended: true },
|
||||
banned: { banned: true },
|
||||
};
|
||||
|
||||
onFilterChange = filter => e =>
|
||||
this.setState({ [filter]: e.target.value }, () => {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = setTimeout(() => {
|
||||
this.search(value);
|
||||
}, 350);
|
||||
this.timer = setTimeout(this.filter, 350);
|
||||
});
|
||||
|
||||
getFilterState = () => {
|
||||
const { role, status } = this.state;
|
||||
|
||||
return {
|
||||
status: this.statusToQuery[status] || null,
|
||||
role: role || null,
|
||||
};
|
||||
};
|
||||
|
||||
search = async value => {
|
||||
return this.props.data.fetchMore({
|
||||
query: SEARCH_QUERY,
|
||||
filter = () =>
|
||||
this.props.data.fetchMore({
|
||||
query: FILTER_QUERY,
|
||||
variables: {
|
||||
value,
|
||||
state: this.getFilterState(),
|
||||
value: this.state.search,
|
||||
limit: 10,
|
||||
},
|
||||
updateQuery: (previous, { fetchMoreResult: { users } }) => {
|
||||
const updated = update(previous, {
|
||||
updateQuery: (previous, { fetchMoreResult: { users } }) =>
|
||||
update(previous, {
|
||||
users: {
|
||||
nodes: {
|
||||
$set: users.nodes,
|
||||
},
|
||||
nodes: { $set: users.nodes },
|
||||
hasNextPage: { $set: users.hasNextPage },
|
||||
endCursor: { $set: users.endCursor },
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
},
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
setUserRole = async (id, role) => {
|
||||
await this.props.setUserRole(id, role);
|
||||
};
|
||||
|
||||
loadMore = () => {
|
||||
return this.props.data.fetchMore({
|
||||
loadMore = () =>
|
||||
this.props.data.fetchMore({
|
||||
query: LOAD_MORE_QUERY,
|
||||
variables: {
|
||||
value: this.state.searchValue,
|
||||
limit: 5,
|
||||
cursor: this.props.root.users.endCursor,
|
||||
state: this.getFilterState(),
|
||||
value: this.state.search,
|
||||
limit: 5,
|
||||
},
|
||||
updateQuery: (previous, { fetchMoreResult: { users } }) => {
|
||||
const updated = update(previous, {
|
||||
updateQuery: (previous, { fetchMoreResult: { users } }) =>
|
||||
update(previous, {
|
||||
users: {
|
||||
nodes: {
|
||||
$apply: nodes => appendNewNodes(nodes, users.nodes),
|
||||
@@ -77,11 +83,8 @@ class PeopleContainer extends React.Component {
|
||||
hasNextPage: { $set: users.hasNextPage },
|
||||
endCursor: { $set: users.endCursor },
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
},
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.props.data.error) {
|
||||
@@ -98,9 +101,9 @@ class PeopleContainer extends React.Component {
|
||||
|
||||
return (
|
||||
<People
|
||||
onSearchChange={this.onSearchChange}
|
||||
onFilterChange={this.onFilterChange}
|
||||
viewUserDetail={this.props.viewUserDetail}
|
||||
setUserRole={this.setUserRole}
|
||||
setUserRole={this.props.setUserRole}
|
||||
showSuspendUserDialog={this.props.showSuspendUserDialog}
|
||||
showBanUserDialog={this.props.showBanUserDialog}
|
||||
unbanUser={this.props.unbanUser}
|
||||
@@ -109,6 +112,7 @@ class PeopleContainer extends React.Component {
|
||||
root={this.props.root}
|
||||
users={this.props.root.users}
|
||||
loadMore={this.loadMore}
|
||||
filters={this.state}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -125,16 +129,6 @@ PeopleContainer.propTypes = {
|
||||
root: PropTypes.object,
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
viewUserDetail,
|
||||
showSuspendUserDialog,
|
||||
showBanUserDialog,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
const LOAD_MORE_QUERY = gql`
|
||||
query TalkAdmin_Community_People_LoadMoreUsers(
|
||||
$limit: Int
|
||||
@@ -169,9 +163,13 @@ const LOAD_MORE_QUERY = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
const SEARCH_QUERY = gql`
|
||||
query TalkAdmin_Community_People_SearchUsers($value: String, $limit: Int) {
|
||||
users(query: { value: $value, limit: $limit }) {
|
||||
const FILTER_QUERY = gql`
|
||||
query TalkAdmin_Community_People_FilterUsers(
|
||||
$state: UserStateInput
|
||||
$value: String
|
||||
$limit: Int
|
||||
) {
|
||||
users(query: { state: $state, value: $value, limit: $limit }) {
|
||||
hasNextPage
|
||||
endCursor
|
||||
nodes {
|
||||
@@ -199,8 +197,21 @@ const SEARCH_QUERY = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
viewUserDetail,
|
||||
showSuspendUserDialog,
|
||||
showBanUserDialog,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
export default compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withSetUserRole,
|
||||
withUnsuspendUser,
|
||||
withUnbanUser,
|
||||
|
||||
@@ -19,6 +19,9 @@ const mapDispatchToProps = dispatch =>
|
||||
);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withRejectUsername
|
||||
)(RejectUsernameDialog);
|
||||
|
||||
@@ -106,6 +106,23 @@ export function handleFlaggedAccountsChange(root, user, notify) {
|
||||
}
|
||||
}
|
||||
|
||||
export const wasUsernameReported = user => {
|
||||
const previousStatus =
|
||||
user.state.status.username.history[
|
||||
user.state.status.username.history.length - 2
|
||||
];
|
||||
|
||||
// Check for correct previous status
|
||||
if (!['SET', 'CHANGES'].includes(previousStatus)) {
|
||||
return false;
|
||||
}
|
||||
// Check for flags
|
||||
if (user.action_summaries.every(as => as.count === 0)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Track indicator status
|
||||
* @param {Object} root current state of the store
|
||||
@@ -119,7 +136,9 @@ export function handleIndicatorChange(root, user) {
|
||||
return incrementFlaggedUserCount(root);
|
||||
case 'APPROVED':
|
||||
case 'REJECTED':
|
||||
return decrementFlaggedUserCount(root);
|
||||
if (wasUsernameReported(user)) {
|
||||
return decrementFlaggedUserCount(root);
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ class EmbedLink extends Component {
|
||||
`;
|
||||
|
||||
return (
|
||||
<ConfigureCard title={'Embed Comment Stream'}>
|
||||
<ConfigureCard title={t('configure.embed_comment_stream')}>
|
||||
<p>{t('configure.copy_and_paste')}</p>
|
||||
<textarea
|
||||
rows={5}
|
||||
@@ -53,7 +53,7 @@ class EmbedLink extends Component {
|
||||
{t('embedlink.copy')}
|
||||
</Button>
|
||||
<div className={styles.copiedText}>
|
||||
{this.state.copied && 'Copied!'}
|
||||
{this.state.copied && t('embedlink.copied')}
|
||||
</div>
|
||||
</div>
|
||||
</ConfigureCard>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SelectField, Option } from 'react-mdl-selectfield';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import styles from './StreamSettings.css';
|
||||
import { Textfield } from 'react-mdl';
|
||||
import { Icon, TextArea } from 'coral-ui';
|
||||
import { Icon } from 'coral-ui';
|
||||
import PropTypes from 'prop-types';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import MarkdownEditor from 'coral-framework/components/MarkdownEditor';
|
||||
@@ -66,8 +66,8 @@ class StreamSettings extends React.Component {
|
||||
this.props.updatePending({ updater });
|
||||
};
|
||||
|
||||
updateClosedMessage = event => {
|
||||
const updater = { closedMessage: { $set: event.target.value } };
|
||||
updateClosedMessage = value => {
|
||||
const updater = { closedMessage: { $set: value } };
|
||||
this.props.updatePending({ updater });
|
||||
};
|
||||
|
||||
@@ -155,9 +155,19 @@ class StreamSettings extends React.Component {
|
||||
<ConfigureCard
|
||||
checked={settings.infoBoxEnable}
|
||||
onCheckbox={this.updateInfoBoxEnable}
|
||||
title={t('configure.include_comment_stream')}
|
||||
title={t('configure.code_of_conduct_summary')}
|
||||
>
|
||||
<p>{t('configure.include_comment_stream_desc')}</p>
|
||||
<p>
|
||||
{t('configure.code_of_conduct_summary_desc')}
|
||||
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://guides.coralproject.net/create-a-code-of-conduct/"
|
||||
>
|
||||
Code of Conduct Guide.
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
className={cn(
|
||||
styles.configSettingInfoBox,
|
||||
@@ -178,7 +188,7 @@ class StreamSettings extends React.Component {
|
||||
>
|
||||
<p>{t('configure.closed_comments_desc')}</p>
|
||||
<div>
|
||||
<TextArea
|
||||
<MarkdownEditor
|
||||
className={styles.descriptionBox}
|
||||
onChange={this.updateClosedMessage}
|
||||
value={settings.closedMessage}
|
||||
@@ -238,12 +248,14 @@ class StreamSettings extends React.Component {
|
||||
onChange={this.updateClosedTimeout}
|
||||
value={getTimeoutAmount(settings.closedTimeout)}
|
||||
label={t('configure.closed_comments_label')}
|
||||
disabled={!settings.autoCloseStream}
|
||||
/>
|
||||
<div className={styles.configTimeoutSelect}>
|
||||
<SelectField
|
||||
label="comments closed time window"
|
||||
value={getTimeoutMeasure(settings.closedTimeout)}
|
||||
onChange={this.updateClosedTimeoutMeasure}
|
||||
readOnly={!settings.autoCloseStream}
|
||||
>
|
||||
<Option value={'hours'}>{t('configure.hours')}</Option>
|
||||
<Option value={'days'}>{t('configure.days')}</Option>
|
||||
|
||||
@@ -6,19 +6,23 @@ import ConfigureCard from 'coral-framework/components/ConfigureCard';
|
||||
|
||||
const Wordlist = ({ suspectWords, bannedWords, onChangeWordlist }) => (
|
||||
<div>
|
||||
<ConfigureCard title={t('configure.banned_words_title')}>
|
||||
<ConfigureCard collapsible title={t('configure.banned_words_title')}>
|
||||
<p>{t('configure.banned_word_text')}</p>
|
||||
<TagsInput
|
||||
value={bannedWords}
|
||||
inputProps={{ placeholder: 'word or phrase' }}
|
||||
inputProps={{
|
||||
placeholder: t('configure.suspect_or_forbidden_words_placeholder'),
|
||||
}}
|
||||
onChange={tags => onChangeWordlist('banned', tags)}
|
||||
/>
|
||||
</ConfigureCard>
|
||||
<ConfigureCard title={t('configure.suspect_word_title')}>
|
||||
<ConfigureCard collapsible title={t('configure.suspect_word_title')}>
|
||||
<p>{t('configure.suspect_word_text')}</p>
|
||||
<TagsInput
|
||||
value={suspectWords}
|
||||
inputProps={{ placeholder: 'word or phrase' }}
|
||||
inputProps={{
|
||||
placeholder: t('configure.suspect_or_forbidden_words_placeholder'),
|
||||
}}
|
||||
onChange={tags => onChangeWordlist('suspect', tags)}
|
||||
/>
|
||||
</ConfigureCard>
|
||||
|
||||
@@ -174,7 +174,10 @@ const mapDispatchToProps = dispatch =>
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withUpdateSettings,
|
||||
withConfigureQuery,
|
||||
withMergedSettings('root.settings', 'pending', 'mergedSettings')
|
||||
|
||||
@@ -42,7 +42,10 @@ export default compose(
|
||||
}
|
||||
`,
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
|
||||
slotPassthrough: {
|
||||
root,
|
||||
|
||||
@@ -37,7 +37,10 @@ export default compose(
|
||||
}
|
||||
`,
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
|
||||
slotPassthrough: {
|
||||
root,
|
||||
|
||||
@@ -45,7 +45,10 @@ export default compose(
|
||||
}
|
||||
`,
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
|
||||
slotPassthrough: {
|
||||
root,
|
||||
|
||||
@@ -39,7 +39,10 @@ export default compose(
|
||||
}
|
||||
`,
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
|
||||
slotPassthrough: {
|
||||
root,
|
||||
|
||||
@@ -83,4 +83,7 @@ const mapDispatchToProps = dispatch =>
|
||||
dispatch
|
||||
);
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(InstallContainer);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(InstallContainer);
|
||||
|
||||
@@ -61,13 +61,19 @@
|
||||
line-height: 1.5;
|
||||
font-weight: 300;
|
||||
margin-bottom: 8px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-top: 0px;
|
||||
flex: 1;
|
||||
color: black;
|
||||
max-width: 500px;
|
||||
/*
|
||||
IE11 fix – Next line was supposed to be:
|
||||
max-width: 500px;
|
||||
*/
|
||||
padding-right: 20px;
|
||||
/** IE11 fix end **/
|
||||
font-weight: 300;
|
||||
font-size: 16px;
|
||||
overflow-wrap: break-word;
|
||||
@@ -188,4 +194,4 @@
|
||||
|
||||
.commentContentFooter {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,10 @@ import cn from 'classnames';
|
||||
import ApproveButton from 'coral-admin/src/components/ApproveButton';
|
||||
import RejectButton from 'coral-admin/src/components/RejectButton';
|
||||
import CommentDeletedTombstone from '../../../components/CommentDeletedTombstone';
|
||||
import TimeAgo from 'coral-framework/components/TimeAgo';
|
||||
|
||||
import t, { timeago } from 'coral-framework/services/i18n';
|
||||
import { buildCommentURL } from 'coral-framework/utils/url';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
class Comment extends React.Component {
|
||||
ref = null;
|
||||
@@ -126,9 +128,10 @@ class Comment extends React.Component {
|
||||
{comment.user.username}
|
||||
</span>
|
||||
|
||||
<span className={styles.created}>
|
||||
{timeago(comment.created_at)}
|
||||
</span>
|
||||
<TimeAgo
|
||||
className={styles.created}
|
||||
datetime={comment.created_at}
|
||||
/>
|
||||
{comment.editing && comment.editing.edited ? (
|
||||
<span>
|
||||
<span className={styles.editedMarker}>
|
||||
@@ -168,8 +171,9 @@ class Comment extends React.Component {
|
||||
<div className={styles.commentContentFooter}>
|
||||
<a
|
||||
className={styles.external}
|
||||
href={`${comment.asset.url}?commentId=${comment.id}`}
|
||||
href={buildCommentURL(comment.asset.url, comment.id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon name="open_in_new" /> {t('comment.view_context')}
|
||||
</a>
|
||||
@@ -179,8 +183,7 @@ class Comment extends React.Component {
|
||||
<div className={styles.sideActions}>
|
||||
<IfHasLink text={comment.body}>
|
||||
<span className={styles.hasLinks}>
|
||||
{/* TODO: translate string */}
|
||||
<Icon name="error_outline" /> Contains Link
|
||||
<Icon name="error_outline" /> {t('common.contains_link')}
|
||||
</span>
|
||||
</IfHasLink>
|
||||
<div className={`actions ${styles.actions}`}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Story.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const formatDate = date => {
|
||||
const d = new Date(date);
|
||||
@@ -14,7 +15,9 @@ const Story = ({ author, title, createdAt, open, id, goToStory }) => {
|
||||
<div className={styles.meta}>
|
||||
<span className={styles.author}>By {author}</span>
|
||||
<span className={styles.createdAt}>{formatDate(createdAt)}</span>
|
||||
<span className={styles.status}>{open ? 'Open' : 'Closed'}</span>
|
||||
<span className={styles.status}>
|
||||
{open ? t('streams.open') : t('streams.closed')}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -3,9 +3,13 @@ import PropTypes from 'prop-types';
|
||||
import styles from './StorySearch.css';
|
||||
import { Button, Spinner, Icon } from 'coral-ui';
|
||||
import Story from './Story';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const StorySearch = props => {
|
||||
const { root: { assets }, data: { loading } } = props;
|
||||
const {
|
||||
root: { assets },
|
||||
data: { loading },
|
||||
} = props;
|
||||
|
||||
if (!props.moderation.storySearchVisible) {
|
||||
return null;
|
||||
@@ -33,14 +37,14 @@ const StorySearch = props => {
|
||||
onClick={props.search}
|
||||
raised
|
||||
>
|
||||
Search
|
||||
{t('streams.search')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.results}>
|
||||
{props.assetId && (
|
||||
<div className={styles.cta}>
|
||||
<a onClick={props.goToModerateAll}>
|
||||
Moderate comments on All Stories
|
||||
{t('moderate_all_streams')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
@@ -48,13 +52,15 @@ const StorySearch = props => {
|
||||
{props.moderation.storySearchString ? (
|
||||
<div className={styles.searchResults}>
|
||||
<Icon name="search" />
|
||||
<span className={styles.headlineRecent}>Search Results</span>
|
||||
<span className={styles.headlineRecent}>
|
||||
{t('streams.search_results')}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.searchResults}>
|
||||
<Icon name="access_time" />
|
||||
<span className={styles.headlineRecent}>
|
||||
Most Recent Stories
|
||||
{t('streams.most_recent_stories')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -82,7 +88,9 @@ const StorySearch = props => {
|
||||
)}
|
||||
|
||||
{assets.nodes.length === 0 && (
|
||||
<div className={styles.noResults}>No results</div>
|
||||
<div className={styles.noResults}>
|
||||
{t('streams.search_results')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,11 @@ class IndicatorContainer extends Component {
|
||||
document: COMMENT_ADDED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentAdded: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentAdded: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
@@ -31,7 +35,11 @@ class IndicatorContainer extends Component {
|
||||
document: COMMENT_FLAGGED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentFlagged: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentFlagged: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
@@ -40,7 +48,11 @@ class IndicatorContainer extends Component {
|
||||
document: COMMENT_EDITED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentEdited: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentEdited: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
@@ -49,7 +61,11 @@ class IndicatorContainer extends Component {
|
||||
document: COMMENT_ACCEPTED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentAccepted: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentAccepted: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
@@ -58,7 +74,11 @@ class IndicatorContainer extends Component {
|
||||
document: COMMENT_REJECTED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentRejected: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentRejected: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
@@ -67,7 +87,11 @@ class IndicatorContainer extends Component {
|
||||
document: COMMENT_RESET_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentReset: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentReset: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
|
||||
@@ -71,7 +71,9 @@ class ModerationContainer extends Component {
|
||||
};
|
||||
|
||||
get activeTab() {
|
||||
const { root: { asset, settings } } = this.props;
|
||||
const {
|
||||
root: { asset, settings },
|
||||
} = this.props;
|
||||
const id = getAssetId(this.props);
|
||||
const tab = getTab(this.props);
|
||||
|
||||
@@ -94,7 +96,11 @@ class ModerationContainer extends Component {
|
||||
variables,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentAdded: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentAdded: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
@@ -104,7 +110,11 @@ class ModerationContainer extends Component {
|
||||
variables,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentAccepted: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentAccepted: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
const user =
|
||||
comment.status_history[comment.status_history.length - 1]
|
||||
@@ -125,7 +135,11 @@ class ModerationContainer extends Component {
|
||||
variables,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentRejected: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentRejected: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
const user =
|
||||
comment.status_history[comment.status_history.length - 1]
|
||||
@@ -146,7 +160,11 @@ class ModerationContainer extends Component {
|
||||
variables,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentReset: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentReset: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
const user =
|
||||
comment.status_history[comment.status_history.length - 1]
|
||||
@@ -167,7 +185,11 @@ class ModerationContainer extends Component {
|
||||
variables,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentEdited: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentEdited: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
@@ -177,7 +199,11 @@ class ModerationContainer extends Component {
|
||||
variables,
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentFlagged: comment } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentFlagged: comment },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
@@ -289,7 +315,11 @@ class ModerationContainer extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { root, root: { asset, settings }, data } = this.props;
|
||||
const {
|
||||
root,
|
||||
root: { asset, settings },
|
||||
data,
|
||||
} = this.props;
|
||||
const assetId = getAssetId(this.props);
|
||||
|
||||
if (assetId) {
|
||||
@@ -546,7 +576,10 @@ const mapDispatchToProps = dispatch => ({
|
||||
|
||||
export default compose(
|
||||
withQueueConfig(baseQueueConfig),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withSetCommentStatus,
|
||||
withModQueueQuery
|
||||
)(ModerationContainer);
|
||||
|
||||
@@ -112,4 +112,7 @@ export const withAssetSearchQuery = withQuery(
|
||||
}
|
||||
);
|
||||
|
||||
export default compose(withRouter, withAssetSearchQuery)(StorySearchContainer);
|
||||
export default compose(
|
||||
withRouter,
|
||||
withAssetSearchQuery
|
||||
)(StorySearchContainer);
|
||||
|
||||
@@ -50,7 +50,7 @@ class StoriesContainer extends Component {
|
||||
const { updateAssetState } = this.props;
|
||||
|
||||
try {
|
||||
updateAssetState(id, closeStream ? Date.now() : null);
|
||||
await updateAssetState(id, closeStream ? Date.now() : null);
|
||||
this.fetchAssets();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -113,4 +113,7 @@ StoriesContainer.propTypes = {
|
||||
updateAssetState: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(StoriesContainer);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(StoriesContainer);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { createPostMessage } from 'coral-framework/services/postMessage';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const staticConfig = getStaticConfiguration();
|
||||
const { STATIC_ORIGIN: origin } = staticConfig;
|
||||
const { BASE_ORIGIN: origin } = staticConfig;
|
||||
const postMessage = createPostMessage(origin);
|
||||
|
||||
// Get the auth element and parse it as JSON by decoding it.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as actions from '../constants/stream';
|
||||
import { buildUrl } from 'coral-framework/utils/url';
|
||||
import queryString from 'query-string';
|
||||
import queryString from 'querystringify';
|
||||
|
||||
export const setActiveReplyBox = id => ({
|
||||
type: actions.SET_ACTIVE_REPLY_BOX,
|
||||
@@ -14,12 +14,13 @@ export const setSort = ({ sortOrder, sortBy }) => ({
|
||||
});
|
||||
|
||||
export const viewAllComments = () => (dispatch, _, { pym }) => {
|
||||
const search = queryString.stringify({
|
||||
...queryString.parse(location.search),
|
||||
comment_id: undefined,
|
||||
});
|
||||
const query = queryString.parse(location.search);
|
||||
|
||||
// remove the comment_id url param
|
||||
delete query.comment_id;
|
||||
|
||||
const search = queryString.stringify(query);
|
||||
|
||||
const url = buildUrl({ ...location, search });
|
||||
|
||||
try {
|
||||
|
||||
@@ -41,7 +41,13 @@ class EmbedContainer extends React.Component {
|
||||
document: USER_BANNED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
_,
|
||||
{ subscriptionData: { data: { userBanned: { state } } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: {
|
||||
userBanned: { state },
|
||||
},
|
||||
},
|
||||
}
|
||||
) => {
|
||||
notify('info', t('your_account_has_been_banned'));
|
||||
props.updateStatus(state.status);
|
||||
@@ -51,7 +57,13 @@ class EmbedContainer extends React.Component {
|
||||
document: USER_SUSPENDED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
_,
|
||||
{ subscriptionData: { data: { userSuspended: { state } } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: {
|
||||
userSuspended: { state },
|
||||
},
|
||||
},
|
||||
}
|
||||
) => {
|
||||
notify('info', t('your_account_has_been_suspended'));
|
||||
props.updateStatus(state.status);
|
||||
@@ -61,7 +73,13 @@ class EmbedContainer extends React.Component {
|
||||
document: USERNAME_REJECTED_SUBSCRIPTION,
|
||||
updateQuery: (
|
||||
_,
|
||||
{ subscriptionData: { data: { usernameRejected: { state } } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: {
|
||||
usernameRejected: { state },
|
||||
},
|
||||
},
|
||||
}
|
||||
) => {
|
||||
notify('info', t('your_username_has_been_rejected'));
|
||||
props.updateStatus(state.status);
|
||||
@@ -324,7 +342,10 @@ const mapDispatchToProps = dispatch =>
|
||||
|
||||
export default compose(
|
||||
withPopupAuthHandler,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
branch(props => !props.checkedInitialLogin, renderComponent(Spinner)),
|
||||
withEmbedQuery
|
||||
)(EmbedContainer);
|
||||
|
||||
@@ -126,7 +126,9 @@ export default {
|
||||
},
|
||||
mutations: {
|
||||
PostComment: ({
|
||||
variables: { input: { asset_id, body, parent_id, tags = [] } },
|
||||
variables: {
|
||||
input: { asset_id, body, parent_id, tags = [] },
|
||||
},
|
||||
state: { auth },
|
||||
}) => ({
|
||||
optimisticResponse: {
|
||||
@@ -193,7 +195,13 @@ export default {
|
||||
updateQueries: {
|
||||
CoralEmbedStream_Embed: (
|
||||
prev,
|
||||
{ mutationResult: { data: { createComment: { comment } } } }
|
||||
{
|
||||
mutationResult: {
|
||||
data: {
|
||||
createComment: { comment },
|
||||
},
|
||||
},
|
||||
}
|
||||
) => {
|
||||
if (
|
||||
(![ADMIN, MODERATOR].includes(prev.me.role) &&
|
||||
@@ -208,7 +216,13 @@ export default {
|
||||
},
|
||||
CoralEmbedStream_Profile: (
|
||||
prev,
|
||||
{ mutationResult: { data: { createComment: { comment } } } }
|
||||
{
|
||||
mutationResult: {
|
||||
data: {
|
||||
createComment: { comment },
|
||||
},
|
||||
},
|
||||
}
|
||||
) => {
|
||||
return update(prev, {
|
||||
me: {
|
||||
@@ -224,7 +238,13 @@ export default {
|
||||
updateQueries: {
|
||||
CoralEmbedStream_Embed: (
|
||||
prev,
|
||||
{ mutationResult: { data: { editComment: { comment } } } }
|
||||
{
|
||||
mutationResult: {
|
||||
data: {
|
||||
editComment: { comment },
|
||||
},
|
||||
},
|
||||
}
|
||||
) => {
|
||||
if (
|
||||
!['PREMOD', 'REJECTED', 'SYSTEM_WITHHELD'].includes(comment.status)
|
||||
|
||||
@@ -139,7 +139,10 @@ const mapDispatchToProps = dispatch =>
|
||||
);
|
||||
|
||||
const enhance = compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withSettingsFragments,
|
||||
withUpdateAssetSettings,
|
||||
withMergedSettings('asset.settings', 'pending', 'mergedSettings')
|
||||
|
||||
@@ -9,6 +9,7 @@ import cn from 'classnames';
|
||||
import { getTotalReactionsCount } from 'coral-framework/utils';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { buildCommentURL } from 'coral-framework/utils/url';
|
||||
|
||||
class Comment extends React.Component {
|
||||
render() {
|
||||
@@ -34,7 +35,7 @@ class Comment extends React.Component {
|
||||
{ [styles.countZero]: reactionCount === 0 }
|
||||
)}
|
||||
>
|
||||
<Icon name="thumb_up" />
|
||||
<Icon name="whatshot" />
|
||||
<span
|
||||
className={cn(
|
||||
styles.reactionCount,
|
||||
@@ -79,9 +80,10 @@ class Comment extends React.Component {
|
||||
<li>
|
||||
<a
|
||||
className={styles.viewLink}
|
||||
href={`${this.props.comment.asset.url}?commentId=${
|
||||
href={buildCommentURL(
|
||||
this.props.comment.asset.url,
|
||||
this.props.comment.id
|
||||
}`}
|
||||
)}
|
||||
target="_parent"
|
||||
>
|
||||
<Icon name="open_in_new" className={styles.iconView} />
|
||||
|
||||
@@ -22,7 +22,14 @@ class CommentHistoryContainer extends Component {
|
||||
limit: 5,
|
||||
cursor: this.props.root.me.comments.endCursor,
|
||||
},
|
||||
updateQuery: (previous, { fetchMoreResult: { me: { comments } } }) => {
|
||||
updateQuery: (
|
||||
previous,
|
||||
{
|
||||
fetchMoreResult: {
|
||||
me: { comments },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
const updated = update(previous, {
|
||||
me: {
|
||||
comments: {
|
||||
@@ -97,7 +104,10 @@ const mapStateToProps = state => ({
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, null),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
),
|
||||
withCommentHistoryFragments,
|
||||
withFetchMore
|
||||
)(CommentHistoryContainer);
|
||||
|
||||
@@ -80,6 +80,7 @@ const mapStateToProps = state => ({
|
||||
currentUser: state.auth.user,
|
||||
});
|
||||
|
||||
export default compose(connect(mapStateToProps), withProfileQuery)(
|
||||
ProfileContainer
|
||||
);
|
||||
export default compose(
|
||||
connect(mapStateToProps),
|
||||
withProfileQuery
|
||||
)(ProfileContainer);
|
||||
|
||||
@@ -55,7 +55,10 @@ export default compose(
|
||||
${Settings.fragments.root}
|
||||
`,
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withSlotElements({
|
||||
slot: 'profileSettings',
|
||||
propName: 'profileSettingsSlotElements',
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
.comment {
|
||||
position: relative;
|
||||
padding-left: 15px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.commentContainer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.commentLevel0 {
|
||||
@@ -138,6 +143,7 @@
|
||||
}
|
||||
|
||||
.commentAvatar {
|
||||
margin-top: 10px;
|
||||
max-width: 60px;
|
||||
}
|
||||
|
||||
|
||||
@@ -117,8 +117,12 @@ export default class Comment extends React.Component {
|
||||
}
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
const { comment: { replies: prevReplies } } = this.props;
|
||||
const { comment: { replies: nextReplies } } = next;
|
||||
const {
|
||||
comment: { replies: prevReplies },
|
||||
} = this.props;
|
||||
const {
|
||||
comment: { replies: nextReplies },
|
||||
} = next;
|
||||
if (
|
||||
prevReplies &&
|
||||
nextReplies &&
|
||||
@@ -243,7 +247,10 @@ export default class Comment extends React.Component {
|
||||
}
|
||||
|
||||
loadNewReplies = () => {
|
||||
const { comment: { replies, replyCount, id }, emit } = this.props;
|
||||
const {
|
||||
comment: { replies, replyCount, id },
|
||||
emit,
|
||||
} = this.props;
|
||||
if (replyCount > replies.nodes.length) {
|
||||
this.setState({ loadingState: 'loading' });
|
||||
this.props
|
||||
@@ -292,7 +299,11 @@ export default class Comment extends React.Component {
|
||||
// getVisibileReplies returns a list containing comments
|
||||
// which were authored by current user or comes before the `idCursor`.
|
||||
getVisibileReplies() {
|
||||
const { comment: { replies }, currentUser, liveUpdates } = this.props;
|
||||
const {
|
||||
comment: { replies },
|
||||
currentUser,
|
||||
liveUpdates,
|
||||
} = this.props;
|
||||
const idCursor = this.state.idCursors[0];
|
||||
const userId = currentUser ? currentUser.id : null;
|
||||
|
||||
@@ -634,7 +645,7 @@ export default class Comment extends React.Component {
|
||||
})}
|
||||
onClick={this.onClickEdit}
|
||||
>
|
||||
Edit
|
||||
{t('framework.edit')}
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -18,6 +18,7 @@ const ModerationLink = props =>
|
||||
className="talk-embed-stream-moderation-link"
|
||||
href={`${BASE_PATH}admin/moderate/${props.assetId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('moderate_this_stream')}
|
||||
</a>
|
||||
|
||||
@@ -38,5 +38,4 @@
|
||||
position: relative;
|
||||
margin-top: 28px;
|
||||
padding-bottom: 50px;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ import StreamError from './StreamError';
|
||||
import Comment from '../containers/Comment';
|
||||
import BannedAccount from '../../../components/BannedAccount';
|
||||
import ChangeUsername from '../containers/ChangeUsername';
|
||||
import Markdown from 'coral-framework/components/Markdown';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import InfoBox from './InfoBox';
|
||||
import { can } from 'coral-framework/services/perms';
|
||||
import ModerationLink from './ModerationLink';
|
||||
import Markdown from 'coral-framework/components/Markdown';
|
||||
import RestrictedMessageBox from 'coral-framework/components/RestrictedMessageBox';
|
||||
import t, { timeago } from 'coral-framework/services/i18n';
|
||||
import CommentBox from '../containers/CommentBox';
|
||||
@@ -206,12 +206,30 @@ class Stream extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderQuestionBox() {
|
||||
const {
|
||||
root,
|
||||
asset,
|
||||
asset: {
|
||||
settings: { questionBoxEnable, questionBoxContent, questionBoxIcon },
|
||||
},
|
||||
} = this.props;
|
||||
const slotPassthrough = { root, asset };
|
||||
if (questionBoxEnable) {
|
||||
return (
|
||||
<QuestionBox content={questionBoxContent} icon={questionBoxIcon}>
|
||||
<Slot fill="streamQuestionArea" passthrough={slotPassthrough} />
|
||||
</QuestionBox>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
root,
|
||||
appendItemArray,
|
||||
asset,
|
||||
asset: { comment: highlightedComment, settings: { questionBoxEnable } },
|
||||
asset: { comment: highlightedComment },
|
||||
postComment,
|
||||
notify,
|
||||
updateItem,
|
||||
@@ -257,14 +275,7 @@ class Stream extends React.Component {
|
||||
content={asset.settings.infoBoxContent}
|
||||
enable={asset.settings.infoBoxEnable}
|
||||
/>
|
||||
{questionBoxEnable && (
|
||||
<QuestionBox
|
||||
content={asset.settings.questionBoxContent}
|
||||
icon={asset.settings.questionBoxIcon}
|
||||
>
|
||||
<Slot fill="streamQuestionArea" passthrough={slotPassthrough} />
|
||||
</QuestionBox>
|
||||
)}
|
||||
{this.renderQuestionBox()}
|
||||
{!banned &&
|
||||
temporarilySuspended && (
|
||||
<RestrictedMessageBox>
|
||||
@@ -298,10 +309,11 @@ class Stream extends React.Component {
|
||||
) : (
|
||||
<div>
|
||||
{asset.isClosed ? (
|
||||
<p>{asset.settings.closedMessage}</p>
|
||||
<Markdown content={asset.settings.closedMessage} />
|
||||
) : (
|
||||
<Markdown content={asset.settings.disableCommentingMessage} />
|
||||
)}
|
||||
{this.renderQuestionBox()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -215,7 +215,10 @@ const mapStateToProps = state => ({
|
||||
|
||||
const enhance = compose(
|
||||
withHooks(['preSubmit', 'postSubmit']),
|
||||
connect(mapStateToProps, null)
|
||||
connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)
|
||||
);
|
||||
|
||||
export default enhance(CommentBox);
|
||||
|
||||
@@ -33,4 +33,7 @@ const mapDispatchToProps = dispatch =>
|
||||
dispatch
|
||||
);
|
||||
|
||||
export default connect(null, mapDispatchToProps)(CommentNotFound);
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(CommentNotFound);
|
||||
|
||||
@@ -54,7 +54,11 @@ class StreamContainer extends React.Component {
|
||||
},
|
||||
updateQuery: (
|
||||
prev,
|
||||
{ subscriptionData: { data: { commentEdited } } }
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentEdited },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
// Ignore mutations from me.
|
||||
// TODO: need way to detect mutations created by this client, and allow mutations from other clients.
|
||||
@@ -87,7 +91,14 @@ class StreamContainer extends React.Component {
|
||||
variables: {
|
||||
assetId: this.props.asset.id,
|
||||
},
|
||||
updateQuery: (prev, { subscriptionData: { data: { commentAdded } } }) => {
|
||||
updateQuery: (
|
||||
prev,
|
||||
{
|
||||
subscriptionData: {
|
||||
data: { commentAdded },
|
||||
},
|
||||
}
|
||||
) => {
|
||||
// Ignore mutations from me.
|
||||
// TODO: need way to detect mutations created by this client, and allow mutations from other clients.
|
||||
if (
|
||||
@@ -488,7 +499,10 @@ const mapDispatchToProps = dispatch =>
|
||||
export default compose(
|
||||
withFragments(fragments),
|
||||
withEmit,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withPostComment,
|
||||
// `talk-plugin-flags` has a custom error handling logic.
|
||||
withPostFlag({ notifyOnError: false }),
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import queryString from 'query-string';
|
||||
/* global __webpack_public_path__ */ // eslint-disable-line no-unused-vars
|
||||
|
||||
import queryString from 'querystringify';
|
||||
import pym from 'pym.js';
|
||||
import EventEmitter from 'eventemitter2';
|
||||
import { buildUrl } from 'coral-framework/utils/url';
|
||||
import Snackbar from './Snackbar';
|
||||
import onIntersect from './onIntersect';
|
||||
import {
|
||||
createStorage,
|
||||
connectStorageToPym,
|
||||
@@ -10,13 +13,14 @@ import {
|
||||
|
||||
const NOTIFICATION_OFFSET = 200;
|
||||
|
||||
// Ensure there is a trailing slash.
|
||||
function ensureEndSlash(p) {
|
||||
return p.match(/\/$/) ? p : `${p}/`;
|
||||
}
|
||||
|
||||
// Build the URL to load in the pym iframe.
|
||||
function buildStreamIframeUrl(talkBaseUrl, query) {
|
||||
let url = [
|
||||
talkBaseUrl,
|
||||
talkBaseUrl.match(/\/$/) ? '' : '/', // make sure no double-'/' if opts.talk already ends with '/'
|
||||
'embed/stream?',
|
||||
].join('');
|
||||
let url = talkBaseUrl + 'embed/stream?';
|
||||
|
||||
url += queryString.stringify(query);
|
||||
|
||||
@@ -47,18 +51,76 @@ export default class Stream {
|
||||
events = null,
|
||||
snackBarStyles = null,
|
||||
onAuthChanged = null,
|
||||
talkStaticUrl = talkBaseUrl,
|
||||
...opts
|
||||
} = config;
|
||||
|
||||
this.onAuthChanged = onAuthChanged;
|
||||
this.el = el;
|
||||
this.talkBaseUrl = ensureEndSlash(talkBaseUrl);
|
||||
this.talkStaticUrl = ensureEndSlash(talkStaticUrl);
|
||||
this.opts = opts;
|
||||
|
||||
this.emitter = new EventEmitter({ wildcard: true });
|
||||
this.pym = new pym.Parent(el.id, buildStreamIframeUrl(talkBaseUrl, query), {
|
||||
title: opts.title,
|
||||
id: `${el.id}_iframe`,
|
||||
name: `${el.id}_iframe`,
|
||||
});
|
||||
this.snackBar = new Snackbar(snackBarStyles || {});
|
||||
this.emitter = new EventEmitter({ wildcard: true });
|
||||
|
||||
// Because we're loading chunks dynamically below, we need to point to the
|
||||
// static URL.
|
||||
//
|
||||
// The __webpack_public_path__ can be referenced:
|
||||
// https://webpack.js.org/configuration/output/#output-publicpath
|
||||
//
|
||||
__webpack_public_path__ = this.talkStaticUrl + 'static/';
|
||||
|
||||
// Attach to the events emitted by the pym parent.
|
||||
if (events) {
|
||||
events(this.emitter);
|
||||
}
|
||||
if (config.lazy || process.env.TALK_DEFAULT_LAZY_RENDER === 'TRUE') {
|
||||
const renderOnIntersect = () => onIntersect(this.el, () => this.render());
|
||||
if (!window.IntersectionObserver) {
|
||||
// Include a polyfill for the intersection observer.
|
||||
import('intersection-observer')
|
||||
.then(() => {
|
||||
// Polyfill applied.
|
||||
renderOnIntersect();
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
// Loading polyfill failed, just render it directly.
|
||||
this.render();
|
||||
});
|
||||
} else {
|
||||
// No need for polyfill.
|
||||
renderOnIntersect();
|
||||
}
|
||||
} else {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
assertRendered() {
|
||||
if (!this.pym) {
|
||||
throw new Error('Stream Embed must be rendered first');
|
||||
}
|
||||
}
|
||||
|
||||
isRendered() {
|
||||
return !!this.pym;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.pym) {
|
||||
throw new Error('Stream Embed already rendered');
|
||||
}
|
||||
this.pym = new pym.Parent(
|
||||
this.el.id,
|
||||
buildStreamIframeUrl(this.talkBaseUrl, this.query),
|
||||
{
|
||||
title: this.opts.title,
|
||||
id: `${this.el.id}_iframe`,
|
||||
name: `${this.el.id}_iframe`,
|
||||
}
|
||||
);
|
||||
|
||||
// Workaround: IOS Safari ignores `width` but respects `min-width` value.
|
||||
this.pym.el.firstChild.style.width = '1px';
|
||||
@@ -73,19 +135,14 @@ export default class Stream {
|
||||
}
|
||||
});
|
||||
|
||||
// Attach to the events emitted by the pym parent.
|
||||
if (events) {
|
||||
events(this.emitter);
|
||||
}
|
||||
|
||||
this.pym.onMessage('getConfig', () => {
|
||||
this.pym.sendMessage('config', JSON.stringify(opts));
|
||||
this.pym.sendMessage('config', JSON.stringify(this.opts));
|
||||
});
|
||||
|
||||
// If the auth changes, and someone is listening for it, then re-emit it.
|
||||
if (onAuthChanged) {
|
||||
if (this.onAuthChanged) {
|
||||
this.pym.onMessage('coral-auth-changed', message => {
|
||||
onAuthChanged(message ? JSON.parse(message) : null);
|
||||
this.onAuthChanged(message ? JSON.parse(message) : null);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -94,12 +151,13 @@ export default class Stream {
|
||||
|
||||
// Remove the permalink comment id from the hash.
|
||||
this.pym.onMessage('coral-view-all-comments', () => {
|
||||
const search = queryString.stringify({
|
||||
...queryString.parse(location.search),
|
||||
commentId: undefined,
|
||||
});
|
||||
const query = queryString.parse(location.search);
|
||||
|
||||
// Remove the commentId url param.
|
||||
delete query.commentId;
|
||||
|
||||
const search = queryString.stringify(query);
|
||||
|
||||
const url = buildUrl({ ...location, search });
|
||||
|
||||
// Change the url.
|
||||
@@ -162,22 +220,27 @@ export default class Stream {
|
||||
}
|
||||
|
||||
enablePluginsDebug() {
|
||||
this.assertRendered();
|
||||
this.pym.sendMessage('enablePluginsDebug');
|
||||
}
|
||||
|
||||
disablePluginsDebug() {
|
||||
this.assertRendered();
|
||||
this.pym.sendMessage('disablePluginsDebug');
|
||||
}
|
||||
|
||||
login(token) {
|
||||
this.assertRendered();
|
||||
this.pym.sendMessage('login', token);
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.assertRendered();
|
||||
this.pym.sendMessage('logout');
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.assertRendered();
|
||||
// Remove the event listeners.
|
||||
document.removeEventListener('click', this.handleClick.bind(this));
|
||||
this.emitter.removeAllListeners();
|
||||
@@ -190,6 +253,7 @@ export default class Stream {
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
this.assertRendered();
|
||||
this.pym.sendMessage('click');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ export class Talk {
|
||||
* @param {String} [config.asset_url] - Asset URL
|
||||
* @param {String} [config.asset_id] - Asset ID
|
||||
* @param {String} [config.auth_token] - (optional) A jwt representing the session
|
||||
* @param {String} [config.lazy] - (optional) If set the stream will only render lazily.
|
||||
* @return {Object}
|
||||
*
|
||||
* Example:
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
export default function onIntersect(el, callback) {
|
||||
if (!IntersectionObserver) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.warn('IntersectionObserver not available');
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
const options = {
|
||||
rootMargin: '100px',
|
||||
threshold: 1.0,
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
if (entries[0].isIntersecting) {
|
||||
observer.disconnect();
|
||||
callback();
|
||||
}
|
||||
}, options);
|
||||
observer.observe(el);
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
import * as actions from '../constants/auth';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
|
||||
function cleanAuthData(localStorage) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('exp');
|
||||
}
|
||||
import { setStorageAuthToken, clearStorageAuthToken } from '../services/auth';
|
||||
|
||||
export const checkLogin = () => (
|
||||
dispatch,
|
||||
@@ -15,21 +10,25 @@ export const checkLogin = () => (
|
||||
rest('/auth')
|
||||
.then(result => {
|
||||
if (!result.user) {
|
||||
cleanAuthData(localStorage);
|
||||
clearStorageAuthToken(localStorage);
|
||||
dispatch(checkLoginSuccess(null));
|
||||
client.resetWebsocket();
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the websocket.
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch(checkLoginSuccess(result.user));
|
||||
pym.sendMessage('coral-auth-changed', JSON.stringify(result.user));
|
||||
|
||||
// We don't need to reset the websocket here because if the request
|
||||
// returned that there was a user (which is the case here), then the
|
||||
// original request has already succeeded, or a previous call to a token
|
||||
// set handler has already reset it.
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.status && error.status === 401 && localStorage) {
|
||||
// Unauthorized.
|
||||
cleanAuthData(localStorage);
|
||||
clearStorageAuthToken(localStorage);
|
||||
client.resetWebsocket();
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
@@ -49,14 +48,20 @@ const checkLoginSuccess = user => ({
|
||||
user,
|
||||
});
|
||||
|
||||
export const setAuthToken = token => (dispatch, _, { localStorage }) => {
|
||||
localStorage.setItem('exp', jwtDecode(token).exp);
|
||||
localStorage.setItem('token', token);
|
||||
export const setAuthToken = token => (
|
||||
dispatch,
|
||||
_,
|
||||
{ localStorage, client }
|
||||
) => {
|
||||
setStorageAuthToken(localStorage, token);
|
||||
|
||||
// Dispatch the set auth token action. For some browsers and situations, we
|
||||
// may not be able to persist the auth token any other way. Keep it in redux!
|
||||
dispatch({ type: actions.SET_AUTH_TOKEN, token });
|
||||
|
||||
// Now that we set a token, let's reset the subscriptions.
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch(checkLogin());
|
||||
};
|
||||
|
||||
@@ -65,9 +70,7 @@ export const handleSuccessfulLogin = (user, token) => (
|
||||
_,
|
||||
{ client, localStorage, postMessage }
|
||||
) => {
|
||||
const { exp } = jwtDecode(token);
|
||||
localStorage.setItem('exp', exp);
|
||||
localStorage.setItem('token', token);
|
||||
setStorageAuthToken(localStorage, token);
|
||||
|
||||
// Send the message via the messages service to the window.opener if it
|
||||
// exists.
|
||||
@@ -79,6 +82,7 @@ export const handleSuccessfulLogin = (user, token) => (
|
||||
);
|
||||
}
|
||||
|
||||
// Now that we just set a token, set the token!
|
||||
client.resetWebsocket();
|
||||
|
||||
dispatch({
|
||||
@@ -96,10 +100,16 @@ export const logout = () => async (
|
||||
_,
|
||||
{ rest, client, pym, localStorage }
|
||||
) => {
|
||||
await rest('/auth', { method: 'DELETE' });
|
||||
try {
|
||||
await rest('/auth', { method: 'DELETE' });
|
||||
} catch (err) {
|
||||
// We ignore any REST related errors from the delete action, which may/may
|
||||
// not have had a cookie/token attached to it. The logout action was still
|
||||
// called, so we still want to cleanup.
|
||||
}
|
||||
|
||||
// Clear the auth data persisted to localStorage.
|
||||
cleanAuthData(localStorage);
|
||||
clearStorageAuthToken(localStorage);
|
||||
|
||||
// Reset the websocket.
|
||||
client.resetWebsocket();
|
||||
|
||||
@@ -78,7 +78,12 @@ function markLinks(body, keyPrefix) {
|
||||
matches.forEach((match, i) => {
|
||||
content.push(body.substring(index, match.index));
|
||||
content.push(
|
||||
<a key={`${keyPrefix}_${i}`} href={match.url} target="_blank">
|
||||
<a
|
||||
key={`${keyPrefix}_${i}`}
|
||||
href={match.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{match.text}
|
||||
</a>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { timeago } from 'coral-framework/services/i18n';
|
||||
import cn from 'classnames';
|
||||
import styles from './CommentTimestamp.css';
|
||||
import TimeAgo from 'coral-framework/components/TimeAgo';
|
||||
|
||||
const CommentTimestamp = ({ className, created_at }) => (
|
||||
<div className={cn(className, styles.timestamp, 'talk-comment-timestamp')}>
|
||||
{timeago(created_at)}
|
||||
</div>
|
||||
<TimeAgo
|
||||
className={cn(className, styles.timestamp, 'talk-comment-timestamp')}
|
||||
datetime={created_at}
|
||||
/>
|
||||
);
|
||||
|
||||
CommentTimestamp.propTypes = {
|
||||
|
||||
@@ -1,50 +1,48 @@
|
||||
.card {
|
||||
margin-bottom: 20px;
|
||||
align-items: flex-start;
|
||||
min-height: 100px;
|
||||
max-width: 600px;
|
||||
min-height: 100px;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.collapsibleCard {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.enabledCard {
|
||||
border-left: 7px solid #00796b;
|
||||
}
|
||||
|
||||
.action {
|
||||
flex-shrink: 0;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-top: 3px;
|
||||
margin-bottom: 7px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
.body {
|
||||
margin-top: 7px;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.action {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: inline-block;
|
||||
padding: 0px 30px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.enabledSetting {
|
||||
border-left-color: #00796b;
|
||||
border-left-style: solid;
|
||||
border-left-width: 7px;
|
||||
}
|
||||
|
||||
.disabledSetting {
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.disabledSettingText {
|
||||
.disabledBody {
|
||||
color: #ccc;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,46 +1,69 @@
|
||||
import React from 'react';
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './ConfigureCard.css';
|
||||
import classnames from 'classnames/bind';
|
||||
import { Card } from 'coral-ui';
|
||||
import { Checkbox } from 'react-mdl';
|
||||
import cn from 'classnames';
|
||||
import { Checkbox, IconButton } from 'react-mdl';
|
||||
|
||||
const ConfigureCard = ({
|
||||
title,
|
||||
children,
|
||||
className,
|
||||
onCheckbox,
|
||||
checked,
|
||||
...rest
|
||||
}) => (
|
||||
<Card
|
||||
{...rest}
|
||||
className={cn(styles.card, className, {
|
||||
[styles.enabledSetting]: checked === true,
|
||||
[styles.disabledSetting]: checked === false,
|
||||
})}
|
||||
>
|
||||
{checked !== undefined && (
|
||||
<div className={styles.action}>
|
||||
<Checkbox onChange={onCheckbox} checked={checked} />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(styles.wrapper, {
|
||||
[styles.content]: checked !== undefined,
|
||||
})}
|
||||
>
|
||||
<div className={styles.header}>{title}</div>
|
||||
<div
|
||||
className={cn({
|
||||
[styles.disabledSettingText]: checked === false,
|
||||
import styles from './ConfigureCard.css';
|
||||
|
||||
const cn = classnames.bind(styles);
|
||||
|
||||
class ConfigureCard extends PureComponent {
|
||||
state = {
|
||||
isOpen: !this.props.collapsible,
|
||||
};
|
||||
|
||||
toggle = () => this.setState({ isOpen: !this.state.isOpen });
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
children,
|
||||
className,
|
||||
onCheckbox,
|
||||
checked,
|
||||
collapsible,
|
||||
...rest
|
||||
} = this.props;
|
||||
|
||||
const { isOpen } = this.state;
|
||||
|
||||
const iconName = isOpen ? 'keyboard_arrow_up' : 'keyboard_arrow_down';
|
||||
|
||||
return (
|
||||
<Card
|
||||
{...rest}
|
||||
className={cn(styles.card, className, {
|
||||
enabledCard: checked === true,
|
||||
collapsibleCard: collapsible,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
{checked !== undefined && (
|
||||
<div className={styles.action}>
|
||||
<Checkbox onChange={onCheckbox} checked={checked} />
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.title}>{title}</div>
|
||||
{collapsible && (
|
||||
<IconButton onClick={this.toggle} name={iconName} ripple />
|
||||
)}
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div
|
||||
className={cn(styles.body, {
|
||||
disabledBody: checked === false,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ConfigureCard.propTypes = {
|
||||
title: PropTypes.string,
|
||||
@@ -48,6 +71,7 @@ ConfigureCard.propTypes = {
|
||||
onCheckbox: PropTypes.func,
|
||||
checked: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
collapsible: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ConfigureCard;
|
||||
|
||||
@@ -103,5 +103,8 @@ export default compose(
|
||||
size: props => props.size,
|
||||
defaultComponent: props => props.defaultComponent,
|
||||
}),
|
||||
connect(mapStateToProps, null)
|
||||
connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
)
|
||||
)(Slot);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.timeago {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { timeago } from 'coral-framework/services/i18n';
|
||||
import cn from 'classnames';
|
||||
import styles from './TimeAgo.css';
|
||||
|
||||
class TimeAgo extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { timeago: true };
|
||||
}
|
||||
toggleDate = () => this.setState({ timeago: !this.state.timeago });
|
||||
render() {
|
||||
var displayTime = this.state.timeago
|
||||
? timeago(this.props.datetime)
|
||||
: new Date(this.props.datetime).toLocaleString();
|
||||
|
||||
var titleDate = !this.state.timeago
|
||||
? timeago(this.props.datetime)
|
||||
: new Date(this.props.datetime).toLocaleString();
|
||||
return (
|
||||
<span
|
||||
onClick={this.toggleDate}
|
||||
className={cn(
|
||||
this.props.className,
|
||||
styles.timeago,
|
||||
'talk-comment-timeago'
|
||||
)}
|
||||
title={titleDate}
|
||||
>
|
||||
{displayTime}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TimeAgo.propTypes = {
|
||||
datetime: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export default TimeAgo;
|
||||
@@ -1,7 +1,7 @@
|
||||
import url from 'url';
|
||||
import parse from 'url-parse';
|
||||
|
||||
const base = document.querySelector('base');
|
||||
const baseUrl = (base && url.parse(base.href)) || {};
|
||||
const baseUrl = (base && parse(base.href)) || {};
|
||||
|
||||
export const BASE_URL = base.href;
|
||||
export const BASE_PATH = baseUrl.pathname;
|
||||
|
||||
@@ -242,6 +242,18 @@ export const withUnsuspendUser = withMutation(
|
||||
}
|
||||
);
|
||||
|
||||
const SetUsernameStatusFragment = gql`
|
||||
fragment Talk_SetUsernameStatus on User {
|
||||
state {
|
||||
status {
|
||||
username {
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const withApproveUsername = withMutation(
|
||||
gql`
|
||||
mutation ApproveUsername($id: ID!) {
|
||||
@@ -257,6 +269,27 @@ export const withApproveUsername = withMutation(
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
update: proxy => {
|
||||
const fragmentId = `User_${id}`;
|
||||
const data = {
|
||||
__typename: 'User',
|
||||
state: {
|
||||
__typename: 'UserState',
|
||||
status: {
|
||||
__typename: 'UserStatus',
|
||||
username: {
|
||||
__typename: 'UsernameStatus',
|
||||
status: 'APPROVED',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
proxy.writeFragment({
|
||||
fragment: SetUsernameStatusFragment,
|
||||
id: fragmentId,
|
||||
data,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
@@ -278,6 +311,27 @@ export const withRejectUsername = withMutation(
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
update: proxy => {
|
||||
const fragmentId = `User_${id}`;
|
||||
const data = {
|
||||
__typename: 'User',
|
||||
state: {
|
||||
__typename: 'UserState',
|
||||
status: {
|
||||
__typename: 'UserStatus',
|
||||
username: {
|
||||
__typename: 'UsernameStatus',
|
||||
status: 'REJECTED',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
proxy.writeFragment({
|
||||
fragment: SetUsernameStatusFragment,
|
||||
id: fragmentId,
|
||||
data,
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const DECIMAL_AMOUNT = 1;
|
||||
|
||||
export function humanizeNumber(number) {
|
||||
const abs = Math.abs(number);
|
||||
|
||||
if (abs >= 1e9) {
|
||||
number = `${(number / 1e9).toFixed(DECIMAL_AMOUNT)}${t(
|
||||
'modqueue.billion'
|
||||
)}`;
|
||||
} else if (abs >= 1e6) {
|
||||
number = `${(number / 1e6).toFixed(DECIMAL_AMOUNT)}${t(
|
||||
'modqueue.million'
|
||||
)}`;
|
||||
} else if (abs >= 1e3) {
|
||||
number = `${(number / 1e3).toFixed(DECIMAL_AMOUNT)}${t(
|
||||
'modqueue.thousand'
|
||||
)}`;
|
||||
}
|
||||
return number;
|
||||
}
|
||||
@@ -2,5 +2,8 @@ import { connect } from 'react-redux';
|
||||
|
||||
export default (mapStateToProps, ...rest) => BaseComponent => {
|
||||
BaseComponent.mapStateToProps = mapStateToProps;
|
||||
return connect(mapStateToProps, ...rest)(BaseComponent);
|
||||
return connect(
|
||||
mapStateToProps,
|
||||
...rest
|
||||
)(BaseComponent);
|
||||
};
|
||||
|
||||
@@ -95,7 +95,10 @@ const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators({ updateUsername, updateStatus }, dispatch);
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
),
|
||||
withSetUsernameMutation,
|
||||
withSetUsername
|
||||
);
|
||||
|
||||
@@ -121,4 +121,7 @@ const withSignUp = hoistStatics(WrappedComponent => {
|
||||
return WithSignUp;
|
||||
});
|
||||
|
||||
export default compose(withSettingsQuery, withSignUp);
|
||||
export default compose(
|
||||
withSettingsQuery,
|
||||
withSignUp
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user