mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
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,6 +6,7 @@ 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,6 +16,14 @@ Learn more about Talk, including a deep dive into features for commenters and mo
|
||||
|
||||
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/).
|
||||
|
||||
## 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 whih 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
|
||||
|
||||
- [Talk Product Roadmap](https://www.pivotaltracker.com/n/projects/1863625)
|
||||
|
||||
@@ -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"
|
||||
|
||||
+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>;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -176,7 +176,7 @@ class People extends React.Component {
|
||||
})
|
||||
}
|
||||
>
|
||||
Suspend User
|
||||
{t('modqueue.suspend')}
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
|
||||
@@ -195,7 +195,7 @@ class People extends React.Component {
|
||||
})
|
||||
}
|
||||
>
|
||||
Ban User
|
||||
{t('modqueue.ban_user_actions')}
|
||||
</ActionsMenuItem>
|
||||
)}
|
||||
</ActionsMenu>
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -200,7 +200,10 @@ const SEARCH_QUERY = gql`
|
||||
`;
|
||||
|
||||
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 });
|
||||
};
|
||||
|
||||
@@ -178,7 +178,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}
|
||||
|
||||
@@ -10,7 +10,9 @@ const Wordlist = ({ suspectWords, bannedWords, onChangeWordlist }) => (
|
||||
<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>
|
||||
@@ -18,7 +20,9 @@ const Wordlist = ({ suspectWords, bannedWords, onChangeWordlist }) => (
|
||||
<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,6 +61,7 @@
|
||||
line-height: 1.5;
|
||||
font-weight: 300;
|
||||
margin-bottom: 8px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.body {
|
||||
@@ -188,4 +189,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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -113,4 +113,7 @@ StoriesContainer.propTypes = {
|
||||
updateAssetState: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(StoriesContainer);
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(StoriesContainer);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
@@ -211,7 +211,10 @@ class Stream extends React.Component {
|
||||
root,
|
||||
appendItemArray,
|
||||
asset,
|
||||
asset: { comment: highlightedComment, settings: { questionBoxEnable } },
|
||||
asset: {
|
||||
comment: highlightedComment,
|
||||
settings: { questionBoxEnable },
|
||||
},
|
||||
postComment,
|
||||
notify,
|
||||
updateItem,
|
||||
@@ -298,7 +301,7 @@ class Stream extends React.Component {
|
||||
) : (
|
||||
<div>
|
||||
{asset.isClosed ? (
|
||||
<p>{asset.settings.closedMessage}</p>
|
||||
<Markdown content={asset.settings.closedMessage} />
|
||||
) : (
|
||||
<Markdown content={asset.settings.disableCommentingMessage} />
|
||||
)}
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
import queryString from 'query-string';
|
||||
import queryString from 'querystringify';
|
||||
import pym from 'pym.js';
|
||||
import EventEmitter from 'eventemitter2';
|
||||
import { buildUrl } from 'coral-framework/utils/url';
|
||||
@@ -94,12 +94,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.
|
||||
|
||||
@@ -17,19 +17,19 @@ export const checkLogin = () => (
|
||||
if (!result.user) {
|
||||
cleanAuthData(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));
|
||||
client.resetWebsocket();
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.status && error.status === 401 && localStorage) {
|
||||
// Unauthorized.
|
||||
cleanAuthData(localStorage);
|
||||
client.resetWebsocket();
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
@@ -96,7 +96,13 @@ 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);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -198,5 +198,11 @@ const mapStateToProps = state => ({
|
||||
* })(MyComponent);
|
||||
*/
|
||||
export default settings => {
|
||||
return compose(connect(mapStateToProps, null), createHOC(settings));
|
||||
return compose(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
null
|
||||
),
|
||||
createHOC(settings)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
/**
|
||||
* createNotificationService returns a notification services based on pym.
|
||||
* @param {func} condition callback that checks that the given argument is valid.
|
||||
* @param {error} string error that is displayed when validation fails.
|
||||
* @return {func} validator function
|
||||
*/
|
||||
export function createValidator(condition, error) {
|
||||
return (v, values) => (condition(v, values) ? undefined : error);
|
||||
}
|
||||
|
||||
/**
|
||||
* composeValidators chains validators and runs them in sequence until
|
||||
* one validator fails and returns an error message.
|
||||
* @param {func[]} validators array of validator functions.
|
||||
* @return {func} validator fuction
|
||||
*/
|
||||
export function composeValidators(...validators) {
|
||||
return value =>
|
||||
validators.reduce(
|
||||
(error, validator) => error || validator(value),
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* required checks that the value is truthy.
|
||||
* @return {func} validator fuction
|
||||
*/
|
||||
export const required = createValidator(v => !!v, t('validators.required'));
|
||||
|
||||
/**
|
||||
* Confirm will check that this field has the same value as another.
|
||||
* @param {string} key key of other form field to compare with.
|
||||
* @param {error} string error that is displayed when validation fails.
|
||||
* @return {func} validator fuction
|
||||
*/
|
||||
export const confirm = (key, error) =>
|
||||
createValidator((v, data) => v === data[key], error);
|
||||
|
||||
/**
|
||||
* Verify email.
|
||||
* @return {func} validator fuction
|
||||
*/
|
||||
export const verifyEmail = createValidator(
|
||||
email => /^.+@.+\..+$/.test(email),
|
||||
t('validators.verify_email')
|
||||
);
|
||||
|
||||
/**
|
||||
* Confirm email.
|
||||
* @param {string} key key of other form field to compare with.
|
||||
* @return {func} validator fuction
|
||||
*/
|
||||
export const confirmEmail = key => confirm(key, t('validators.confirm_email'));
|
||||
|
||||
/**
|
||||
* Verify password.
|
||||
* @return {func} validator fuction
|
||||
*/
|
||||
export const verifyPassword = createValidator(
|
||||
pass => /^(?=.{8,}).*$/.test(pass),
|
||||
t('validators.verify_password')
|
||||
);
|
||||
|
||||
/**
|
||||
* Confirm password.
|
||||
* @param {string} key key of other form field to compare with.
|
||||
* @return {func} validator fuction
|
||||
*/
|
||||
export const confirmPassword = key =>
|
||||
confirm(key, t('validators.confirm_password'));
|
||||
|
||||
/**
|
||||
* Verify username.
|
||||
* @return {func} validator fuction
|
||||
*/
|
||||
export const verifyUsername = createValidator(
|
||||
username => /^[a-zA-Z0-9_]+$/.test(username),
|
||||
t('validators.verify_username')
|
||||
);
|
||||
|
||||
/**
|
||||
* Verify organization name.
|
||||
* @return {func} validator fuction
|
||||
*/
|
||||
export const verifyOrganizationName = createValidator(
|
||||
org => /^[a-zA-Z0-9_ ]+$/.test(org),
|
||||
t('validators.verify_organization_name')
|
||||
);
|
||||
@@ -3,9 +3,11 @@ import {
|
||||
ENABLE_PLUGINS_DEBUG,
|
||||
DISABLE_PLUGINS_DEBUG,
|
||||
} from '../constants/config';
|
||||
import { LOGOUT } from '../constants/auth';
|
||||
import { LOGOUT, SET_AUTH_TOKEN } from '../constants/auth';
|
||||
|
||||
const initialState = {};
|
||||
const initialState = {
|
||||
auth_token: null,
|
||||
};
|
||||
|
||||
export default function config(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
@@ -30,6 +32,11 @@ export default function config(state = initialState, action) {
|
||||
...state,
|
||||
auth_token: null,
|
||||
};
|
||||
case SET_AUTH_TOKEN:
|
||||
return {
|
||||
...state,
|
||||
auth_token: action.token || null,
|
||||
};
|
||||
case MERGE_CONFIG:
|
||||
return {
|
||||
...state,
|
||||
|
||||
+4
-8
@@ -42,17 +42,13 @@ const getAuthToken = (store, storage) => {
|
||||
let state = store.getState();
|
||||
|
||||
if (state.config && state.config.auth_token) {
|
||||
// if an auth_token exists in config, use it.
|
||||
return state.config.auth_token;
|
||||
// If the embed is called with `embed.login(token)`, and the browser is not
|
||||
// capable of storing the token in localStorage, then we would have
|
||||
// persisted it to the redux state.
|
||||
return state.config.auth_token || state.auth.token;
|
||||
} else if (!bowser.safari && !bowser.ios && storage) {
|
||||
// Use local storage auth tokens where there's a stable api.
|
||||
return storage.getItem('token');
|
||||
} else if (state.auth && state.auth.token) {
|
||||
// Use the redux token state if the remaining methods fall out. If the embed
|
||||
// is called with `embed.login(token)`, and the browser is not capable of
|
||||
// storing the token in localStorage, then we would have persisted it to the
|
||||
// redux state.
|
||||
return state.auth.token;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -9,7 +9,11 @@ export function createReduxEmitter(eventEmitter) {
|
||||
// Handle apollo actions.
|
||||
if (action.type.startsWith('APOLLO_')) {
|
||||
if (action.type === 'APOLLO_SUBSCRIPTION_RESULT') {
|
||||
const { operationName, variables, result: { data } } = action;
|
||||
const {
|
||||
operationName,
|
||||
variables,
|
||||
result: { data },
|
||||
} = action;
|
||||
eventEmitter.emit(`subscription.${operationName}.data`, {
|
||||
variables,
|
||||
data,
|
||||
|
||||
@@ -75,10 +75,15 @@ let TIMEAGO_INSTANCE;
|
||||
// detectLanguage will try to get the locale from storage if available,
|
||||
// otherwise will try to get it from the navigator, otherwise, it will fallback
|
||||
// to the default language.
|
||||
const detectLanguage = () =>
|
||||
first(
|
||||
const detectLanguage = () => {
|
||||
var browserLanguages = navigator.languages;
|
||||
//IE11 and MS-EDGE do not provide navigator.languages
|
||||
if (!browserLanguages) {
|
||||
browserLanguages = [navigator.language];
|
||||
}
|
||||
return first(
|
||||
negotiateLanguages(
|
||||
navigator.languages,
|
||||
browserLanguages,
|
||||
whitelistedLanguages || supportedLocales,
|
||||
{
|
||||
defaultLocale,
|
||||
@@ -86,6 +91,7 @@ const detectLanguage = () =>
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export function setupTranslations() {
|
||||
// locale
|
||||
@@ -100,10 +106,10 @@ export function setupTranslations() {
|
||||
ta.register('da', daTA);
|
||||
ta.register('de', deTA);
|
||||
ta.register('fr', frTA);
|
||||
ta.register('nl-NL', nlTA);
|
||||
ta.register('pt-BR', pt_BRTA);
|
||||
ta.register('zh-CN', zh_CNTA);
|
||||
ta.register('zh-TW', zh_TWTA);
|
||||
ta.register('nl_NL', nlTA);
|
||||
ta.register('pt_BR', pt_BRTA);
|
||||
ta.register('zh_CN', zh_CNTA);
|
||||
ta.register('zh_TW', zh_TWTA);
|
||||
TIMEAGO_INSTANCE = ta();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { capitalize } from 'coral-framework/helpers/strings';
|
||||
import assignWith from 'lodash/assignWith';
|
||||
import mapValues from 'lodash/mapValues';
|
||||
export * from 'coral-framework/helpers/strings';
|
||||
export * from './url';
|
||||
|
||||
export const getTotalActionCount = (type, comment) => {
|
||||
return comment.action_summaries
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import parse from 'url-parse';
|
||||
|
||||
export function buildUrl(
|
||||
{ protocol, hostname, port, pathname, search, hash } = window.location
|
||||
) {
|
||||
@@ -10,3 +12,10 @@ export function buildUrl(
|
||||
port ? `:${port}` : ''
|
||||
}${pathname}${search}${hash}`;
|
||||
}
|
||||
|
||||
export function buildCommentURL(assetURL, commentID) {
|
||||
const u = parse(assetURL, true);
|
||||
u.query.commentId = commentID;
|
||||
u.set('query', u.query);
|
||||
return u.href;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,24 @@ export const isBanned = user => {
|
||||
return get(user, 'state.status.banned.status');
|
||||
};
|
||||
|
||||
/**
|
||||
* isUsernameRejected
|
||||
* retrieves boolean based on the username status
|
||||
*/
|
||||
|
||||
export const isUsernameRejected = user => {
|
||||
return get(user, 'state.status.username.status') === 'REJECTED';
|
||||
};
|
||||
|
||||
/**
|
||||
* isUsernameChanged
|
||||
* retrieves boolean based on the username status
|
||||
*/
|
||||
|
||||
export const isUsernameChanged = user => {
|
||||
return get(user, 'state.status.username.status') === 'CHANGED';
|
||||
};
|
||||
|
||||
/**
|
||||
* canUsernameBeUpdated
|
||||
* retrieves boolean whether a username can be updated or not
|
||||
|
||||
@@ -64,7 +64,9 @@ const CONFIG = {
|
||||
process.env.TALK_LOGGING_LEVEL
|
||||
)
|
||||
? process.env.TALK_LOGGING_LEVEL
|
||||
: process.env.NODE_ENV === 'test' ? 'fatal' : 'info',
|
||||
: process.env.NODE_ENV === 'test'
|
||||
? 'fatal'
|
||||
: 'info',
|
||||
|
||||
// REVISION_HASH when using the docker build will contain the build hash that
|
||||
// it was built at.
|
||||
@@ -204,6 +206,9 @@ const CONFIG = {
|
||||
// STATIC_URI is the base uri where static files are hosted.
|
||||
STATIC_URI: process.env.TALK_STATIC_URI || process.env.TALK_ROOT_URL,
|
||||
|
||||
// SCRAPER_PROXY_URL is the url to be used as a proxy by the scraper
|
||||
SCRAPER_PROXY_URL: process.env.TALK_SCRAPER_PROXY_URL || null,
|
||||
|
||||
// The keepalive timeout (in ms) that should be used to send keep alive
|
||||
// messages through the websocket to keep the socket alive.
|
||||
KEEP_ALIVE: process.env.TALK_KEEP_ALIVE || '30s',
|
||||
|
||||
+11
-1
@@ -90,6 +90,8 @@ sidebar:
|
||||
url: /installation-from-source/
|
||||
- title: Planning your Talk Architecture
|
||||
url: /planning-architecture/
|
||||
- title: Pre-Launch Checklist
|
||||
url: /pre-launch-checklist/
|
||||
- title: Configuration
|
||||
children:
|
||||
- title: Required Configuration
|
||||
@@ -100,7 +102,7 @@ sidebar:
|
||||
children:
|
||||
- title: Authentication
|
||||
url: /integrating/authentication/
|
||||
- title: Asset Managment
|
||||
- title: Asset Management
|
||||
url: /integrating/asset-management/
|
||||
- title: Configuring the Comment Stream
|
||||
url: /integrating/configuring-comment-stream/
|
||||
@@ -162,10 +164,18 @@ sidebar:
|
||||
url: /when-youve-installed-talk/
|
||||
- title: Migrating
|
||||
children:
|
||||
- title: Migrating from v3.x.x
|
||||
url: /migration/3/
|
||||
- title: Migrating to v4.0.0
|
||||
url: /migration/4/
|
||||
- title: Migrating to v4.1.0
|
||||
url: /migration/4.1/
|
||||
- title: FAQ & Troubleshooting
|
||||
children:
|
||||
- title: FAQ
|
||||
url: /faq/
|
||||
- title: Troubleshooting Tips
|
||||
url: /troubleshooting-tips/
|
||||
- title: Contact
|
||||
url: /contact/
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user