mirror of
https://github.com/wassname/talk.git
synced 2026-09-13 13:10:43 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d986e7a0b | ||
|
|
79d312df50 | ||
|
|
eb969f8050 | ||
|
|
dde1f7b5be | ||
|
|
343d4418b0 | ||
|
|
4c28e43e62 | ||
|
|
1030ca1fa2 | ||
|
|
b1bc97c8d5 | ||
|
|
18b802c555 | ||
|
|
1c16a75f6b | ||
|
|
8724cfe325 | ||
|
|
d3d7f909d8 | ||
|
|
c2f22bbf98 | ||
|
|
9b39017f25 | ||
|
|
075d62a32a | ||
|
|
2d92284622 | ||
|
|
e99b79687f | ||
|
|
9182d5b09c | ||
|
|
89b87b01df | ||
|
|
257f361998 | ||
|
|
02dff0c726 | ||
|
|
17f5800363 | ||
|
|
7aaad6ccd9 | ||
|
|
43cb0efe7e | ||
|
|
06ced883ba | ||
|
|
f7e6f67f4e | ||
|
|
307fe58cf3 | ||
|
|
48f2cce893 | ||
|
|
b597f730f6 | ||
|
|
d396f43fe3 | ||
|
|
799a21fecd | ||
|
|
7d41421526 | ||
|
|
9bd55b267e | ||
|
|
90470b8331 | ||
|
|
df737ad201 | ||
|
|
09ee854302 | ||
|
|
f88bd8b485 | ||
|
|
36ba7367d1 | ||
|
|
f9693c7154 | ||
|
|
445107ef64 | ||
|
|
1c3be913d3 | ||
|
|
09f9782505 | ||
|
|
1d6e9527e5 | ||
|
|
d0ce832319 | ||
|
|
2e7ee6bb8b | ||
|
|
ad3958a2e0 | ||
|
|
58acdef043 | ||
|
|
99aad0f74b | ||
|
|
5d718f6dd4 | ||
|
|
44ce71ef14 | ||
|
|
47cde9a842 | ||
|
|
219399a231 | ||
|
|
95114bc847 | ||
|
|
12047d1ae1 | ||
|
|
0362dad14b | ||
|
|
97b81cc1cc | ||
|
|
8d6b9dd23c | ||
|
|
9adf11e1d1 | ||
|
|
ef49d9a3d2 | ||
|
|
921eea59c8 | ||
|
|
c5cd88f4c0 | ||
|
|
762f4f7898 | ||
|
|
e400ae37f4 | ||
|
|
45b0ec48de | ||
|
|
f3f8c6b24e | ||
|
|
baa5bddd19 | ||
|
|
bea7d75db7 | ||
|
|
8e5edd65c7 | ||
|
|
9ae59ef998 | ||
|
|
0f382bd742 | ||
|
|
55b1f4831a | ||
|
|
3d2575413d | ||
|
|
c40393347a | ||
|
|
8c05e3df87 | ||
|
|
fa4adda61e | ||
|
|
ea06b334f6 | ||
|
|
203d3c7ee9 | ||
|
|
825e1b5e5c | ||
|
|
3db406bf26 | ||
|
|
c5e078f1e1 | ||
|
|
e6f18d8558 | ||
|
|
76f259a2aa | ||
|
|
3f90f72bee | ||
|
|
f211bdf34e | ||
|
|
d2e5633d91 | ||
|
|
b01e919029 | ||
|
|
62b407179f | ||
|
|
536ef70415 | ||
|
|
981c319810 | ||
|
|
19e23479ee | ||
|
|
86687f10d8 | ||
|
|
3b6bcba055 | ||
|
|
acdfaeee9f | ||
|
|
792d0b9a2d | ||
|
|
9be2379f30 | ||
|
|
1658b2594b | ||
|
|
d9e2fba569 | ||
|
|
d9cabeafe2 | ||
|
|
171a6d506b | ||
|
|
a19de7bfb4 | ||
|
|
6138768588 | ||
|
|
4c5f8b3359 | ||
|
|
605c87a799 | ||
|
|
1e0451ceba | ||
|
|
d60b869937 | ||
|
|
9f4c97e138 | ||
|
|
66518f50dd | ||
|
|
7328926ad7 | ||
|
|
a736c1620e | ||
|
|
d359d915fe | ||
|
|
235b2e1068 | ||
|
|
f183aeb748 | ||
|
|
143adc7c6f | ||
|
|
91714705ea | ||
|
|
64cc85eb24 | ||
|
|
7a043dac5b | ||
|
|
edd3e7f6fe | ||
|
|
3ef9bd5e52 | ||
|
|
b60196e455 | ||
|
|
d315458e0e | ||
|
|
271f0cc237 | ||
|
|
864aa2be28 | ||
|
|
7e536cfb31 | ||
|
|
5fa4b4d734 | ||
|
|
e7c78a2465 | ||
|
|
95fadbd05a | ||
|
|
6d04b4add3 |
@@ -26,5 +26,7 @@ plugins/*
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-deep-reply-count
|
||||
!plugins/talk-plugin-subscriber
|
||||
!plugins/talk-plugin-flag-details
|
||||
|
||||
node_modules
|
||||
|
||||
+1
-73
@@ -1,75 +1,3 @@
|
||||
{
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2017
|
||||
},
|
||||
"plugins": [
|
||||
"json",
|
||||
"promise",
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"promise/no-return-wrap": "error",
|
||||
"promise/param-names": "error",
|
||||
"promise/catch-or-return": "warn",
|
||||
"promise/no-native": "off",
|
||||
"promise/no-nesting": "warn",
|
||||
"promise/no-promise-in-callback": "warn",
|
||||
"promise/no-callback-in-promise": "warn",
|
||||
|
||||
"react/display-name": "off",
|
||||
|
||||
// We'll keeping using this, as we'll switch over to Preact and we don't
|
||||
// need to worry about deprecation.
|
||||
"react/no-find-dom-node": "off",
|
||||
|
||||
// TODO: Use default after fixing the errors.
|
||||
"react/prop-types": "warn",
|
||||
|
||||
"array-callback-return": "warn",
|
||||
"arrow-parens": ["warn", "always"],
|
||||
"comma-spacing": ["error", {"after": true}],
|
||||
"curly": "error",
|
||||
"eol-last": "warn",
|
||||
"eqeqeq": ["error", "smart"],
|
||||
"indent": ["error", 2],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"lines-around-comment": ["warn", {"beforeLineComment": true}],
|
||||
"newline-per-chained-call": ["error", {"ignoreChainWithDepth": 2}],
|
||||
"no-console": "off",
|
||||
"no-const-assign": "error",
|
||||
"no-duplicate-imports": "error",
|
||||
"no-eval": "error",
|
||||
"no-global-assign": "error",
|
||||
"no-implied-eval": "error",
|
||||
"no-lonely-if": "error",
|
||||
"no-multiple-empty-lines": ["error", {"max": 1}],
|
||||
"no-nested-ternary": "warn",
|
||||
"no-path-concat": "error",
|
||||
"no-script-url": "error",
|
||||
"no-tabs": "error",
|
||||
"no-template-curly-in-string": "warn",
|
||||
"no-throw-literal": "error",
|
||||
"no-unneeded-ternary": "warn",
|
||||
"no-unsafe-negation": "warn",
|
||||
"no-unused-vars": ["error", {"argsIgnorePattern": "^_|next", "varsIgnorePattern": "^_"}],
|
||||
"no-var": "error",
|
||||
"object-curly-spacing": "warn",
|
||||
"prefer-template": "warn",
|
||||
"quotes": ["error", "single"],
|
||||
"semi": ["error", "always"],
|
||||
"space-in-parens": ["error", "never"],
|
||||
"space-infix-ops": ["error"],
|
||||
"space-unary-ops": ["error", {"words": true, "nonwords": false}],
|
||||
"spaced-comment": ["warn", "always", {"line": {"exceptions": ["-", "="]}}],
|
||||
"template-curly-spacing": "warn",
|
||||
"yoda": "warn"
|
||||
}
|
||||
"extends": "@coralproject/eslint-config-talk"
|
||||
}
|
||||
|
||||
@@ -43,5 +43,7 @@ plugins/*
|
||||
!plugins/talk-plugin-toxic-comments
|
||||
!plugins/talk-plugin-remember-sort
|
||||
!plugins/talk-plugin-deep-reply-count
|
||||
!plugins/talk-plugin-subscriber
|
||||
!plugins/talk-plugin-flag-details
|
||||
|
||||
**/node_modules/*
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
---
|
||||
title: Contributor Covenant Code of Conduct
|
||||
permalink: /docs/development/code-of-conduct/
|
||||
---
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
@@ -46,7 +43,7 @@ Other inappropriate behavior:
|
||||
* 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).
|
||||
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
|
||||
@@ -71,11 +68,11 @@ If you’re too frustrated to have a direct conversation, there are a number of
|
||||
|
||||
* 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).
|
||||
* 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.
|
||||
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.
|
||||
@@ -1,13 +1,10 @@
|
||||
---
|
||||
title: Contributor's Guide
|
||||
permalink: /docs/development/contributing/
|
||||
---
|
||||
# Contributor's Guide
|
||||
|
||||
Welcome! We are very excited that you are interested in contributing to Talk.
|
||||
|
||||
This document is a companion to help you approach contributing. If it does not do so, please [let us know how we can improve it](https://github.com/coralproject/talk/issues)!
|
||||
|
||||
By contributing to this project you agree to the [Code of Conduct](https://coralproject.net/code-of-conduct).
|
||||
By contributing to this project you agree to the [Code of Conduct](CODE_OF_CONDUCT.md).
|
||||
|
||||
## What should I Contribute?
|
||||
|
||||
@@ -33,7 +30,7 @@ Please file issues if:
|
||||
|
||||
### What should I include?
|
||||
|
||||
Coral has adopted an iterative, agile development philosophy. All contributions that make it into the Talk repo should start with a story or this form:
|
||||
Coral has adopted an iterative, agile development philosophy. All contributions that make it into the Talk repository should start with a story or this form:
|
||||
|
||||
> As a [type of person] I'd like to be able to [do something] so that I can [get some result].
|
||||
|
||||
@@ -56,15 +53,24 @@ We are looking for _documentarians_ to:
|
||||
* create new / missing sections, and
|
||||
* take the lead in making sections, or the over all structure better.
|
||||
|
||||
Information about how to update docs can be found in our [FAQ]({{ "/docs/faq" | absolute_url }}#how-do-i-contribute-to-these-docs).
|
||||
Our documentation is stored in markdown files in the [docs](docs) directory. We
|
||||
use Jekyll to provide our docs. To preview:
|
||||
|
||||
```shell
|
||||
cd docs
|
||||
bundle install
|
||||
bundle exec jekyll serve
|
||||
```
|
||||
|
||||
Then visit http://127.0.0.1:4000/talk/.
|
||||
|
||||
If you'd like to discuss a contribution, please [file an issue](https://github.com/coralproject/talk/issues) describing the changes you would like to see.
|
||||
|
||||
## Contributing Translations
|
||||
|
||||
Talk's tranlations are stored in `.yml` files [here](https://github.com/coralproject/talk/tree/master/locales).
|
||||
Talk's translations are stored in `.yml` files [here](https://github.com/coralproject/talk/tree/master/locales).
|
||||
|
||||
Translations can be submitted via pull request. If you do not use github, you can use 'en.yml' as a template and [email](https://coralproject.net/contact) the translations to us. We can import it into the repo.
|
||||
Translations can be submitted via pull request. If you do not use github, you can use 'en.yml' as a template and [email](https://coralproject.net/contact) the translations to us. We can import it into the repository.
|
||||
|
||||
## I want to contribute but I'm not sure what to do!
|
||||
|
||||
@@ -84,4 +90,4 @@ Have a favorite analytics engine? Data science service? CMS? Auth platform? Depl
|
||||
|
||||
### Favorite Features?
|
||||
|
||||
Do you have a favorite feature of an existing platform that's not yet been done in Talk? Sounds like Talk needs that feature.
|
||||
Do you have a favorite feature of an existing platform that's not yet been done in Talk? Sounds like Talk needs that feature.
|
||||
@@ -1,29 +1,19 @@
|
||||
# Talk [](https://circleci.com/gh/coralproject/talk)
|
||||
[](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)
|
||||
|
||||
Online comments are broken. Our open-source Talk tool rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here.](https://coralproject.net/products/talk.html)
|
||||
Online comments are broken. Our open-source Talk tool rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here](https://coralproject.net/products/talk.html).
|
||||
|
||||
Third party licenses are available via the `/client/3rdpartylicenses.txt`
|
||||
endpoint when the server is running with built assets.
|
||||
## Documentation
|
||||
|
||||
## Try Talk!
|
||||
Developer Documentation & Setup Guides https://coralproject.github.io/talk/.
|
||||
|
||||
- Developer Documentation & Setup Guides: https://coralproject.github.io/talk/ (includes Installation Guide, Quickstart, Plugin Guide, API Docs, and more)
|
||||
|
||||
## Roadmap and Release Schedule
|
||||
|
||||
- Talk Roadmap: https://www.pivotaltracker.com/n/projects/1863625
|
||||
|
||||
|
||||
## Learn More about Coral
|
||||
## Relevant Links
|
||||
|
||||
- Blog: https://blog.coralproject.net/
|
||||
- Community Forums: https://community.coralproject.net/
|
||||
|
||||
- Website: https://coralproject.net
|
||||
|
||||
- Blog: https://blog.coralproject.net
|
||||
|
||||
- Community Guides for Journalism: https://guides.coralproject.net/
|
||||
- Project: https://coralproject.net/
|
||||
- Roadmap: https://www.pivotaltracker.com/n/projects/1863625
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -6,13 +6,9 @@ const merge = require('lodash/merge');
|
||||
const helmet = require('helmet');
|
||||
const compression = require('compression');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const {
|
||||
BASE_URL,
|
||||
BASE_PATH,
|
||||
MOUNT_PATH,
|
||||
STATIC_URL,
|
||||
HELMET_CONFIGURATION,
|
||||
} = require('./url');
|
||||
const {HELMET_CONFIGURATION} = require('./config');
|
||||
const {MOUNT_PATH} = require('./url');
|
||||
const {applyLocals} = require('./services/locals');
|
||||
const routes = require('./routes');
|
||||
const debug = require('debug')('talk:app');
|
||||
|
||||
@@ -57,12 +53,8 @@ app.set('view engine', 'ejs');
|
||||
// ROUTES
|
||||
//==============================================================================
|
||||
|
||||
// Apply the BASE_PATH, BASE_URL, and MOUNT_PATH on the app.locals, which will
|
||||
// make them available on the templates and the routers.
|
||||
app.locals.BASE_URL = BASE_URL;
|
||||
app.locals.BASE_PATH = BASE_PATH;
|
||||
app.locals.MOUNT_PATH = MOUNT_PATH;
|
||||
app.locals.STATIC_URL = STATIC_URL;
|
||||
// Add the locals to the app renderer.
|
||||
applyLocals(app.locals);
|
||||
|
||||
debug(`mounting routes on the ${MOUNT_PATH} path`);
|
||||
|
||||
|
||||
+1
-22
@@ -1,24 +1,3 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"extends": "../.eslintrc.json",
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error",
|
||||
"no-console": ["warn", { "allow": ["warn", "error"] }]
|
||||
}
|
||||
"extends": "@coralproject/eslint-config-talk/client"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import Configure from 'routes/Configure';
|
||||
import Dashboard from 'routes/Dashboard';
|
||||
import Install from 'routes/Install';
|
||||
import Stories from 'routes/Stories';
|
||||
import {CommunityLayout, Community} from 'routes/Community';
|
||||
import Community from 'routes/Community/containers/Community';
|
||||
import {ModerationLayout, Moderation} from 'routes/Moderation';
|
||||
|
||||
import Layout from 'containers/Layout';
|
||||
@@ -22,7 +22,7 @@ const routes = (
|
||||
|
||||
{/* Community Routes */}
|
||||
|
||||
<Route path='community' component={CommunityLayout}>
|
||||
<Route path='community'>
|
||||
<Route path='flagged' components={Community}>
|
||||
<Route path=':id' components={Community} />
|
||||
</Route>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import qs from 'qs';
|
||||
import queryString from 'query-string';
|
||||
|
||||
import {
|
||||
FETCH_COMMENTERS_REQUEST,
|
||||
@@ -17,9 +17,8 @@ import {
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
export const fetchAccounts = (query = {}) => (dispatch, _, {rest}) => {
|
||||
|
||||
dispatch(requestFetchAccounts());
|
||||
rest(`/users?${qs.stringify(query)}`)
|
||||
rest(`/users?${queryString.stringify(query)}`)
|
||||
.then(({result, page, count, limit, totalPages}) =>{
|
||||
dispatch({
|
||||
type: FETCH_COMMENTERS_SUCCESS,
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import styles from './ModerationList.css';
|
||||
import {Button} from 'coral-ui';
|
||||
import {menuActionsMap} from '../utils/moderationQueueActionsMap';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const ActionButton = ({type = '', active, ...props}) => {
|
||||
const typeName = type.toLowerCase();
|
||||
let text = menuActionsMap[type].text;
|
||||
|
||||
if (text === 'approve' && active) {
|
||||
text = 'approved';
|
||||
} else if (text === 'reject' && active) {
|
||||
text = 'rejected';
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={`${typeName} ${styles.actionButton} ${props.minimal ? styles.minimal : ''} ${active ? styles[`${typeName}__active`] : ''}`}
|
||||
cStyle={typeName}
|
||||
icon={menuActionsMap[type].icon}
|
||||
onClick={type === 'APPROVE' ? props.acceptComment : props.rejectComment}
|
||||
>{props.minimal ? '' : t(`modqueue.${text}`)}</Button>
|
||||
);
|
||||
};
|
||||
|
||||
ActionButton.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
type: PropTypes.oneOf(['APPROVE', 'REJECT']),
|
||||
minimal: PropTypes.bool,
|
||||
acceptComment: PropTypes.func,
|
||||
rejectComment: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ActionButton;
|
||||
@@ -0,0 +1,44 @@
|
||||
.root {
|
||||
display: block;
|
||||
color: #519954;
|
||||
border: solid 2px rgba(81, 153, 84, 0.75);
|
||||
background: white;
|
||||
padding: 10px 12px;
|
||||
box-sizing: border-box;
|
||||
vertical-align: middle;
|
||||
line-height: 24px;
|
||||
font-size: 17px;
|
||||
height: 47px;
|
||||
border-radius: 3px;
|
||||
text-transform: capitalize;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
|
||||
width: 129px;
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
background-color: #519954;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
background-color: #519954;
|
||||
|
||||
&:hover {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.minimal {
|
||||
width: 45px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import cn from 'classnames';
|
||||
import styles from './ApproveButton.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const ApproveButton = ({active, minimal, onClick}) => {
|
||||
const text = active ? t('modqueue.approved') : t('modqueue.approve');
|
||||
return (
|
||||
<button
|
||||
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active})}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon name={'done'} className={styles.icon} />
|
||||
{!minimal && text}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
ApproveButton.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
minimal: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ApproveButton;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
.root {
|
||||
min-height: 25px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.moreDetail {
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: black;
|
||||
right: 16px;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './CommentDetails.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import IfSlotIsNotEmpty from 'coral-framework/components/IfSlotIsNotEmpty';
|
||||
|
||||
class CommentDetails extends Component {
|
||||
state = {
|
||||
showDetail: false
|
||||
};
|
||||
|
||||
constructor () {
|
||||
super();
|
||||
this.state = {
|
||||
showDetail: false
|
||||
};
|
||||
}
|
||||
|
||||
toggleDetail = () => {
|
||||
this.setState((state) => ({
|
||||
showDetail: !state.showDetail
|
||||
}));
|
||||
}
|
||||
|
||||
render() {
|
||||
const {data, root, comment} = this.props;
|
||||
const {showDetail} = this.state;
|
||||
const queryData = {
|
||||
root,
|
||||
comment,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<IfSlotIsNotEmpty
|
||||
queryData={queryData}
|
||||
slot={['adminCommentMoreDetails', 'adminCommentMoreFlagDetails']}
|
||||
>
|
||||
<a onClick={this.toggleDetail} className={styles.moreDetail}>
|
||||
{showDetail ? t('modqueue.less_detail') : t('modqueue.more_detail')}
|
||||
</a>
|
||||
</IfSlotIsNotEmpty>
|
||||
<Slot
|
||||
fill="adminCommentDetailArea"
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
more={showDetail}
|
||||
/>
|
||||
{showDetail && <Slot
|
||||
fill="adminCommentMoreDetails"
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CommentDetails.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
comment: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default CommentDetails;
|
||||
@@ -1,83 +0,0 @@
|
||||
.flagBox {
|
||||
border-top: 1px solid rgba(66, 66, 66, 0.12);
|
||||
margin-top: 10px;
|
||||
.container {
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
.detail {
|
||||
padding: 0 20px 16px;
|
||||
ul {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
.moreDetail {
|
||||
float: right;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin-right: 10px;
|
||||
margin-top: 8px;
|
||||
color: black;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
i {
|
||||
vertical-align: middle;
|
||||
font-size: 12px;
|
||||
}
|
||||
ul {
|
||||
vertical-align: middle;
|
||||
list-style: none;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
vertical-align: middle;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
display: inline-block;
|
||||
margin-left: 7px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.lessDetail {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.subDetail {
|
||||
font-weight: normal;
|
||||
color: #888;
|
||||
|
||||
span {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
color: #393B44;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
padding: 2px 5px;
|
||||
border-radius: 2px;
|
||||
margin-left: -5px;
|
||||
transition: background-color 200ms ease;
|
||||
&:hover {
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Icon} from 'coral-ui';
|
||||
import styles from './FlagBox.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const shortReasons = {
|
||||
'This comment is offensive': t('modqueue.offensive'),
|
||||
'This looks like an ad/marketing': t('modqueue.spam_ads'),
|
||||
'This user is impersonating': t('modqueue.impersonating'),
|
||||
'I don\'t like this username': t('modqueue.dont_like_username'),
|
||||
'Other': t('modqueue.other')
|
||||
};
|
||||
|
||||
class FlagBox extends Component {
|
||||
constructor () {
|
||||
super();
|
||||
this.state = {
|
||||
showDetail: false
|
||||
};
|
||||
}
|
||||
|
||||
toggleDetail = () => {
|
||||
this.setState((state) => ({
|
||||
showDetail: !state.showDetail
|
||||
}));
|
||||
}
|
||||
|
||||
reasonMap = (reason) => {
|
||||
const shortReason = shortReasons[reason];
|
||||
|
||||
// if the short reason isn't found, just return the long one.
|
||||
return shortReason ? shortReason : reason;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {actionSummaries, actions, viewUserDetail} = this.props;
|
||||
const {showDetail} = this.state;
|
||||
|
||||
return (
|
||||
<div className={styles.flagBox}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<Icon name='flag'/><h3>{t('community.flags')} ({actionSummaries.length}):</h3>
|
||||
<ul>
|
||||
{actionSummaries.map((action, i) =>
|
||||
<li key={i} className={styles.lessDetail}> {this.reasonMap(action.reason)} (<strong>{action.count}</strong>)</li>
|
||||
)}
|
||||
</ul>
|
||||
<a onClick={this.toggleDetail} className={styles.moreDetail}>{showDetail ? t('modqueue.less_detail') : t('modqueue.more_detail')}</a>
|
||||
</div>
|
||||
{showDetail && (
|
||||
<div className={styles.detail}>
|
||||
<ul>
|
||||
{actionSummaries.map((summary, i) => {
|
||||
|
||||
const actionList = actions.filter((a) => a.reason === summary.reason);
|
||||
|
||||
return (
|
||||
<li key={i}>
|
||||
{this.reasonMap(summary.reason)} (<strong>{summary.count}</strong>)
|
||||
<ul>
|
||||
{actionList.map((action, j) =>
|
||||
<li key={`${i}_${j}`} className={styles.subDetail}>
|
||||
{action.user &&
|
||||
<a className={styles.username} onClick={() => viewUserDetail(action.user.id)}>
|
||||
{action.user.username}
|
||||
</a>
|
||||
}
|
||||
{action.message}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FlagBox.propTypes = {
|
||||
actionSummaries: PropTypes.arrayOf(PropTypes.shape({
|
||||
reason: PropTypes.string,
|
||||
count: PropTypes.number
|
||||
})).isRequired,
|
||||
actions: PropTypes.arrayOf(PropTypes.shape({
|
||||
message: PropTypes.string,
|
||||
user: PropTypes.shape({username: PropTypes.string})
|
||||
})).isRequired
|
||||
};
|
||||
|
||||
export default FlagBox;
|
||||
@@ -1,208 +0,0 @@
|
||||
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.list {
|
||||
padding: 8px 0;
|
||||
list-style: none;
|
||||
display: block;
|
||||
|
||||
&.singleView .listItem {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.singleView .listItem.activeItem {
|
||||
display: block;
|
||||
height: 100%;
|
||||
font-size: 1.5em;
|
||||
line-height: 1.5em;
|
||||
border: none;
|
||||
|
||||
.actions {
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
left: 25%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 50%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.listItem {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
max-width: 660px;
|
||||
min-width: 400px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 14px;
|
||||
position: relative;
|
||||
transition: box-shadow 200ms;
|
||||
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.sideActions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
padding: 40px 18px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.itemHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.author {
|
||||
min-width: 230px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.itemBody {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-right: 16px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: #757575;
|
||||
font-size: 40px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.created {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-top: 20px;
|
||||
flex: 1;
|
||||
font-size: 0.88em;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.flagged {
|
||||
color: rgba(255, 0, 0, .5);
|
||||
padding-top: 15px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.flagCount{
|
||||
font-size: 12px;
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: #444;
|
||||
margin-top: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@media (--big-viewport) {
|
||||
.listItem {
|
||||
border: 1px solid #e0e0e0;
|
||||
margin-bottom: 30px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
&.activeItem {
|
||||
border: 2px solid #333;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.hasLinks {
|
||||
color: #f00;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.banned {
|
||||
color: #f00;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.ban {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.banButton {
|
||||
width: 114px;
|
||||
letter-spacing: 1px;
|
||||
|
||||
i {
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.selected {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
|
||||
.actionButton {
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.minimal {
|
||||
width: 45px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.approve__active {
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
background-color: #519954;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.reject__active, .rejected__active {
|
||||
color: white;
|
||||
background-color: #D03235;
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './ModerationList.css';
|
||||
import key from 'keymaster';
|
||||
import Hammer from 'hammerjs';
|
||||
import Comment from './Comment';
|
||||
import User from './User';
|
||||
import SuspendUserModal from './SuspendUserModal';
|
||||
|
||||
// Each action has different meaning and configuration
|
||||
const menuOptionsMap = {
|
||||
'reject': {status: 'REJECTED', icon: 'close', key: 'f'},
|
||||
'approve': {status: 'ACCEPTED', icon: 'done', key: 'd'},
|
||||
'flag': {status: 'FLAGGED', icon: 'flag', filter: 'Untouched'},
|
||||
'ban': {status: 'BANNED', icon: 'not interested'}
|
||||
};
|
||||
|
||||
// Renders a comment list and allow performing actions
|
||||
export default class ModerationList extends React.Component {
|
||||
static propTypes = {
|
||||
isActive: PropTypes.bool,
|
||||
singleView: PropTypes.bool,
|
||||
commentIds: PropTypes.arrayOf(PropTypes.string),
|
||||
actionIds: PropTypes.arrayOf(PropTypes.string),
|
||||
comments: PropTypes.object,
|
||||
users: PropTypes.object.isRequired,
|
||||
actions: PropTypes.object,
|
||||
userStatusUpdate: PropTypes.func.isRequired,
|
||||
suspendUser: PropTypes.func.isRequired,
|
||||
|
||||
// list of actions (flags, etc) associated with the comments
|
||||
modActions: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
loading: PropTypes.bool,
|
||||
|
||||
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired
|
||||
}
|
||||
|
||||
state = {active: null, suspendUserModal: null, email: null};
|
||||
|
||||
// remove key handlers before leaving
|
||||
componentWillUnmount () {
|
||||
this.unbindKeyHandlers();
|
||||
}
|
||||
|
||||
// add key handlers and gestures
|
||||
componentDidMount () {
|
||||
this.bindKeyHandlers();
|
||||
|
||||
// this.bindGestures() // need to check whether we're on a mobile device or this throws an Error
|
||||
}
|
||||
|
||||
// If entering to singleview and no active, active is the first eleement
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.singleView && !this.state.active) {
|
||||
this.setState({active: nextProps.commentIds[0]});
|
||||
}
|
||||
}
|
||||
|
||||
// Add swipe to approve or reject
|
||||
bindGestures () {
|
||||
const {modActions} = this.props;
|
||||
this._hammer = new Hammer(this.base);
|
||||
this._hammer.get('swipe').set({direction: Hammer.DIRECTION_HORIZONTAL});
|
||||
|
||||
if (modActions.indexOf('reject') !== -1) {
|
||||
this._hammer.on('swipeleft', () => this.props.singleView && this.actionKeyHandler('Rejected'));
|
||||
}
|
||||
if (modActions.indexOf('approve') !== -1) {
|
||||
this._hammer.on('swiperight', () => this.props.singleView && this.actionKeyHandler('Approved'));
|
||||
}
|
||||
}
|
||||
|
||||
// Add key handlers. Each action has one and added j/k for moving around
|
||||
bindKeyHandlers () {
|
||||
const {modActions, isActive} = this.props;
|
||||
modActions.filter((action) => menuOptionsMap[action].key).forEach((action) => {
|
||||
key(menuOptionsMap[action].key, 'moderationList', () => isActive && this.actionKeyHandler(menuOptionsMap[action].status));
|
||||
});
|
||||
key('j', 'moderationList', () => isActive && this.moveKeyHandler('down'));
|
||||
key('k', 'moderationList', () => isActive && this.moveKeyHandler('up'));
|
||||
key.setScope('moderationList');
|
||||
}
|
||||
|
||||
// Perform an action using the keys only if the comment is active
|
||||
actionKeyHandler (action) {
|
||||
if (this.props.isActive && this.state.active) {
|
||||
this.onClickAction(action, this.state.active);
|
||||
}
|
||||
}
|
||||
|
||||
// move around with j/k
|
||||
moveKeyHandler (direction) {
|
||||
if (!this.props.isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {commentIds} = this.props;
|
||||
const {active} = this.state;
|
||||
|
||||
// check boundaries
|
||||
if (active === null || !commentIds.length) {
|
||||
this.setState({active: commentIds[0]});
|
||||
} else if (direction === 'up' && active !== commentIds[0]) {
|
||||
this.setState({active: commentIds[commentIds.indexOf(active) - 1]});
|
||||
} else if (direction === 'down' && active !== commentIds[commentIds.length - 1]) {
|
||||
this.setState({active: commentIds[commentIds.indexOf(active) + 1]});
|
||||
}
|
||||
|
||||
// scroll to the position
|
||||
const index = Math.max(commentIds.indexOf(this.state.active), 0);
|
||||
this.base.childNodes[index] && this.base.childNodes[index].focus();
|
||||
}
|
||||
|
||||
unbindKeyHandlers () {
|
||||
key.deleteScope('moderationList');
|
||||
}
|
||||
|
||||
// If we are performing an action over a comment (aka removing from the list) we need to select a new active.
|
||||
// TODO: In the future this can be improved and look at the actual state to
|
||||
// resolve since the content of the list could change externally. For now it works as expected
|
||||
onClickAction = (menuOption, id, action) => {
|
||||
|
||||
// activate the next comment
|
||||
if (id === this.state.active) {
|
||||
const moderationIds = this.getModerationIds();
|
||||
if (moderationIds[moderationIds.length - 1] === this.state.active) {
|
||||
this.setState({active: moderationIds[moderationIds.length - 2]});
|
||||
} else {
|
||||
this.setState({active: moderationIds[Math.min(moderationIds.indexOf(this.state.active) + 1, moderationIds.length - 1)]});
|
||||
}
|
||||
}
|
||||
|
||||
// Update the status right away if this is a comment
|
||||
if (action.item_type === 'COMMENTS') {
|
||||
this.props.updateCommentStatus(menuOption, id);
|
||||
} else if (action.item_type === 'USERS') {
|
||||
|
||||
// If a user bio or name is rejected, bring up a dialog before suspending them.
|
||||
if (menuOption === 'REJECTED') {
|
||||
this.setState({suspendUserModal: action});
|
||||
} else if (menuOption === 'ACCEPTED') {
|
||||
this.props.userStatusUpdate('APPROVED', action.item_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClickShowBanDialog = (userId, userName, commentId) => {
|
||||
this.props.onClickShowBanDialog(userId, userName, commentId);
|
||||
}
|
||||
|
||||
mapModItems = (itemId, index) => {
|
||||
|
||||
const {comments = {}, users, actions = {}, modActions, suspectWords, hideActive} = this.props;
|
||||
const {active} = this.state;
|
||||
|
||||
// Because ids are unique, the id will either appear as an action or as a comment.
|
||||
|
||||
const item = comments[itemId] || actions[itemId];
|
||||
let modItem;
|
||||
|
||||
if (item.body) {
|
||||
|
||||
// If the item is a comment...
|
||||
const author = users[item.author_id];
|
||||
modItem = <Comment
|
||||
suspectWords={suspectWords}
|
||||
comment={item}
|
||||
author={author}
|
||||
key={index}
|
||||
index={index}
|
||||
onClickAction={this.onClickAction}
|
||||
onClickShowBanDialog={this.onClickShowBanDialog}
|
||||
modActions={modActions}
|
||||
menuOptionsMap={menuOptionsMap}
|
||||
isActive={itemId === active}
|
||||
hideActive={hideActive} />;
|
||||
} else {
|
||||
|
||||
// If the item is an action...
|
||||
const user = users[item.item_id];
|
||||
modItem = user && <User
|
||||
suspectWords={suspectWords}
|
||||
action={item}
|
||||
user={user}
|
||||
key={index}
|
||||
index={index}
|
||||
onClickAction={this.onClickAction}
|
||||
onClickShowBanDialog={this.onClickShowBanDialog}
|
||||
modActions={modActions}
|
||||
menuOptionsMap={menuOptionsMap}
|
||||
isActive={itemId === active}
|
||||
hideActive={hideActive} />;
|
||||
}
|
||||
return modItem;
|
||||
}
|
||||
|
||||
getModerationIds = () => {
|
||||
const {commentIds = [], actionIds = [], comments, actions} = this.props;
|
||||
if (comments && actions) {
|
||||
return [ ...commentIds, ...actionIds ].sort((a, b) => {
|
||||
const itemA = comments[a] || actions[a];
|
||||
const itemB = comments[b] || actions[b];
|
||||
return itemB.updated_at - itemA.updated_at;
|
||||
});
|
||||
} else {
|
||||
return comments ? commentIds : actionIds;
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {singleView, key, suspendUser} = this.props;
|
||||
|
||||
// Combine moderations and actions into a single stream and sort by most recently updated.
|
||||
const moderationIds = this.getModerationIds();
|
||||
|
||||
return (
|
||||
<ul
|
||||
className={`${styles.list} ${singleView ? styles.singleView : ''}`} {...key}
|
||||
id='moderationList'>
|
||||
{moderationIds.map(this.mapModItems)}
|
||||
<SuspendUserModal
|
||||
action = {this.state.suspendUserModal}
|
||||
onClose={() => this.setState({suspendUserModal:null})}
|
||||
suspendUser={suspendUser} />
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
.root {
|
||||
display: block;
|
||||
color: #D03235;
|
||||
border: solid 1px #D03235;
|
||||
background: white;
|
||||
padding: 10px 11px;
|
||||
box-sizing: border-box;
|
||||
vertical-align: middle;
|
||||
line-height: 24px;
|
||||
font-size: 17px;
|
||||
height: 47px;
|
||||
border-radius: 3px;
|
||||
text-transform: capitalize;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
|
||||
width: 129px;
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: #D03235;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
color: white;
|
||||
background-color: #D03235;
|
||||
box-shadow: none;
|
||||
|
||||
&:hover {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.minimal {
|
||||
width: 45px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import cn from 'classnames';
|
||||
import styles from './RejectButton.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const RejectButton = ({active, minimal, onClick}) => {
|
||||
const text = active ? t('modqueue.rejected') : t('modqueue.reject');
|
||||
return (
|
||||
<button
|
||||
className={cn(styles.root, {[styles.minimal]: minimal, [styles.active]: active})}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon name={'close'} className={styles.icon} />
|
||||
{!minimal && text}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
RejectButton.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
minimal: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
export default RejectButton;
|
||||
|
||||
@@ -5,7 +5,6 @@ import styles from './UserDetail.css';
|
||||
import {Icon, Button, Drawer, Spinner} from 'coral-ui';
|
||||
import {Slot} from 'coral-framework/components';
|
||||
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
|
||||
import {actionsMap} from '../utils/moderationQueueActionsMap';
|
||||
import ClickOutside from 'coral-framework/components/ClickOutside';
|
||||
import LoadMore from '../components/LoadMore';
|
||||
import cn from 'classnames';
|
||||
@@ -70,6 +69,7 @@ export default class UserDetail extends React.Component {
|
||||
|
||||
renderLoaded() {
|
||||
const {
|
||||
data,
|
||||
root,
|
||||
root: {
|
||||
user,
|
||||
@@ -177,15 +177,15 @@ export default class UserDetail extends React.Component {
|
||||
<div>
|
||||
{
|
||||
nodes.map((comment) => {
|
||||
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
|
||||
const selected = selectedCommentIds.indexOf(comment.id) !== -1;
|
||||
return <Comment
|
||||
key={comment.id}
|
||||
user={user}
|
||||
root={root}
|
||||
data={data}
|
||||
comment={comment}
|
||||
suspectWords={suspectWords}
|
||||
bannedWords={bannedWords}
|
||||
actions={actionsMap[status]}
|
||||
acceptComment={this.acceptThenReload}
|
||||
rejectComment={this.rejectThenReload}
|
||||
selected={selected}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.root {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin: 0;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
|
||||
@@ -3,37 +3,41 @@ import PropTypes from 'prop-types';
|
||||
import {Link} from 'react-router';
|
||||
|
||||
import {Icon} from 'coral-ui';
|
||||
import FlagBox from './FlagBox';
|
||||
import CommentDetails from './CommentDetails';
|
||||
import styles from './UserDetailComment.css';
|
||||
import {getActionSummary} from 'coral-framework/utils';
|
||||
import ActionButton from 'coral-admin/src/components/ActionButton';
|
||||
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
|
||||
import IfHasLink from 'coral-admin/src/components/IfHasLink';
|
||||
import cn from 'classnames';
|
||||
import CommentAnimatedEdit from './CommentAnimatedEdit';
|
||||
import CommentLabels from '../containers/CommentLabels';
|
||||
import ApproveButton from './ApproveButton';
|
||||
import RejectButton from 'coral-admin/src/components/RejectButton';
|
||||
|
||||
import t, {timeago} from 'coral-framework/services/i18n';
|
||||
|
||||
class UserDetailComment extends React.Component {
|
||||
|
||||
approve = () => (this.props.comment.status === 'ACCEPTED'
|
||||
? null
|
||||
: this.props.acceptComment({commentId: this.props.comment.id})
|
||||
);
|
||||
|
||||
reject = () => (this.props.comment.status === 'REJECTED'
|
||||
? null
|
||||
: this.props.rejectComment({commentId: this.props.comment.id})
|
||||
);
|
||||
|
||||
render() {
|
||||
const {
|
||||
actions = [],
|
||||
comment,
|
||||
viewUserDetail,
|
||||
suspectWords,
|
||||
bannedWords,
|
||||
selected,
|
||||
toggleSelect,
|
||||
className,
|
||||
user,
|
||||
...props
|
||||
data,
|
||||
} = this.props;
|
||||
|
||||
const flagActionSummaries = getActionSummary('FlagActionSummary', comment);
|
||||
const flagActions = comment.actions && comment.actions.filter((a) => a.__typename === 'FlagAction');
|
||||
|
||||
return (
|
||||
<li
|
||||
tabIndex={0}
|
||||
@@ -66,7 +70,7 @@ class UserDetailComment extends React.Component {
|
||||
</div>
|
||||
<CommentAnimatedEdit body={comment.body}>
|
||||
<div className={styles.bodyContainer}>
|
||||
<p className={styles.body}>
|
||||
<div className={styles.body}>
|
||||
<CommentBodyHighlighter
|
||||
suspectWords={suspectWords}
|
||||
bannedWords={bannedWords}
|
||||
@@ -80,7 +84,7 @@ class UserDetailComment extends React.Component {
|
||||
>
|
||||
<Icon name="open_in_new" /> {t('comment.view_context')}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.sideActions}>
|
||||
<IfHasLink text={comment.body}>
|
||||
<span className={styles.hasLinks}>
|
||||
@@ -88,41 +92,26 @@ class UserDetailComment extends React.Component {
|
||||
</span>
|
||||
</IfHasLink>
|
||||
<div className={styles.actions}>
|
||||
{actions.map((action, i) => {
|
||||
const active =
|
||||
(action === 'REJECT' && comment.status === 'REJECTED') ||
|
||||
(action === 'APPROVE' && comment.status === 'ACCEPTED');
|
||||
return (
|
||||
<ActionButton
|
||||
minimal={true}
|
||||
key={i}
|
||||
type={action}
|
||||
user={user}
|
||||
status={comment.status}
|
||||
active={active}
|
||||
acceptComment={() =>
|
||||
(comment.status === 'ACCEPTED'
|
||||
? null
|
||||
: props.acceptComment({commentId: comment.id}))}
|
||||
rejectComment={() =>
|
||||
(comment.status === 'REJECTED'
|
||||
? null
|
||||
: props.rejectComment({commentId: comment.id}))}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<ApproveButton
|
||||
active={comment.status === 'ACCEPTED'}
|
||||
onClick={this.approve}
|
||||
minimal
|
||||
/>
|
||||
<RejectButton
|
||||
active={comment.status === 'REJECTED'}
|
||||
onClick={this.reject}
|
||||
minimal
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CommentAnimatedEdit>
|
||||
</div>
|
||||
{flagActions && flagActions.length
|
||||
? <FlagBox
|
||||
actions={flagActions}
|
||||
actionSummaries={flagActionSummaries}
|
||||
viewUserDetail={viewUserDetail}
|
||||
/>
|
||||
: null}
|
||||
<CommentDetails
|
||||
data={data}
|
||||
root={root}
|
||||
comment={comment}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -138,8 +127,9 @@ UserDetailComment.propTypes = {
|
||||
bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
toggleSelect: PropTypes.func,
|
||||
comment: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
body: PropTypes.string.isRequired,
|
||||
action_summaries: PropTypes.array,
|
||||
actions: PropTypes.array,
|
||||
created_at: PropTypes.string.isRequired,
|
||||
asset: PropTypes.shape({
|
||||
|
||||
@@ -6,101 +6,110 @@ import styles from './Header.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {Logo} from './Logo';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
import Indicator from './Indicator';
|
||||
|
||||
const CoralHeader = ({
|
||||
handleLogout,
|
||||
showShortcuts = () => {},
|
||||
auth
|
||||
}) => (
|
||||
<Header className={styles.header}>
|
||||
<Logo className={styles.logo} />
|
||||
<div>
|
||||
{
|
||||
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
|
||||
<Navigation className={styles.nav}>
|
||||
<IndexLink
|
||||
id='dashboardNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/dashboard"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.dashboard')}
|
||||
</IndexLink>
|
||||
{
|
||||
can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
<Link
|
||||
id='moderateNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/moderate"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.moderate')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
<Link
|
||||
id='streamsNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/stories"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.stories')}
|
||||
</Link>
|
||||
<Link
|
||||
id='communityNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/community"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.community')}
|
||||
</Link>
|
||||
{
|
||||
can(auth.user, 'UPDATE_CONFIG') && (
|
||||
<Link
|
||||
id='configureNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/configure"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.configure')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
</Navigation>
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className={styles.rightPanel}>
|
||||
<ul>
|
||||
<li className={styles.settings}>
|
||||
<div>
|
||||
<IconButton name="settings" id="menu-settings"/>
|
||||
<Menu target="menu-settings" align="right">
|
||||
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://github.com/coralproject/talk/releases" target="_blank" rel="noopener noreferrer">
|
||||
View latest version
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://coralproject.net/contribute.html#other-ideas-and-bug-reports" target="_blank" rel="noopener noreferrer">
|
||||
Report a bug or give feedback
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleLogout}>
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
{`v${process.env.VERSION}`}
|
||||
</li>
|
||||
</ul>
|
||||
auth,
|
||||
root
|
||||
}) => {
|
||||
return (
|
||||
<Header className={styles.header}>
|
||||
<Logo className={styles.logo} />
|
||||
<div>
|
||||
{
|
||||
auth && auth.user && can(auth.user, 'ACCESS_ADMIN') ?
|
||||
<Navigation className={styles.nav}>
|
||||
<IndexLink
|
||||
id='dashboardNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/dashboard"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.dashboard')}
|
||||
</IndexLink>
|
||||
{
|
||||
can(auth.user, 'MODERATE_COMMENTS') && (
|
||||
<Link
|
||||
id='moderateNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/moderate"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.moderate')}
|
||||
{(root.premodCount !== 0 || root.reportedCount !== 0) && <Indicator />}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
<Link
|
||||
id='streamsNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/stories"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.stories')}
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
id='communityNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/community"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.community')}
|
||||
{root.flaggedUsernamesCount !== 0 && <Indicator />}
|
||||
</Link>
|
||||
|
||||
{
|
||||
can(auth.user, 'UPDATE_CONFIG') && (
|
||||
<Link
|
||||
id='configureNav'
|
||||
className={styles.navLink}
|
||||
to="/admin/configure"
|
||||
activeClassName={styles.active}>
|
||||
{t('configure.configure')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
</Navigation>
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className={styles.rightPanel}>
|
||||
<ul>
|
||||
<li className={styles.settings}>
|
||||
<div>
|
||||
<IconButton name="settings" id="menu-settings"/>
|
||||
<Menu target="menu-settings" align="right">
|
||||
<MenuItem onClick={() => showShortcuts(true)}>{t('configure.shortcuts')}</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://github.com/coralproject/talk/releases" target="_blank" rel="noopener noreferrer">
|
||||
View latest version
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a href="https://coralproject.net/contribute.html#other-ideas-and-bug-reports" target="_blank" rel="noopener noreferrer">
|
||||
Report a bug or give feedback
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleLogout}>
|
||||
{t('configure.sign_out')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
{`v${process.env.VERSION}`}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Header>
|
||||
);
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
CoralHeader.propTypes = {
|
||||
auth: PropTypes.object,
|
||||
showShortcuts: PropTypes.func,
|
||||
handleLogout: PropTypes.func.isRequired
|
||||
handleLogout: PropTypes.func.isRequired,
|
||||
root: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default CoralHeader;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
.indicator {
|
||||
background-color: #E46D59;
|
||||
border-radius: 10px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin-top: -4px;
|
||||
margin-left: 7px;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import styles from './Indicator.css';
|
||||
|
||||
const Indicator = () =>
|
||||
<span className={styles.indicator}></span>;
|
||||
|
||||
export default Indicator;
|
||||
@@ -1,22 +1,27 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Layout as LayoutMDL} from 'react-mdl';
|
||||
import Header from './Header';
|
||||
import Header from '../../containers/Header';
|
||||
import Drawer from './Drawer';
|
||||
import styles from './Layout.css';
|
||||
|
||||
const Layout = ({
|
||||
children,
|
||||
handleLogout = () => {},
|
||||
toggleShortcutModal,
|
||||
toggleShortcutModal = () => {},
|
||||
restricted = false,
|
||||
...props}) => (
|
||||
auth,
|
||||
}) => (
|
||||
<LayoutMDL className={styles.layout} fixedDrawer>
|
||||
<Header
|
||||
handleLogout={handleLogout}
|
||||
showShortcuts={toggleShortcutModal}
|
||||
{...props} />
|
||||
<Drawer handleLogout={handleLogout} restricted={restricted} {...props} />
|
||||
auth={auth}
|
||||
/>
|
||||
<Drawer
|
||||
handleLogout={handleLogout}
|
||||
restricted={restricted}
|
||||
/>
|
||||
<div className={styles.layout}>
|
||||
{children}
|
||||
</div>
|
||||
@@ -24,6 +29,8 @@ const Layout = ({
|
||||
);
|
||||
|
||||
Layout.propTypes = {
|
||||
children: PropTypes.node,
|
||||
auth: PropTypes.object,
|
||||
handleLogout: PropTypes.func,
|
||||
toggleShortcutModal: PropTypes.func,
|
||||
restricted: PropTypes.bool // hide elements from a user that's logged out
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import styles from './Logo.css';
|
||||
import {CoralLogo} from 'coral-ui';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const Logo = ({className = ''}) => (
|
||||
<div className={`${styles.logo} ${className}`}>
|
||||
@@ -10,3 +11,7 @@ export const Logo = ({className = ''}) => (
|
||||
</h1>
|
||||
</div>
|
||||
);
|
||||
|
||||
Logo.propTypes = {
|
||||
className: PropTypes.string
|
||||
};
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import {gql} from 'react-apollo';
|
||||
import CommentDetails from '../components/CommentDetails';
|
||||
import {getSlotFragmentSpreads} from 'coral-framework/utils';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
|
||||
const slots = [
|
||||
'adminCommentDetailArea',
|
||||
'adminCommentMoreDetails',
|
||||
];
|
||||
|
||||
export default withFragments({
|
||||
root: gql`
|
||||
fragment CoralAdmin_CommentDetails_root on RootQuery {
|
||||
__typename
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
}
|
||||
`,
|
||||
comment: gql`
|
||||
fragment CoralAdmin_CommentDetails_comment on Comment {
|
||||
__typename
|
||||
${getSlotFragmentSpreads(slots, 'comment')}
|
||||
}
|
||||
`
|
||||
})(CommentDetails);
|
||||
@@ -8,6 +8,12 @@ const slots = [
|
||||
];
|
||||
|
||||
export default withFragments({
|
||||
root: gql`
|
||||
fragment CoralAdmin_CommentLabels_root on RootQuery {
|
||||
__typename
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
}
|
||||
`,
|
||||
comment: gql`
|
||||
fragment CoralAdmin_CommentLabels_comment on Comment {
|
||||
hasParent
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import {gql} from 'react-apollo';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import Header from '../components/ui/Header';
|
||||
|
||||
export default withQuery(gql`
|
||||
query TalkAdmin_Header {
|
||||
__typename
|
||||
premodCount: commentCount(query: {
|
||||
statuses: [PREMOD]
|
||||
})
|
||||
reportedCount: commentCount(query: {
|
||||
statuses: [NONE, PREMOD, SYSTEM_WITHHELD],
|
||||
action_type: FLAG
|
||||
})
|
||||
flaggedUsernamesCount: userCount(query: {
|
||||
action_type: FLAG,
|
||||
statuses: [PENDING]
|
||||
})
|
||||
}
|
||||
`, {
|
||||
options: {
|
||||
pollInterval: 5000
|
||||
}
|
||||
})(Header);
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, {Component} from 'react';
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import Layout from '../components/ui/Layout';
|
||||
import {fetchConfig} from '../actions/config';
|
||||
import AdminLogin from '../components/AdminLogin';
|
||||
@@ -10,15 +11,18 @@ import {toggleModal as toggleShortcutModal} from '../actions/moderation';
|
||||
import {checkLogin, handleLogin, requestPasswordReset, logout} from '../actions/auth';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
import UserDetail from 'coral-admin/src/containers/UserDetail';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class LayoutContainer extends Component {
|
||||
class LayoutContainer extends React.Component {
|
||||
componentWillMount() {
|
||||
const {checkLogin, fetchConfig} = this.props;
|
||||
|
||||
checkLogin();
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const {
|
||||
user,
|
||||
loggedIn,
|
||||
@@ -29,13 +33,16 @@ class LayoutContainer extends Component {
|
||||
} = this.props.auth;
|
||||
|
||||
const {
|
||||
handleLogout,
|
||||
children,
|
||||
logout,
|
||||
toggleShortcutModal,
|
||||
TALK_RECAPTCHA_PUBLIC
|
||||
TALK_RECAPTCHA_PUBLIC,
|
||||
} = this.props;
|
||||
|
||||
if (loadingUser) {
|
||||
return <FullLoading />;
|
||||
}
|
||||
|
||||
if (!loggedIn) {
|
||||
return (
|
||||
<AdminLogin
|
||||
@@ -48,17 +55,17 @@ class LayoutContainer extends Component {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (can(user, 'ACCESS_ADMIN') && loggedIn) {
|
||||
return (
|
||||
<Layout
|
||||
handleLogout={handleLogout}
|
||||
handleLogout={logout}
|
||||
toggleShortcutModal={toggleShortcutModal}
|
||||
{...this.props}
|
||||
>
|
||||
auth={this.props.auth} >
|
||||
<BanUserDialog />
|
||||
<SuspendUserDialog />
|
||||
<UserDetail />
|
||||
{this.props.children}
|
||||
{children}
|
||||
</Layout>
|
||||
);
|
||||
} else if (loggedIn) {
|
||||
@@ -72,19 +79,32 @@ class LayoutContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
LayoutContainer.propTypes = {
|
||||
children: PropTypes.node,
|
||||
requestPasswordReset: PropTypes.func,
|
||||
handleLogin: PropTypes.func,
|
||||
auth: PropTypes.object,
|
||||
handleLogout: PropTypes.func,
|
||||
logout: PropTypes.func,
|
||||
toggleShortcutModal: PropTypes.func,
|
||||
TALK_RECAPTCHA_PUBLIC: PropTypes.string,
|
||||
checkLogin: PropTypes.func,
|
||||
fetchConfig: PropTypes.func
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
auth: state.auth,
|
||||
TALK_RECAPTCHA_PUBLIC: state.config.data.TALK_RECAPTCHA_PUBLIC,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
checkLogin: () => dispatch(checkLogin()),
|
||||
fetchConfig: () => dispatch(fetchConfig()),
|
||||
handleLogin: (username, password, recaptchaResponse) =>
|
||||
dispatch(handleLogin(username, password, recaptchaResponse)),
|
||||
requestPasswordReset: (email) => dispatch(requestPasswordReset(email)),
|
||||
toggleShortcutModal: (toggle) => dispatch(toggleShortcutModal(toggle)),
|
||||
handleLogout: () => dispatch(logout())
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
checkLogin,
|
||||
fetchConfig,
|
||||
handleLogin,
|
||||
requestPasswordReset,
|
||||
toggleShortcutModal,
|
||||
logout
|
||||
}, dispatch);
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer);
|
||||
|
||||
@@ -160,8 +160,10 @@ export const withUserDetailQuery = withQuery(gql`
|
||||
}) {
|
||||
...CoralAdmin_Moderation_CommentConnection
|
||||
}
|
||||
...${getDefinitionName(UserDetailComment.fragments.root)}
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
}
|
||||
${UserDetailComment.fragments.root}
|
||||
${commentConnectionFragment}
|
||||
`, {
|
||||
options: ({userId, statuses}) => {
|
||||
|
||||
@@ -3,10 +3,20 @@ import UserDetailComment from '../components/UserDetailComment';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
import {getDefinitionName} from 'coral-framework/utils';
|
||||
import CommentLabels from './CommentLabels';
|
||||
import CommentDetails from './CommentDetails';
|
||||
|
||||
export default withFragments({
|
||||
root: gql`
|
||||
fragment CoralAdmin_UserDetailComment_root on RootQuery {
|
||||
__typename
|
||||
...${getDefinitionName(CommentLabels.fragments.root)}
|
||||
...${getDefinitionName(CommentDetails.fragments.root)}
|
||||
}
|
||||
${CommentLabels.fragments.root}
|
||||
${CommentDetails.fragments.root}
|
||||
`,
|
||||
comment: gql`
|
||||
fragment CoralAdmin_UserDetail_comment on Comment {
|
||||
fragment CoralAdmin_UserDetailComment_comment on Comment {
|
||||
id
|
||||
body
|
||||
created_at
|
||||
@@ -17,28 +27,13 @@ export default withFragments({
|
||||
title
|
||||
url
|
||||
}
|
||||
action_summaries {
|
||||
count
|
||||
... on FlagActionSummary {
|
||||
reason
|
||||
}
|
||||
}
|
||||
actions {
|
||||
... on FlagAction {
|
||||
id
|
||||
reason
|
||||
message
|
||||
user {
|
||||
id
|
||||
username
|
||||
}
|
||||
}
|
||||
}
|
||||
editing {
|
||||
edited
|
||||
}
|
||||
...${getDefinitionName(CommentLabels.fragments.comment)}
|
||||
...${getDefinitionName(CommentDetails.fragments.comment)}
|
||||
}
|
||||
${CommentLabels.fragments.comment}
|
||||
${CommentDetails.fragments.comment}
|
||||
`
|
||||
})(UserDetailComment);
|
||||
|
||||
@@ -4,7 +4,7 @@ export default {
|
||||
mutations: {
|
||||
SetUserStatus: ({variables: {status, userId}}) => ({
|
||||
updateQueries: {
|
||||
TalkAdmin_FlaggedAccounts: (prev) => {
|
||||
TalkAdmin_Community: (prev) => {
|
||||
if (status !== 'APPROVED') {
|
||||
return prev;
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export default {
|
||||
}),
|
||||
RejectUsername: ({variables: {input: {id: userId}}}) => ({
|
||||
updateQueries: {
|
||||
TalkAdmin_FlaggedAccounts: (prev) => {
|
||||
TalkAdmin_Community: (prev) => {
|
||||
const updated = update(prev, {
|
||||
users: {
|
||||
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)},
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.actionButton {
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import React from 'react';
|
||||
import styles from './ActionButton.css';
|
||||
import {Button} from 'coral-ui';
|
||||
import {menuActionsMap} from '../../../utils/moderationQueueActionsMap';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
// TODO: Needs refactoring.
|
||||
const ActionButton = ({type = '', user, ...props}) => {
|
||||
return (
|
||||
<Button
|
||||
className={`${type.toLowerCase()} ${styles.actionButton}`}
|
||||
cStyle={type.toLowerCase()}
|
||||
icon={menuActionsMap[type].icon}
|
||||
onClick={() => {
|
||||
type === 'APPROVE' ? props.approveUser({userId: user.id}) : props.showRejectUsernameDialog({user: user});
|
||||
}}
|
||||
>{t(`modqueue.${menuActionsMap[type].text}`)}</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionButton;
|
||||
@@ -1,9 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const CommunityLayout = (props) => (
|
||||
<div>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default CommunityLayout;
|
||||
@@ -47,7 +47,6 @@ class FlaggedAccounts extends React.Component {
|
||||
<FlaggedUser
|
||||
user={user}
|
||||
key={user.id}
|
||||
modActionButtons={['APPROVE', 'REJECT']}
|
||||
showBanUserDialog={showBanUserDialog}
|
||||
showSuspendUserDialog={showSuspendUserDialog}
|
||||
showRejectUsernameDialog={showRejectUsernameDialog}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import styles from './FlaggedUser.css';
|
||||
|
||||
import ActionButton from './ActionButton';
|
||||
|
||||
// TODO: Should not rely on plugin.
|
||||
import {username} from 'talk-plugin-flags/helpers/flagReasons';
|
||||
|
||||
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
|
||||
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
import ApproveButton from 'coral-admin/src/components/ApproveButton';
|
||||
import RejectButton from 'coral-admin/src/components/RejectButton';
|
||||
|
||||
import cn from 'classnames';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
@@ -35,15 +35,16 @@ class User extends React.Component {
|
||||
});
|
||||
|
||||
viewAuthorDetail = () => this.props.viewUserDetail(this.props.user.id);
|
||||
showRejectUsernameDialog = () => this.props.showRejectUsernameDialog({id: this.props.user.id});
|
||||
approveUser = () => this.props.approveUser({
|
||||
userId: this.props.user.id,
|
||||
});
|
||||
|
||||
render() {
|
||||
const {
|
||||
user,
|
||||
modActionButtons,
|
||||
viewUserDetail,
|
||||
selected,
|
||||
approveUser,
|
||||
showRejectUsernameDialog,
|
||||
me,
|
||||
className,
|
||||
} = this.props;
|
||||
@@ -125,14 +126,12 @@ class User extends React.Component {
|
||||
</div>
|
||||
<div className={styles.sideActions}>
|
||||
<div className={styles.actions}>
|
||||
{modActionButtons.map((action, i) =>
|
||||
<ActionButton key={i}
|
||||
type={action.toUpperCase()}
|
||||
user={user}
|
||||
approveUser={approveUser}
|
||||
showRejectUsernameDialog={showRejectUsernameDialog}
|
||||
/>
|
||||
)}
|
||||
<ApproveButton
|
||||
onClick={this.approveUser}
|
||||
/>
|
||||
<RejectButton
|
||||
onClick={this.showRejectUsernameDialog}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,7 @@ class RejectUsernameDialog extends Component {
|
||||
const next = () => this.setState({stage: stage + 1});
|
||||
const suspend = async () => {
|
||||
try {
|
||||
await rejectUsername({id: user.user.id, message: this.state.email});
|
||||
await rejectUsername({id: user.id, message: this.state.email});
|
||||
this.props.handleClose();
|
||||
} catch (err) {
|
||||
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import withQuery from 'coral-framework/hocs/withQuery';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {getDefinitionName} from 'coral-framework/utils';
|
||||
import {withSetUserStatus, withRejectUsername} from 'coral-framework/graphql/mutations';
|
||||
import FlaggedAccounts from '../containers/FlaggedAccounts';
|
||||
import FlaggedUser from '../containers/FlaggedUser';
|
||||
|
||||
import {withSetUserStatus, withRejectUsername} from 'coral-framework/graphql/mutations';
|
||||
import {getDefinitionName} from 'coral-framework/utils';
|
||||
|
||||
import {
|
||||
fetchAccounts,
|
||||
updateSorting,
|
||||
@@ -27,7 +25,7 @@ class CommunityContainer extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Community
|
||||
return <Community
|
||||
fetchAccounts={this.props.fetchAccounts}
|
||||
community={this.props.community}
|
||||
hideRejectUsernameDialog={this.props.hideRejectUsernameDialog}
|
||||
@@ -56,28 +54,6 @@ CommunityContainer.propTypes = {
|
||||
root: PropTypes.object
|
||||
};
|
||||
|
||||
const withData = withQuery(gql`
|
||||
query TalkAdmin_FlaggedUsernamesCount {
|
||||
flaggedUsernamesCount: userCount(query: {
|
||||
action_type: FLAG,
|
||||
statuses: [PENDING]
|
||||
})
|
||||
...${getDefinitionName(FlaggedAccounts.fragments.root)}
|
||||
...${getDefinitionName(FlaggedUser.fragments.root)}
|
||||
me {
|
||||
...${getDefinitionName(FlaggedUser.fragments.me)}
|
||||
__typename
|
||||
}
|
||||
}
|
||||
${FlaggedAccounts.fragments.root}
|
||||
${FlaggedUser.fragments.root}
|
||||
${FlaggedUser.fragments.me}
|
||||
`, {
|
||||
options: {
|
||||
fetchPolicy: 'network-only',
|
||||
},
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
fetchAccounts,
|
||||
@@ -86,9 +62,31 @@ const mapDispatchToProps = (dispatch) =>
|
||||
newPage,
|
||||
}, dispatch);
|
||||
|
||||
const withData = withQuery(gql`
|
||||
query TalkAdmin_Community {
|
||||
flaggedUsernamesCount: userCount(query: {
|
||||
action_type: FLAG,
|
||||
statuses: [PENDING]
|
||||
})
|
||||
...${getDefinitionName(FlaggedAccounts.fragments.root)}
|
||||
...${getDefinitionName(FlaggedUser.fragments.root)}
|
||||
me {
|
||||
...${getDefinitionName(FlaggedUser.fragments.me)}
|
||||
__typename
|
||||
}
|
||||
}
|
||||
${FlaggedAccounts.fragments.root}
|
||||
${FlaggedUser.fragments.root}
|
||||
${FlaggedUser.fragments.me}
|
||||
`, {
|
||||
options: {
|
||||
fetchPolicy: 'network-only',
|
||||
},
|
||||
});
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withSetUserStatus,
|
||||
withRejectUsername,
|
||||
withData,
|
||||
withData
|
||||
)(CommunityContainer);
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export {default as Community} from './containers/Community';
|
||||
export {default as CommunityLayout} from './components/CommunityLayout';
|
||||
@@ -0,0 +1,181 @@
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.root {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 18px;
|
||||
width: 100%;
|
||||
max-width: 650px;
|
||||
min-width: 400px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
transition: all 200ms;
|
||||
padding: 10px 0;
|
||||
min-height: 0;
|
||||
|
||||
/*
|
||||
Fix rendering issues in Safari by promoting this
|
||||
into its own layer.
|
||||
|
||||
https://www.pivotaltracker.com/story/show/151142211
|
||||
*/
|
||||
transform: translateZ(0);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
.itemHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
}
|
||||
|
||||
.author {
|
||||
font-weight: 300;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #262626;
|
||||
font-size: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sideActions {
|
||||
height: 100%;
|
||||
top: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.itemBody {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
font-weight: 300;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-top: 0px;
|
||||
flex: 1;
|
||||
color: black;
|
||||
max-width: 500px;
|
||||
word-wrap: break-word;
|
||||
font-weight: 300;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.created {
|
||||
padding: 5px;
|
||||
color: #262626;
|
||||
font-size: 14px;
|
||||
line-height: 1px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.moderateArticle {
|
||||
font-size: 14px;
|
||||
margin: 10px 0;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
max-width: 500px;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
color: #063b9a;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
letter-spacing: .5px;
|
||||
margin-left: 10px;
|
||||
|
||||
font-size: 13px;
|
||||
margin-left: 5px;
|
||||
padding-bottom: 0px;
|
||||
border-bottom: solid 1px;
|
||||
line-height: 16px;
|
||||
|
||||
&:hover {
|
||||
opacity: .9;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
color: #393B44;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
padding: 2px 5px;
|
||||
border-radius: 2px;
|
||||
margin-left: -5px;
|
||||
transition: background-color 200ms ease;
|
||||
&:hover {
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
}
|
||||
|
||||
.external {
|
||||
font-size: .7em;
|
||||
text-decoration: none;
|
||||
color: #063b9a;
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
margin-left: 10px;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 12px;
|
||||
top: 2px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.editedMarker {
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
line-height: 1px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.adminCommentInfoBar {
|
||||
min-width: 100px;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.hasLinks {
|
||||
color: #f00;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-right: 14px;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (--big-viewport) {
|
||||
.root {
|
||||
margin-bottom: 30px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,18 @@ import PropTypes from 'prop-types';
|
||||
import {Link} from 'react-router';
|
||||
|
||||
import {Icon} from 'coral-ui';
|
||||
import FlagBox from 'coral-admin/src/components/FlagBox';
|
||||
import styles from './styles.css';
|
||||
import CommentDetails from 'coral-admin/src/components/CommentDetails';
|
||||
import styles from './Comment.css';
|
||||
import CommentLabels from 'coral-admin/src/components/CommentLabels';
|
||||
import CommentAnimatedEdit from 'coral-admin/src/components/CommentAnimatedEdit';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import {getActionSummary} from 'coral-framework/utils';
|
||||
import ActionButton from 'coral-admin/src/components/ActionButton';
|
||||
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
|
||||
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
|
||||
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
|
||||
import IfHasLink from 'coral-admin/src/components/IfHasLink';
|
||||
import cn from 'classnames';
|
||||
import ApproveButton from 'coral-admin/src/components/ApproveButton';
|
||||
import RejectButton from 'coral-admin/src/components/RejectButton';
|
||||
|
||||
import t, {timeago} from 'coral-framework/services/i18n';
|
||||
|
||||
@@ -45,11 +45,19 @@ class Comment extends React.Component {
|
||||
return viewUserDetail(comment.user.id);
|
||||
};
|
||||
|
||||
approve = () => (this.props.comment.status === 'ACCEPTED'
|
||||
? null
|
||||
: this.props.acceptComment({commentId: this.props.comment.id})
|
||||
);
|
||||
|
||||
reject = () => (this.props.comment.status === 'REJECTED'
|
||||
? null
|
||||
: this.props.rejectComment({commentId: this.props.comment.id})
|
||||
);
|
||||
|
||||
render() {
|
||||
const {
|
||||
actions = [],
|
||||
comment,
|
||||
viewUserDetail,
|
||||
suspectWords,
|
||||
bannedWords,
|
||||
selected,
|
||||
@@ -58,21 +66,16 @@ class Comment extends React.Component {
|
||||
root,
|
||||
currentUserId,
|
||||
currentAsset,
|
||||
acceptComment,
|
||||
rejectComment,
|
||||
} = this.props;
|
||||
|
||||
const flagActionSummaries = getActionSummary('FlagActionSummary', comment);
|
||||
const flagActions = comment.actions && comment.actions.filter((a) => a.__typename === 'FlagAction');
|
||||
|
||||
const selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
|
||||
|
||||
const queryData = {root, comment, asset: comment.asset};
|
||||
|
||||
return (
|
||||
<li
|
||||
tabIndex={0}
|
||||
className={cn(className, 'mdl-card', selectionStateCSS, styles.Comment, styles.listItem, {[styles.selected]: selected})}
|
||||
className={cn(className, 'mdl-card', selectionStateCSS, styles.root, {[styles.selected]: selected})}
|
||||
id={`comment_${comment.id}`}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.itemHeader}>
|
||||
@@ -152,28 +155,14 @@ class Comment extends React.Component {
|
||||
</span>
|
||||
</IfHasLink>
|
||||
<div className={`actions ${styles.actions}`}>
|
||||
{actions.map((action, i) => {
|
||||
const active =
|
||||
(action === 'REJECT' && comment.status === 'REJECTED') ||
|
||||
(action === 'APPROVE' && comment.status === 'ACCEPTED');
|
||||
return (
|
||||
<ActionButton
|
||||
key={i}
|
||||
type={action}
|
||||
user={comment.user}
|
||||
status={comment.status}
|
||||
active={active}
|
||||
acceptComment={() =>
|
||||
(comment.status === 'ACCEPTED'
|
||||
? null
|
||||
: acceptComment({commentId: comment.id}))}
|
||||
rejectComment={() =>
|
||||
(comment.status === 'REJECTED'
|
||||
? null
|
||||
: rejectComment({commentId: comment.id}))}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<ApproveButton
|
||||
active={comment.status === 'ACCEPTED'}
|
||||
onClick={this.approve}
|
||||
/>
|
||||
<RejectButton
|
||||
active={comment.status === 'REJECTED'}
|
||||
onClick={this.reject}
|
||||
/>
|
||||
</div>
|
||||
<Slot
|
||||
fill="adminSideActions"
|
||||
@@ -184,18 +173,11 @@ class Comment extends React.Component {
|
||||
</div>
|
||||
</CommentAnimatedEdit>
|
||||
</div>
|
||||
<Slot
|
||||
fill="adminCommentDetailArea"
|
||||
<CommentDetails
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
root={root}
|
||||
comment={comment}
|
||||
/>
|
||||
{flagActions && flagActions.length
|
||||
? <FlagBox
|
||||
actions={flagActions}
|
||||
actionSummaries={flagActionSummaries}
|
||||
viewUserDetail={viewUserDetail}
|
||||
/>
|
||||
: null}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -213,6 +195,8 @@ Comment.propTypes = {
|
||||
showSuspendUserDialog: PropTypes.func.isRequired,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
comment: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
body: PropTypes.string.isRequired,
|
||||
action_summaries: PropTypes.array,
|
||||
actions: PropTypes.array,
|
||||
@@ -225,8 +209,11 @@ Comment.propTypes = {
|
||||
title: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
id: PropTypes.string
|
||||
})
|
||||
})
|
||||
}),
|
||||
}),
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
selected: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Comment;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import key from 'keymaster';
|
||||
import styles from './styles.css';
|
||||
|
||||
import ModerationQueue from './ModerationQueue';
|
||||
import ModerationMenu from './ModerationMenu';
|
||||
@@ -9,12 +9,13 @@ import ModerationKeysModal from '../../../components/ModerationKeysModal';
|
||||
import StorySearch from '../containers/StorySearch';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
|
||||
export default class Moderation extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
class Moderation extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const comments = this.getComments(props);
|
||||
|
||||
this.state = {
|
||||
selectedIndex: 0
|
||||
selectedCommentId: comments[0] ? comments[0].id : null,
|
||||
};
|
||||
|
||||
}
|
||||
@@ -25,10 +26,10 @@ export default class Moderation extends Component {
|
||||
key('s', () => singleView());
|
||||
key('shift+/', () => toggleModal(true));
|
||||
key('esc', () => toggleModal(false));
|
||||
key('j', this.select(true));
|
||||
key('k', this.select(false));
|
||||
key('f', this.moderate(false));
|
||||
key('d', this.moderate(true));
|
||||
key('j', () => this.select(true));
|
||||
key('k', () => this.select(false));
|
||||
key('f', () => this.moderate(false));
|
||||
key('d', () => this.moderate(true));
|
||||
}
|
||||
|
||||
onClose = () => {
|
||||
@@ -44,39 +45,102 @@ export default class Moderation extends Component {
|
||||
this.props.toggleStorySearch(true);
|
||||
}
|
||||
|
||||
moderate = (accept) => () => {
|
||||
const {acceptComment, rejectComment} = this.props;
|
||||
const {selectedIndex} = this.state;
|
||||
const comments = this.getComments();
|
||||
const comment = comments[selectedIndex];
|
||||
const commentId = {commentId: comment.id};
|
||||
getActiveTabCount = (props = this.props) => {
|
||||
return props.root[`${props.activeTab}Count`];
|
||||
}
|
||||
|
||||
moderate = (accept) => {
|
||||
const {acceptComment, rejectComment} = this.props;
|
||||
const {selectedCommentId} = this.state;
|
||||
|
||||
const comments = this.getComments();
|
||||
const commentIdx = comments.findIndex((comment) => comment.id === selectedCommentId);
|
||||
const comment = comments[commentIdx];
|
||||
|
||||
if (accept) {
|
||||
comment.status !== 'ACCEPTED' && acceptComment(commentId);
|
||||
comment.status !== 'ACCEPTED' && acceptComment({commentId: comment.id});
|
||||
} else {
|
||||
comment.status !== 'REJECTED' && rejectComment(commentId);
|
||||
comment.status !== 'REJECTED' && rejectComment({commentId: comment.id});
|
||||
}
|
||||
}
|
||||
|
||||
getComments = () => {
|
||||
const {root, activeTab} = this.props;
|
||||
getComments = (props = this.props) => {
|
||||
const {root, activeTab} = props;
|
||||
return root[activeTab].nodes;
|
||||
}
|
||||
|
||||
select = (next) => () => {
|
||||
if (next) {
|
||||
this.setState((state) => ({
|
||||
...state,
|
||||
selectedIndex: state.selectedIndex < this.getComments().length - 1
|
||||
? state.selectedIndex + 1 : state.selectedIndex
|
||||
}));
|
||||
} else {
|
||||
this.setState((state) => ({
|
||||
...state,
|
||||
selectedIndex: state.selectedIndex > 0
|
||||
? state.selectedIndex - 1 : state.selectedIndex
|
||||
}));
|
||||
scrollTo = (toId, smooth = true) =>
|
||||
document.querySelector(`#comment_${toId}`).scrollIntoView(smooth ? {behavior: 'smooth'} : {});
|
||||
|
||||
select = async (next, props = this.props, selectedCommentId = this.state.selectedCommentId) => {
|
||||
const comments = this.getComments(props);
|
||||
|
||||
// No comments to be selected.
|
||||
if (comments.length === 0){
|
||||
return;
|
||||
}
|
||||
|
||||
// Find current index if we have a selected comment.
|
||||
const index = selectedCommentId
|
||||
? comments.findIndex((comment) => comment.id === selectedCommentId)
|
||||
: null;
|
||||
|
||||
if (next) {
|
||||
|
||||
// Grab first one if we don't have a selected comment yet.
|
||||
if (!selectedCommentId) {
|
||||
this.setState({selectedCommentId: comments[0].id}, () => this.scrollTo(comments[0].id));
|
||||
return;
|
||||
}
|
||||
|
||||
// Select next one when we still have more comments left.
|
||||
if (index < comments.length - 1) {
|
||||
this.setState({selectedCommentId: comments[index + 1].id}, () => this.scrollTo(comments[index + 1].id));
|
||||
return;
|
||||
} else {
|
||||
|
||||
// We hit the end of the list, load more comments if we have.
|
||||
if (comments.length < this.getActiveTabCount()) {
|
||||
const res = await this.loadMore();
|
||||
|
||||
// If `loadMore` was already in progress, res would be false.
|
||||
if (res) {
|
||||
|
||||
// Select next comment after loading has completed.
|
||||
this.select(true);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
|
||||
// We have no selected comment, so just skip it.
|
||||
if (!selectedCommentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we still have previous comments take the one before.
|
||||
if (index > 0) {
|
||||
this.setState({selectedCommentId: comments[index - 1].id}, () => this.scrollTo(comments[index - 1].id));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadMore = async () => {
|
||||
if (!this.isLoadingMore) {
|
||||
this.isLoadingMore = true;
|
||||
try {
|
||||
const result = await this.props.loadMore(this.props.activeTab);
|
||||
this.isLoadingMore = false;
|
||||
return result;
|
||||
}
|
||||
catch (e) {
|
||||
this.isLoadingMore = false;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -89,25 +153,56 @@ export default class Moderation extends Component {
|
||||
key.unbind('d');
|
||||
}
|
||||
|
||||
componentDidUpdate(_, prevState) {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
|
||||
// If paging through using keybaord shortcuts, scroll the page to keep the selected
|
||||
// comment in view.
|
||||
if (prevState.selectedIndex !== this.state.selectedIndex) {
|
||||
if (this.props.activeTab !== nextProps.activeTab) {
|
||||
|
||||
// the 'smooth' flag only works in FF as of March 2017
|
||||
document.querySelector(`.${styles.selected}`).scrollIntoView({behavior: 'smooth'});
|
||||
// Reset selection when changing tabs.
|
||||
this.select(true, nextProps, null);
|
||||
} else {
|
||||
|
||||
// Detect if comment has left the queue and find next or prev selected comment to set it
|
||||
// as the new selectedCommentId.
|
||||
const prevComments = this.getComments(this.props);
|
||||
const nextComments = this.getComments(nextProps);
|
||||
if (nextComments.length < prevComments.length) {
|
||||
|
||||
// Comments have changed, now check if our selected comment has left the queue.
|
||||
if (
|
||||
this.state.selectedCommentId &&
|
||||
!nextComments.some((comment) => comment.id === this.state.selectedCommentId)
|
||||
) {
|
||||
|
||||
// Determine a comment to select.
|
||||
const prevIndex = prevComments.findIndex((comment) => comment.id === this.state.selectedCommentId);
|
||||
if (prevIndex !== prevComments.length - 1) {
|
||||
this.setState({selectedCommentId: prevComments[prevIndex + 1].id});
|
||||
} else if(prevIndex > 0) {
|
||||
this.setState({selectedCommentId: prevComments[prevIndex - 1].id});
|
||||
} else {
|
||||
this.setState({selectedCommentId: null});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
|
||||
// Scroll to comment when changing from single wiew to normal view.
|
||||
if (prevProps.moderation.singleView !== this.props.moderation.singleView && this.state.selectedCommentId) {
|
||||
this.scrollTo(this.state.selectedCommentId, false);
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {root, data, moderation, settings, viewUserDetail, hideUserDetail, activeTab, getModPath, queueConfig, handleCommentChange, ...props} = this.props;
|
||||
const {root, data, moderation, settings, viewUserDetail, activeTab, getModPath, queueConfig, handleCommentChange, ...props} = this.props;
|
||||
const {asset} = root;
|
||||
const assetId = asset && asset.id;
|
||||
|
||||
const comments = root[activeTab];
|
||||
|
||||
const activeTabCount = root[`${activeTab}Count`];
|
||||
const activeTabCount = this.getActiveTabCount();
|
||||
const menuItems = Object.keys(queueConfig).map((queue) => ({
|
||||
key: queue,
|
||||
name: queueConfig[queue].name,
|
||||
@@ -132,26 +227,26 @@ export default class Moderation extends Component {
|
||||
activeTab={activeTab}
|
||||
/>
|
||||
<ModerationQueue
|
||||
key={`${activeTab}_${this.props.moderation.sortOrder}`}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
currentAsset={asset}
|
||||
comments={comments.nodes}
|
||||
activeTab={activeTab}
|
||||
singleView={moderation.singleView}
|
||||
selectedIndex={this.state.selectedIndex}
|
||||
selectedCommentId={this.state.selectedCommentId}
|
||||
bannedWords={settings.wordlist.banned}
|
||||
suspectWords={settings.wordlist.suspect}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
showSuspendUserDialog={props.showSuspendUserDialog}
|
||||
acceptComment={props.acceptComment}
|
||||
rejectComment={props.rejectComment}
|
||||
loadMore={props.loadMore}
|
||||
loadMore={this.loadMore}
|
||||
assetId={assetId}
|
||||
sort={this.props.moderation.sortOrder}
|
||||
commentCount={activeTabCount}
|
||||
currentUserId={this.props.auth.user.id}
|
||||
viewUserDetail={viewUserDetail}
|
||||
hideUserDetail={hideUserDetail}
|
||||
/>
|
||||
<ModerationKeysModal
|
||||
hideShortcutsNote={props.hideShortcutsNote}
|
||||
@@ -177,3 +272,28 @@ export default class Moderation extends Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Moderation.propTypes = {
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
toggleModal: PropTypes.func.isRequired,
|
||||
toggleStorySearch: PropTypes.func.isRequired,
|
||||
getModPath: PropTypes.func.isRequired,
|
||||
storySearchChange: PropTypes.func.isRequired,
|
||||
moderation: PropTypes.object.isRequired,
|
||||
auth: PropTypes.object.isRequired,
|
||||
settings: PropTypes.object.isRequired,
|
||||
queueConfig: PropTypes.object.isRequired,
|
||||
handleCommentChange: PropTypes.func.isRequired,
|
||||
setSortOrder: PropTypes.func.isRequired,
|
||||
showBanUserDialog: PropTypes.func.isRequired,
|
||||
showSuspendUserDialog: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
loadMore: PropTypes.func.isRequired,
|
||||
singleView: PropTypes.func.isRequired,
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default Moderation;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
.header {
|
||||
background-color: #2c2c2c;
|
||||
color: white;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.searchTrigger {
|
||||
position: relative;
|
||||
top: .2em;
|
||||
}
|
||||
|
||||
.moderateAsset {
|
||||
a {
|
||||
-webkit-box-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
color: white;
|
||||
text-transform: capitalize;
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
letter-spacing: 1px;
|
||||
transition: background-color 200ms;
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: #212121;
|
||||
}
|
||||
span {
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
max-width: 344px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Icon} from 'coral-ui';
|
||||
import styles from './styles.css';
|
||||
import styles from './ModerationHeader.css';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const ModerationHeader = ({asset, searchVisible, openSearch, closeSearch}) => {
|
||||
@@ -30,7 +30,8 @@ ModerationHeader.propTypes = {
|
||||
id: PropTypes.string
|
||||
}),
|
||||
openSearch: PropTypes.func.isRequired,
|
||||
closeSearch: PropTypes.func.isRequired
|
||||
closeSearch: PropTypes.func.isRequired,
|
||||
searchVisible: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ModerationHeader;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const ModerationLayout = (props) => (
|
||||
<div>
|
||||
@@ -6,4 +7,8 @@ const ModerationLayout = (props) => (
|
||||
</div>
|
||||
);
|
||||
|
||||
ModerationLayout.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default ModerationLayout;
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.tabBar {
|
||||
background-color: rgba(44, 44, 44, 0.89);
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tabBarPadding {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
color: #BDBDBD;
|
||||
text-transform: capitalize;
|
||||
font-weight: 100;
|
||||
font-size: 14px;
|
||||
letter-spacing: 1px;
|
||||
transition: border-bottom 200ms;
|
||||
transition: color 200ms;
|
||||
padding: 0px 10px;
|
||||
margin-right: 20px;
|
||||
&:hover {
|
||||
color: white;
|
||||
/*border-bottom: solid 2px #F36451;*/
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
color: white;
|
||||
box-sizing: border-box;
|
||||
border-bottom: solid 4px #F36451;
|
||||
font-weight: 400;
|
||||
&:hover {
|
||||
border-bottom: solid 4px #F36451;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.active > span {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.active:after {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.selectField {
|
||||
position: relative;
|
||||
width: 140px;
|
||||
height: 36px;
|
||||
top: 5px;
|
||||
margin-right: 10px;
|
||||
background: #FFF;
|
||||
padding: 10px 15px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
bor
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
|
||||
|
||||
> div {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
i {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.7px;
|
||||
font-weight: 400;
|
||||
border-bottom: 0px;
|
||||
}
|
||||
|
||||
label {
|
||||
top: -4px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.tabIcon {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
|
||||
@media (--big-viewport) {
|
||||
.tab {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import CountBadge from '../../../components/CountBadge';
|
||||
import styles from './styles.css';
|
||||
import styles from './ModerationMenu.css';
|
||||
import {SelectField, Option} from 'react-mdl-selectfield';
|
||||
import {Icon} from 'coral-ui';
|
||||
import {Link} from 'react-router';
|
||||
@@ -49,7 +49,11 @@ ModerationMenu.propTypes = {
|
||||
items: PropTypes.array.isRequired,
|
||||
asset: PropTypes.shape({
|
||||
id: PropTypes.string
|
||||
})
|
||||
}),
|
||||
selectSort: PropTypes.func.isRequired,
|
||||
sort: PropTypes.string.isRequired,
|
||||
getModPath: PropTypes.func.isRequired,
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ModerationMenu;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
.root {
|
||||
padding: 8px 0;
|
||||
list-style: none;
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.list {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.commentLeave {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.commentLeaveActive {
|
||||
opacity: 0;
|
||||
transition: opacity 800ms;
|
||||
}
|
||||
|
||||
.commentEnter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.commentEnterActive {
|
||||
opacity: 1.0;
|
||||
transition: opacity 800ms;
|
||||
}
|
||||
|
||||
@@ -2,42 +2,55 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Comment from '../containers/Comment';
|
||||
import styles from './styles.css';
|
||||
import styles from './ModerationQueue.css';
|
||||
import EmptyCard from '../../../components/EmptyCard';
|
||||
import {actionsMap} from '../../../utils/moderationQueueActionsMap';
|
||||
import LoadMore from '../../../components/LoadMore';
|
||||
import ViewMore from './ViewMore';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {CSSTransitionGroup} from 'react-transition-group';
|
||||
|
||||
const hasComment = (nodes, id) => nodes.some((node) => node.id === id);
|
||||
|
||||
// resetCursors will return the id cursors of the first and second comment of
|
||||
// the current comment list. The cursors are used to dertermine which
|
||||
// comments to show. The spare cursor functions as a backup in case one
|
||||
// of the comments gets deleted.
|
||||
function resetCursors(state, props) {
|
||||
if (props.comments && props.comments.length) {
|
||||
const idCursors = [props.comments[0].id];
|
||||
if (props.comments[1]) {
|
||||
idCursors.push(props.comments[1].id);
|
||||
}
|
||||
return {idCursors};
|
||||
}
|
||||
return {idCursors: []};
|
||||
}
|
||||
|
||||
// invalidateCursor is called whenever a comment is removed which is referenced
|
||||
// by one of the 2 id cursors. It returns a new set of id cursors calculated
|
||||
// using the help of the backup cursor.
|
||||
function invalidateCursor(invalidated, state, props) {
|
||||
const alt = invalidated === 1 ? 0 : 1;
|
||||
const idCursors = [];
|
||||
if (state.idCursors[alt]) {
|
||||
idCursors.push(state.idCursors[alt]);
|
||||
const index = props.comments.findIndex((node) => node.id === idCursors[0]);
|
||||
const nextInLine = props.comments[index + 1];
|
||||
if (nextInLine) {
|
||||
idCursors.push(nextInLine.id);
|
||||
}
|
||||
}
|
||||
return {idCursors};
|
||||
}
|
||||
|
||||
class ModerationQueue extends React.Component {
|
||||
isLoadingMore = false;
|
||||
|
||||
static propTypes = {
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
currentAsset: PropTypes.object,
|
||||
showBanUserDialog: PropTypes.func.isRequired,
|
||||
showSuspendUserDialog: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
comments: PropTypes.array.isRequired
|
||||
}
|
||||
|
||||
loadMore = () => {
|
||||
if (!this.isLoadingMore) {
|
||||
this.isLoadingMore = true;
|
||||
this.props.loadMore(this.props.activeTab)
|
||||
.then(() => this.isLoadingMore = false)
|
||||
.catch((e) => {
|
||||
this.isLoadingMore = false;
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
...resetCursors(this.state, props),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate (prev) {
|
||||
@@ -47,14 +60,67 @@ class ModerationQueue extends React.Component {
|
||||
// AND there are more comments available on the server,
|
||||
// go ahead and load more comments
|
||||
if (prev.comments.length > 0 && comments.length === 0 && commentCount > 0) {
|
||||
this.loadMore();
|
||||
this.props.loadMore();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
const {comments: prevComments} = this.props;
|
||||
const {comments: nextComments} = next;
|
||||
|
||||
if (!prevComments && nextComments) {
|
||||
this.setState(resetCursors);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
prevComments && nextComments &&
|
||||
nextComments.length < prevComments.length
|
||||
) {
|
||||
|
||||
// Invalidate first cursor if referenced comment was removed.
|
||||
if (this.state.idCursors[0] && !hasComment(nextComments, this.state.idCursors[0])) {
|
||||
this.setState(invalidateCursor(0, this.state, next));
|
||||
}
|
||||
|
||||
// Invalidate second cursor if referenced comment was removed.
|
||||
if (this.state.idCursors[1] && !hasComment(nextComments, this.state.idCursors[1])) {
|
||||
this.setState(invalidateCursor(1, this.state, next));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewNewComments = () => {
|
||||
this.setState(resetCursors);
|
||||
};
|
||||
|
||||
// getVisibileComments returns a list containing comments
|
||||
// which comes after the `idCursor`.
|
||||
getVisibleComments() {
|
||||
const {comments} = this.props;
|
||||
const idCursor = this.state.idCursors[0];
|
||||
|
||||
if (!comments) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const view = [];
|
||||
let pastCursor = false;
|
||||
comments.forEach((comment) => {
|
||||
if (comment.id === idCursor) {
|
||||
pastCursor = true;
|
||||
}
|
||||
if (pastCursor) {
|
||||
view.push(comment);
|
||||
}
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
comments,
|
||||
selectedIndex,
|
||||
selectedCommentId,
|
||||
commentCount,
|
||||
singleView,
|
||||
viewUserDetail,
|
||||
@@ -62,12 +128,51 @@ class ModerationQueue extends React.Component {
|
||||
...props
|
||||
} = this.props;
|
||||
|
||||
if (comments.length === 0) {
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<EmptyCard>{t('modqueue.empty_queue')}</EmptyCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (singleView) {
|
||||
const index = comments.findIndex((comment) => comment.id === selectedCommentId);
|
||||
const comment = comments[index];
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<Comment
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
key={comment.id}
|
||||
comment={comment}
|
||||
selected={true}
|
||||
suspectWords={props.suspectWords}
|
||||
bannedWords={props.bannedWords}
|
||||
viewUserDetail={viewUserDetail}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
showSuspendUserDialog={props.showSuspendUserDialog}
|
||||
acceptComment={props.acceptComment}
|
||||
rejectComment={props.rejectComment}
|
||||
currentAsset={props.currentAsset}
|
||||
currentUserId={this.props.currentUserId}
|
||||
/>;
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const view = this.getVisibleComments();
|
||||
|
||||
return (
|
||||
<div id="moderationList" className={`${styles.list} ${singleView ? styles.singleView : ''}`}>
|
||||
<div className={styles.root}>
|
||||
<ViewMore
|
||||
viewMore={this.viewNewComments}
|
||||
count={comments.length - view.length}
|
||||
/>
|
||||
<CSSTransitionGroup
|
||||
key={activeTab}
|
||||
component={'ul'}
|
||||
style={{paddingLeft: 0}}
|
||||
className={styles.list}
|
||||
transitionName={{
|
||||
enter: styles.commentEnter,
|
||||
enterActive: styles.commentEnterActive,
|
||||
@@ -80,36 +185,30 @@ class ModerationQueue extends React.Component {
|
||||
transitionLeaveTimeout={1000}
|
||||
>
|
||||
{
|
||||
comments.map((comment, i) => {
|
||||
const status = comment.action_summaries ? 'FLAGGED' : comment.status;
|
||||
return <Comment
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
key={comment.id}
|
||||
comment={comment}
|
||||
selected={i === selectedIndex}
|
||||
suspectWords={props.suspectWords}
|
||||
bannedWords={props.bannedWords}
|
||||
viewUserDetail={viewUserDetail}
|
||||
actions={actionsMap[status]}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
showSuspendUserDialog={props.showSuspendUserDialog}
|
||||
acceptComment={props.acceptComment}
|
||||
rejectComment={props.rejectComment}
|
||||
currentAsset={props.currentAsset}
|
||||
currentUserId={this.props.currentUserId}
|
||||
/>;
|
||||
})
|
||||
view
|
||||
.map((comment) => {
|
||||
return <Comment
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
key={comment.id}
|
||||
comment={comment}
|
||||
selected={comment.id === selectedCommentId}
|
||||
suspectWords={props.suspectWords}
|
||||
bannedWords={props.bannedWords}
|
||||
viewUserDetail={viewUserDetail}
|
||||
showBanUserDialog={props.showBanUserDialog}
|
||||
showSuspendUserDialog={props.showSuspendUserDialog}
|
||||
acceptComment={props.acceptComment}
|
||||
rejectComment={props.rejectComment}
|
||||
currentAsset={props.currentAsset}
|
||||
currentUserId={this.props.currentUserId}
|
||||
/>;
|
||||
})
|
||||
}
|
||||
</CSSTransitionGroup>
|
||||
{comments.length === 0 &&
|
||||
<div className={styles.emptyCardContainer}>
|
||||
<EmptyCard>{t('modqueue.empty_queue')}</EmptyCard>
|
||||
</div>
|
||||
}
|
||||
|
||||
<LoadMore
|
||||
loadMore={this.loadMore}
|
||||
loadMore={this.props.loadMore}
|
||||
showLoadMore={comments.length < commentCount}
|
||||
/>
|
||||
</div>
|
||||
@@ -117,4 +216,24 @@ class ModerationQueue extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
ModerationQueue.propTypes = {
|
||||
viewUserDetail: PropTypes.func.isRequired,
|
||||
bannedWords: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
suspectWords: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
currentAsset: PropTypes.object,
|
||||
showBanUserDialog: PropTypes.func.isRequired,
|
||||
showSuspendUserDialog: PropTypes.func.isRequired,
|
||||
rejectComment: PropTypes.func.isRequired,
|
||||
acceptComment: PropTypes.func.isRequired,
|
||||
comments: PropTypes.array.isRequired,
|
||||
commentCount: PropTypes.number.isRequired,
|
||||
loadMore: PropTypes.func.isRequired,
|
||||
selectedCommentId: PropTypes.string,
|
||||
singleView: PropTypes.bool,
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ModerationQueue;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
.notFound {
|
||||
position: relative;
|
||||
margin: 20px auto;
|
||||
text-align: center;
|
||||
padding: 68px 45px;
|
||||
vertical-align: middle;
|
||||
min-width: 500px;
|
||||
|
||||
a {
|
||||
color: rgb(244, 126, 107);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.goToStreams {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './styles.css';
|
||||
import styles from './NotFoundAsset.css';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const NotFound = (props) => (
|
||||
<div className={`mdl-card mdl-shadow--2dp ${styles.notFound}`}>
|
||||
@@ -11,4 +12,8 @@ const NotFound = (props) => (
|
||||
</div>
|
||||
);
|
||||
|
||||
NotFound.propTypes = {
|
||||
assetId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default NotFound;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
.story {
|
||||
padding: 7px 50px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
min-height: 50px;
|
||||
box-sizing: border-box;
|
||||
transition: background-color 400ms;
|
||||
|
||||
&:hover {
|
||||
background-color: #efefef;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.title, .meta {
|
||||
margin: 0;
|
||||
color: black;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.author, .createdAt, .status {
|
||||
font-size: 17px;
|
||||
display: inline-block;
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
.createdAt {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.author {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.createdAt {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './StorySearch.css';
|
||||
import styles from './Story.css';
|
||||
|
||||
const formatDate = (date) => {
|
||||
const d = new Date(date);
|
||||
@@ -25,7 +25,8 @@ Story.propTypes = {
|
||||
author: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
createdAt: PropTypes.string.isRequired,
|
||||
open: PropTypes.bool.isRequired
|
||||
open: PropTypes.bool.isRequired,
|
||||
goToStory: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Story;
|
||||
|
||||
@@ -67,54 +67,6 @@
|
||||
/*.storyList {
|
||||
border-top: 1px solid #ddd;
|
||||
}*/
|
||||
|
||||
.story {
|
||||
padding: 7px 50px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
min-height: 50px;
|
||||
box-sizing: border-box;
|
||||
transition: background-color 400ms;
|
||||
|
||||
&:hover {
|
||||
background-color: #efefef;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.title, .meta {
|
||||
margin: 0;
|
||||
color: black;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.author, .createdAt, .status {
|
||||
font-size: 17px;
|
||||
display: inline-block;
|
||||
font-size: .8em;
|
||||
}
|
||||
|
||||
.createdAt {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.author {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.createdAt {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.searchButton {
|
||||
width: 90px;
|
||||
height: 35px;
|
||||
|
||||
@@ -93,7 +93,13 @@ StorySearch.propTypes = {
|
||||
clearAndCloseSearch: PropTypes.func.isRequired,
|
||||
moderation: PropTypes.object.isRequired,
|
||||
handleSearchChange: PropTypes.func.isRequired,
|
||||
assetId: PropTypes.string
|
||||
assetId: PropTypes.string,
|
||||
data: PropTypes.object.isRequired,
|
||||
root: PropTypes.object.isRequired,
|
||||
handleEsc: PropTypes.func.isRequired,
|
||||
handleEnter: PropTypes.func.isRequired,
|
||||
goToModerateAll: PropTypes.func.isRequired,
|
||||
searchValue: PropTypes.string,
|
||||
};
|
||||
|
||||
export default StorySearch;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
.viewMoreContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.viewMore {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: #FFF;
|
||||
max-width: 660px;
|
||||
margin-bottom: 30px;
|
||||
background-color: #2376D8;
|
||||
cursor: pointer;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.viewMore:hover {
|
||||
background-color: #4399FF;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Button} from 'coral-ui';
|
||||
import styles from './ViewMore.css';
|
||||
import cn from 'classnames';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const ViewMore = ({viewMore, count, className, ...rest}) =>
|
||||
<div {...rest} className={cn(className, styles.viewMoreContainer)}>
|
||||
{
|
||||
count > 0 && <Button
|
||||
className={styles.viewMore}
|
||||
onClick={viewMore}>
|
||||
{count === 1
|
||||
? t('framework.new_count', count, t('framework.comment'))
|
||||
: t('framework.new_count', count, t('framework.comments'))}
|
||||
</Button>
|
||||
}
|
||||
</div>;
|
||||
|
||||
ViewMore.propTypes = {
|
||||
viewMore: PropTypes.func.isRequired,
|
||||
count: PropTypes.number.isRequired,
|
||||
className: PropTypes.string
|
||||
};
|
||||
|
||||
export default ViewMore;
|
||||
@@ -1,495 +0,0 @@
|
||||
/**
|
||||
* @TODO: deprecated as this file contains styles from multiple components. Please remove this file
|
||||
* when styles have been refactored.
|
||||
*/
|
||||
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.listContainer {
|
||||
max-width: 860px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.tabBar {
|
||||
background-color: rgba(44, 44, 44, 0.89);
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tabBarPadding {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
color: #BDBDBD;
|
||||
text-transform: capitalize;
|
||||
font-weight: 100;
|
||||
font-size: 14px;
|
||||
letter-spacing: 1px;
|
||||
transition: border-bottom 200ms;
|
||||
transition: color 200ms;
|
||||
padding: 0px 10px;
|
||||
margin-right: 20px;
|
||||
&:hover {
|
||||
color: white;
|
||||
/*border-bottom: solid 2px #F36451;*/
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
color: white;
|
||||
box-sizing: border-box;
|
||||
border-bottom: solid 4px #F36451;
|
||||
font-weight: 400;
|
||||
&:hover {
|
||||
border-bottom: solid 4px #F36451;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.active > span {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.active:after {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.showShortcuts {
|
||||
position: absolute;
|
||||
right: 130px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
|
||||
span {
|
||||
margin-left: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (--big-viewport) {
|
||||
.tab {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
.notFound {
|
||||
position: relative;
|
||||
margin: 20px auto;
|
||||
text-align: center;
|
||||
padding: 68px 45px;
|
||||
vertical-align: middle;
|
||||
min-width: 500px;
|
||||
|
||||
a {
|
||||
color: rgb(244, 126, 107);
|
||||
font-weight: 500;
|
||||
|
||||
&.goToStreams {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #2c2c2c;
|
||||
color: white;
|
||||
margin-bottom: -1px;
|
||||
|
||||
.settingsButton {
|
||||
vertical-align: middle;
|
||||
margin-left: 10px;
|
||||
margin-top: -4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.moderateAsset {
|
||||
a {
|
||||
-webkit-box-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1;
|
||||
color: white;
|
||||
text-transform: capitalize;
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
letter-spacing: 1px;
|
||||
transition: background-color 200ms;
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: #212121;
|
||||
}
|
||||
span {
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
max-width: 344px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.list {
|
||||
padding: 8px 0;
|
||||
list-style: none;
|
||||
display: block;
|
||||
|
||||
&.singleView .listItem {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.singleView .listItem.selected {
|
||||
display: block;
|
||||
height: 100%;
|
||||
font-size: 1.5em;
|
||||
line-height: 1.5em;
|
||||
border: none;
|
||||
|
||||
.actions {
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
left: 25%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 50%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.listItem {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 18px;
|
||||
width: 100%;
|
||||
max-width: 650px;
|
||||
min-width: 400px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
transition: all 200ms;
|
||||
padding: 10px 0;
|
||||
min-height: 0;
|
||||
|
||||
/*
|
||||
Fix rendering issues in Safari by promoting this
|
||||
into its own layer.
|
||||
|
||||
https://www.pivotaltracker.com/story/show/151142211
|
||||
*/
|
||||
transform: translateZ(0);
|
||||
|
||||
.container {
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
}
|
||||
|
||||
.context {
|
||||
a {
|
||||
color: #f36451;
|
||||
text-decoration: underline;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.sideActions {
|
||||
height: 100%;
|
||||
top: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.itemHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.author {
|
||||
font-weight: 300;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #262626;
|
||||
font-size: 16px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.itemBody {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-right: 16px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: #757575;
|
||||
font-size: 40px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.created {
|
||||
padding: 5px;
|
||||
color: #262626;
|
||||
font-size: 14px;
|
||||
line-height: 1px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-top: 0px;
|
||||
flex: 1;
|
||||
color: black;
|
||||
max-width: 500px;
|
||||
word-wrap: break-word;
|
||||
font-weight: 300;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.flagged {
|
||||
color: rgba(255, 0, 0, .5);
|
||||
padding-top: 15px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.flagCount{
|
||||
font-size: 12px;
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: #444;
|
||||
margin-top: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@media (--big-viewport) {
|
||||
.listItem {
|
||||
margin-bottom: 30px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
&.activeItem {
|
||||
border: 2px solid #333;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.hasLinks {
|
||||
color: #f00;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.banned {
|
||||
color: #f00;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.ban {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.Comment {
|
||||
|
||||
.moderateArticle {
|
||||
font-size: 14px;
|
||||
margin: 10px 0;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
max-width: 500px;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
color: #063b9a;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
letter-spacing: .5px;
|
||||
margin-left: 10px;
|
||||
|
||||
font-size: 13px;
|
||||
margin-left: 5px;
|
||||
padding-bottom: 0px;
|
||||
border-bottom: solid 1px;
|
||||
line-height: 16px;
|
||||
|
||||
&:hover {
|
||||
opacity: .9;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.selectField {
|
||||
position: relative;
|
||||
width: 140px;
|
||||
height: 36px;
|
||||
top: 5px;
|
||||
margin-right: 10px;
|
||||
background: #FFF;
|
||||
padding: 10px 15px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
bor
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
|
||||
|
||||
> div {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
i {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.7px;
|
||||
font-weight: 400;
|
||||
border-bottom: 0px;
|
||||
}
|
||||
|
||||
label {
|
||||
top: -4px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.tabIcon {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.username {
|
||||
color: #393B44;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
padding: 2px 5px;
|
||||
border-radius: 2px;
|
||||
margin-left: -5px;
|
||||
transition: background-color 200ms ease;
|
||||
&:hover {
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
}
|
||||
|
||||
.external {
|
||||
font-size: .7em;
|
||||
text-decoration: none;
|
||||
color: #063b9a;
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
margin-left: 10px;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 12px;
|
||||
top: 2px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.emptyCardContainer {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.commentLeave {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.commentLeaveActive {
|
||||
opacity: 0;
|
||||
transition: opacity 800ms;
|
||||
}
|
||||
|
||||
.commentEnter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.commentEnterActive {
|
||||
opacity: 1.0;
|
||||
transition: opacity 800ms;
|
||||
}
|
||||
|
||||
.editedMarker {
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
line-height: 1px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.searchTrigger {
|
||||
position: relative;
|
||||
top: .2em;
|
||||
}
|
||||
|
||||
.adminCommentInfoBar {
|
||||
min-width: 100px;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import {gql} from 'react-apollo';
|
||||
import Comment from '../components/Comment';
|
||||
import CommentLabels from '../../../containers/CommentLabels';
|
||||
import CommentDetails from '../../../containers/CommentDetails';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
import {getSlotFragmentSpreads, getDefinitionName} from 'coral-framework/utils';
|
||||
|
||||
@@ -16,8 +17,12 @@ export default withFragments({
|
||||
fragment CoralAdmin_ModerationComment_root on RootQuery {
|
||||
__typename
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
...${getDefinitionName(CommentLabels.fragments.root)}
|
||||
...${getDefinitionName(CommentDetails.fragments.root)}
|
||||
}
|
||||
`,
|
||||
${CommentLabels.fragments.root}
|
||||
${CommentDetails.fragments.root}
|
||||
`,
|
||||
comment: gql`
|
||||
fragment CoralAdmin_ModerationComment_comment on Comment {
|
||||
id
|
||||
@@ -34,33 +39,15 @@ export default withFragments({
|
||||
title
|
||||
url
|
||||
}
|
||||
action_summaries {
|
||||
count
|
||||
... on FlagActionSummary {
|
||||
reason
|
||||
__typename
|
||||
}
|
||||
}
|
||||
actions {
|
||||
... on FlagAction {
|
||||
id
|
||||
reason
|
||||
message
|
||||
user {
|
||||
id
|
||||
username
|
||||
}
|
||||
__typename
|
||||
}
|
||||
__typename
|
||||
}
|
||||
editing {
|
||||
edited
|
||||
}
|
||||
hasParent
|
||||
${getSlotFragmentSpreads(slots, 'comment')}
|
||||
...${getDefinitionName(CommentLabels.fragments.comment)}
|
||||
...${getDefinitionName(CommentDetails.fragments.comment)}
|
||||
}
|
||||
${CommentLabels.fragments.comment}
|
||||
${CommentDetails.fragments.comment}
|
||||
`
|
||||
})(Comment);
|
||||
|
||||
@@ -82,50 +82,56 @@ class ModerationContainer extends Component {
|
||||
}
|
||||
|
||||
subscribeToUpdates(variables = this.props.data.variables) {
|
||||
const sub1 = this.props.data.subscribeToMore({
|
||||
document: COMMENT_ACCEPTED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentAccepted: comment}}}) => {
|
||||
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
|
||||
const notifyText = this.props.auth.user.id === user.id
|
||||
? ''
|
||||
: t('modqueue.notify_accepted', user.username, prepareNotificationText(comment.body));
|
||||
return this.handleCommentChange(prev, comment, notifyText);
|
||||
const parameters = [
|
||||
{
|
||||
document: COMMENT_ADDED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentAdded: comment}}}) => {
|
||||
return this.handleCommentChange(prev, comment);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const sub2 = this.props.data.subscribeToMore({
|
||||
document: COMMENT_REJECTED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentRejected: comment}}}) => {
|
||||
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
|
||||
const notifyText = this.props.auth.user.id === user.id
|
||||
? ''
|
||||
: t('modqueue.notify_rejected', user.username, prepareNotificationText(comment.body));
|
||||
return this.handleCommentChange(prev, comment, notifyText);
|
||||
{
|
||||
document: COMMENT_ACCEPTED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentAccepted: comment}}}) => {
|
||||
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
|
||||
const notifyText = this.props.auth.user.id === user.id
|
||||
? ''
|
||||
: t('modqueue.notify_accepted', user.username, prepareNotificationText(comment.body));
|
||||
return this.handleCommentChange(prev, comment, notifyText);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const sub3 = this.props.data.subscribeToMore({
|
||||
document: COMMENT_EDITED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentEdited: comment}}}) => {
|
||||
const notifyText = t('modqueue.notify_edited', comment.user.username, prepareNotificationText(comment.body));
|
||||
return this.handleCommentChange(prev, comment, notifyText);
|
||||
{
|
||||
document: COMMENT_REJECTED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentRejected: comment}}}) => {
|
||||
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
|
||||
const notifyText = this.props.auth.user.id === user.id
|
||||
? ''
|
||||
: t('modqueue.notify_rejected', user.username, prepareNotificationText(comment.body));
|
||||
return this.handleCommentChange(prev, comment, notifyText);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const sub4 = this.props.data.subscribeToMore({
|
||||
document: COMMENT_FLAGGED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentFlagged: comment}}}) => {
|
||||
const user = comment.actions[comment.actions.length - 1].user;
|
||||
const notifyText = t('modqueue.notify_flagged', user.username, prepareNotificationText(comment.body));
|
||||
return this.handleCommentChange(prev, comment, notifyText);
|
||||
{
|
||||
document: COMMENT_EDITED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentEdited: comment}}}) => {
|
||||
const notifyText = t('modqueue.notify_edited', comment.user.username, prepareNotificationText(comment.body));
|
||||
return this.handleCommentChange(prev, comment, notifyText);
|
||||
},
|
||||
},
|
||||
});
|
||||
{
|
||||
document: COMMENT_FLAGGED_SUBSCRIPTION,
|
||||
variables,
|
||||
updateQuery: (prev, {subscriptionData: {data: {commentFlagged: comment}}}) => {
|
||||
const user = comment.actions[comment.actions.length - 1].user;
|
||||
const notifyText = t('modqueue.notify_flagged', user.username, prepareNotificationText(comment.body));
|
||||
return this.handleCommentChange(prev, comment, notifyText);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
this.subscriptions.push(sub1, sub2, sub3, sub4);
|
||||
this.subscriptions = parameters.map((param) => this.props.data.subscribeToMore(param));
|
||||
}
|
||||
|
||||
unsubscribe() {
|
||||
@@ -204,12 +210,9 @@ class ModerationContainer extends Component {
|
||||
// Not found.
|
||||
return <NotFoundAsset assetId={assetId} />;
|
||||
}
|
||||
if (asset === undefined || asset.id !== assetId) {
|
||||
}
|
||||
|
||||
// Still loading.
|
||||
return <Spinner />;
|
||||
}
|
||||
} else if (asset !== undefined || !('premodCount' in root)) {
|
||||
if(data.loading) {
|
||||
|
||||
// loading.
|
||||
return <Spinner />;
|
||||
@@ -240,6 +243,14 @@ class ModerationContainer extends Component {
|
||||
/>;
|
||||
}
|
||||
}
|
||||
const COMMENT_ADDED_SUBSCRIPTION = gql`
|
||||
subscription CommentAdded($asset_id: ID){
|
||||
commentAdded(asset_id: $asset_id, statuses: null){
|
||||
...${getDefinitionName(Comment.fragments.comment)}
|
||||
}
|
||||
}
|
||||
${Comment.fragments.comment}
|
||||
`;
|
||||
|
||||
const COMMENT_EDITED_SUBSCRIPTION = gql`
|
||||
subscription CommentEdited($asset_id: ID){
|
||||
@@ -352,7 +363,9 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql`
|
||||
organizationName
|
||||
moderation
|
||||
}
|
||||
...${getDefinitionName(Comment.fragments.root)}
|
||||
}
|
||||
${Comment.fragments.root}
|
||||
${commentConnectionFragment}
|
||||
`, {
|
||||
options: (props) => {
|
||||
@@ -369,29 +382,6 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql`
|
||||
},
|
||||
});
|
||||
|
||||
const withQueueCountPolling = withQuery(({queueConfig}) => gql`
|
||||
query CoralAdmin_ModerationCountPoll($asset_id: ID) {
|
||||
${Object.keys(queueConfig).map((queue) => `
|
||||
${queue}Count: commentCount(query: {
|
||||
${queueConfig[queue].statuses ? `statuses: [${queueConfig[queue].statuses.join(', ')}],` : ''}
|
||||
${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''}
|
||||
${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''}
|
||||
asset_id: $asset_id,
|
||||
})
|
||||
`)}
|
||||
}
|
||||
`, {
|
||||
options: (props) => {
|
||||
const id = getAssetId(props);
|
||||
return {
|
||||
pollInterval: 5000,
|
||||
variables: {
|
||||
asset_id: id
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
moderation: state.moderation,
|
||||
settings: state.settings,
|
||||
@@ -419,6 +409,5 @@ export default compose(
|
||||
withQueueConfig(baseQueueConfig),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withSetCommentStatus,
|
||||
withQueueCountPolling,
|
||||
withModQueueQuery,
|
||||
)(ModerationContainer);
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
export const actionsMap = {
|
||||
PREMOD: ['APPROVE', 'REJECT'],
|
||||
FLAGGED: ['APPROVE', 'REJECT'],
|
||||
REJECTED: ['APPROVE', 'REJECTED']
|
||||
};
|
||||
|
||||
export const menuActionsMap = {
|
||||
'REJECT': {status: 'REJECTED', text: 'reject', icon: 'close', key: 'f'},
|
||||
'REJECTED': {status: 'REJECTED', text: 'rejected', icon: 'close'},
|
||||
'APPROVE': {status: 'ACCEPTED', text: 'approve', icon: 'done', key: 'd'},
|
||||
'FLAGGED': {status: 'FLAGGED', text: 'flag', icon: 'flag', filter: 'Untouched'},
|
||||
'BAN': {status: 'BANNED', text: 'ban_user', icon: 'not interested'},
|
||||
'': {icon: 'done'}
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import {Button, Checkbox} from 'coral-ui';
|
||||
import QuestionBoxBuilder from './QuestionBoxBuilder';
|
||||
import cn from 'classnames';
|
||||
|
||||
import styles from './ConfigureCommentStream.css';
|
||||
|
||||
@@ -13,7 +14,7 @@ export default ({handleChange, handleApply, changed, ...props}) => (
|
||||
<h3>{t('configure.title')}</h3>
|
||||
<Button
|
||||
type="submit"
|
||||
className={styles.apply}
|
||||
className={cn(styles.apply, 'talk-embed-stream-configuration-submit-button')}
|
||||
onChange={handleChange}
|
||||
cStyle={changed ? 'green' : 'darkGrey'} >
|
||||
{t('configure.apply')}
|
||||
|
||||
@@ -102,7 +102,7 @@ class ConfigureStreamContainer extends Component {
|
||||
const closedTimeout = dirtySettings.closedTimeout;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='talk-embed-stream-configuration-container'>
|
||||
<ConfigureCommentStream
|
||||
handleChange={this.handleChange}
|
||||
handleApply={this.handleApply}
|
||||
|
||||
@@ -45,16 +45,17 @@
|
||||
border-left: 3px solid rgb(35,118,216);
|
||||
}
|
||||
|
||||
.bylineSecondary {
|
||||
display: flex;
|
||||
color: #696969;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pendingComment {
|
||||
filter: blur(2px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bylineSecondary {
|
||||
color: #696969;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.editedMarker {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import TagLabel from 'talk-plugin-tag-label/TagLabel';
|
||||
import PubDate from 'talk-plugin-pubdate/PubDate';
|
||||
import CommentTimestamp from 'coral-framework/components/CommentTimestamp';
|
||||
import {ReplyBox, ReplyButton} from 'talk-plugin-replies';
|
||||
import {FlagComment} from 'talk-plugin-flags';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
@@ -16,7 +16,7 @@ import mapValues from 'lodash/mapValues';
|
||||
import LoadMore from './LoadMore';
|
||||
import {getEditableUntilDate} from './util';
|
||||
import {findCommentWithId} from '../graphql/utils';
|
||||
import CommentContent from './CommentContent';
|
||||
import CommentContent from 'coral-framework/components/CommentContent';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import CommentTombstone from './CommentTombstone';
|
||||
import InactiveCommentLabel from './InactiveCommentLabel';
|
||||
@@ -465,7 +465,14 @@ export default class Comment extends React.Component {
|
||||
/>
|
||||
|
||||
<span className={`${styles.bylineSecondary} talk-stream-comment-user-byline`} >
|
||||
<PubDate created_at={comment.created_at} className={'talk-stream-comment-published-date'} />
|
||||
<Slot
|
||||
fill="commentTimestamp"
|
||||
defaultComponent={CommentTimestamp}
|
||||
className={'talk-stream-comment-published-date'}
|
||||
created_at={comment.created_at}
|
||||
queryData={queryData}
|
||||
{...slotProps}
|
||||
/>
|
||||
{
|
||||
(comment.editing && comment.editing.edited)
|
||||
? <span> <span className={styles.editedMarker}>({t('comment.edited')})</span></span>
|
||||
@@ -522,7 +529,7 @@ export default class Comment extends React.Component {
|
||||
<div className={cn(styles.footer, 'talk-stream-comment-footer')}>
|
||||
{isActive &&
|
||||
<div className={'talk-stream-comment-actions-container'}>
|
||||
<div className="commentActionsLeft comment__action-container">
|
||||
<div className="talk-embed-stream-comment-actions-container-left commentActionsLeft comment__action-container">
|
||||
<Slot
|
||||
fill="commentReactions"
|
||||
{...slotProps}
|
||||
@@ -538,7 +545,7 @@ export default class Comment extends React.Component {
|
||||
/>
|
||||
</ActionButton>}
|
||||
</div>
|
||||
<div className="commentActionsRight comment__action-container">
|
||||
<div className="talk-embed-stream-comment-actions-container-right commentActionsRight comment__action-container">
|
||||
<Slot
|
||||
fill="commentActions"
|
||||
wrapperComponent={ActionButton}
|
||||
|
||||
@@ -29,7 +29,7 @@ export default class Embed extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {activeTab, commentId, root, data, auth: {showSignInDialog, signInDialogFocus}, blurSignInDialog, focusSignInDialog, hideSignInDialog} = this.props;
|
||||
const {activeTab, commentId, root, data, auth: {showSignInDialog, signInDialogFocus}, blurSignInDialog, focusSignInDialog, hideSignInDialog, router: {location: {query: {parentUrl}}}} = this.props;
|
||||
const {user} = this.props.auth;
|
||||
const hasHighlightedComment = !!commentId;
|
||||
|
||||
@@ -37,7 +37,7 @@ export default class Embed extends React.Component {
|
||||
<div className={cn('talk-embed-stream', {'talk-embed-stream-highlight-comment': hasHighlightedComment})}>
|
||||
<IfSlotIsNotEmpty slot="login">
|
||||
<Popup
|
||||
href='embed/stream/login'
|
||||
href={`embed/stream/login?parentUrl=${encodeURIComponent(parentUrl)}`}
|
||||
title='Login'
|
||||
features='menubar=0,resizable=0,width=500,height=550,top=200,left=500'
|
||||
open={showSignInDialog}
|
||||
|
||||
@@ -9,11 +9,8 @@ class LoadMore extends React.Component {
|
||||
if (!count) {
|
||||
return t('framework.show_all_replies');
|
||||
}
|
||||
if (count === 1) {
|
||||
return t('framework.show_1_more_reply');
|
||||
}
|
||||
|
||||
return t('framework.show_x_more_replies', count);
|
||||
return t('framework.show_more_replies');
|
||||
}
|
||||
|
||||
render () {
|
||||
|
||||
@@ -17,7 +17,8 @@ const slots = [
|
||||
'commentReactions',
|
||||
'commentAvatar',
|
||||
'commentAuthorName',
|
||||
'commentAuthorTags'
|
||||
'commentAuthorTags',
|
||||
'commentTimestamp',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -96,6 +96,11 @@ export default {
|
||||
user {
|
||||
id
|
||||
username
|
||||
tags {
|
||||
tag {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
action_summaries {
|
||||
count
|
||||
@@ -129,7 +134,19 @@ export default {
|
||||
user: {
|
||||
__typename: 'User',
|
||||
id: auth.user.id,
|
||||
username: auth.user.username
|
||||
username: auth.user.username,
|
||||
tags: tags.map((tag) => ({
|
||||
tag: {
|
||||
name: tag,
|
||||
created_at: new Date().toISOString(),
|
||||
__typename: 'Tag'
|
||||
},
|
||||
assigned_by: {
|
||||
id: auth.user.id,
|
||||
__typename: 'User'
|
||||
},
|
||||
__typename: 'TagLink'
|
||||
})),
|
||||
},
|
||||
created_at: new Date().toISOString(),
|
||||
body,
|
||||
@@ -140,10 +157,6 @@ export default {
|
||||
created_at: new Date().toISOString(),
|
||||
__typename: 'Tag'
|
||||
},
|
||||
assigned_by: {
|
||||
id: auth.user.id,
|
||||
__typename: 'User'
|
||||
},
|
||||
__typename: 'TagLink'
|
||||
})),
|
||||
status: 'NONE',
|
||||
|
||||
@@ -85,10 +85,6 @@ body {
|
||||
|
||||
/* Info Box Styles */
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.talk-plugin-infobox-info {
|
||||
top: 0;
|
||||
@@ -235,10 +231,6 @@ button.comment__action-button[disabled],
|
||||
color: #F00;
|
||||
}
|
||||
|
||||
.talk-plugin-pubdate-text {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Flag Styles */
|
||||
|
||||
.talk-plugin-flags-popup-form {
|
||||
@@ -367,4 +359,7 @@ button.comment__action-button[disabled],
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
display: none;
|
||||
}
|
||||
|
||||
+3
-3
@@ -2,18 +2,18 @@ import React from 'react';
|
||||
|
||||
const CommentContent = ({comment}) => {
|
||||
const textbreaks = comment.body.split('\n');
|
||||
return <div className={`${name}-text`}>
|
||||
return <span className={`${name}-text`}>
|
||||
{
|
||||
textbreaks.map((line, i) => {
|
||||
return (
|
||||
<span key={i} className={`${name}-line`}>
|
||||
{line}
|
||||
<br className={`${name}-linebreak`}/>
|
||||
{i === textbreaks.length - 1 && <br className={`${name}-linebreak`}/>}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>;
|
||||
</span>;
|
||||
};
|
||||
|
||||
export default CommentContent;
|
||||
@@ -0,0 +1,37 @@
|
||||
.root {
|
||||
}
|
||||
|
||||
.headerContainer {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
vertical-align: middle;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.info {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.details {
|
||||
padding: 0 20px 16px;
|
||||
font-size: 12px;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
vertical-align: middle;
|
||||
font-size: 12px;
|
||||
margin-right: 7px;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import styles from './CommentDetail.css';
|
||||
import {Icon} from 'coral-ui';
|
||||
|
||||
const CommentDetail = ({icon, header, info, children, className}) => {
|
||||
return (
|
||||
<div className={cn(className, styles.root)}>
|
||||
<div className={styles.headerContainer}>
|
||||
{icon && <Icon className={styles.icon} name={icon}/>}
|
||||
<h3 className={styles.header}>{header}:</h3>
|
||||
<div className={styles.info}>{info}</div>
|
||||
</div>
|
||||
{children &&
|
||||
<div className={styles.details}>
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CommentDetail.propTypes = {
|
||||
className: PropTypes.string,
|
||||
header: PropTypes.node,
|
||||
icon: PropTypes.string,
|
||||
info: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default CommentDetail;
|
||||
@@ -0,0 +1,6 @@
|
||||
.timestamp {
|
||||
display: inline-block;
|
||||
color: #696969;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
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';
|
||||
|
||||
const CommentTimestamp = ({className, created_at}) => (
|
||||
<div className={cn(className, styles.timestamp, 'talk-comment-timestamp')}>
|
||||
{timeago(created_at)}
|
||||
</div>
|
||||
);
|
||||
|
||||
CommentTimestamp.propTypes = {
|
||||
className: PropTypes.string,
|
||||
created_at: PropTypes.string,
|
||||
};
|
||||
|
||||
export default CommentTimestamp;
|
||||
@@ -23,8 +23,9 @@ class IfSlotIsEmpty extends React.Component {
|
||||
}
|
||||
|
||||
isSlotEmpty(props = this.props) {
|
||||
const {slot, className: _a, reduxState, component: _b = 'div', children: _c, ...rest} = props;
|
||||
return this.context.plugins.isSlotEmpty(slot, reduxState, rest);
|
||||
const {slot, className: _a, reduxState, component: _b = 'div', children: _c, queryData, ...rest} = props;
|
||||
const slots = Array.isArray(slot) ? slot : [slot];
|
||||
return slots.every((slot) => this.context.plugins.isSlotEmpty(slot, reduxState, rest, queryData));
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -34,7 +35,7 @@ class IfSlotIsEmpty extends React.Component {
|
||||
}
|
||||
|
||||
IfSlotIsEmpty.propTypes = {
|
||||
slot: PropTypes.string,
|
||||
slot: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
|
||||
@@ -23,8 +23,9 @@ class IfSlotIsNotEmpty extends React.Component {
|
||||
}
|
||||
|
||||
isSlotEmpty(props = this.props) {
|
||||
const {slot, className: _a, reduxState, component: _b = 'div', children: _c, ...rest} = props;
|
||||
return this.context.plugins.isSlotEmpty(slot, reduxState, rest);
|
||||
const {slot, className: _a, reduxState, component: _b = 'div', children: _c, queryData, ...rest} = props;
|
||||
const slots = Array.isArray(slot) ? slot : [slot];
|
||||
return slots.every((slot) => this.context.plugins.isSlotEmpty(slot, reduxState, rest, queryData));
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -34,7 +35,7 @@ class IfSlotIsNotEmpty extends React.Component {
|
||||
}
|
||||
|
||||
IfSlotIsNotEmpty.propTypes = {
|
||||
slot: PropTypes.string,
|
||||
slot: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
export default (condition) => (BaseComponent) => {
|
||||
BaseComponent.isExcluded = condition;
|
||||
return BaseComponent;
|
||||
};
|
||||
import React from 'react';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
|
||||
/**
|
||||
* ExcludeIf provides a property `emit: (eventName, value)`
|
||||
* to the wrapped component.
|
||||
*/
|
||||
export default (condition) => hoistStatics((WrappedComponent) => {
|
||||
class ExcludeIf extends React.Component {
|
||||
render() {
|
||||
return <WrappedComponent
|
||||
{...this.props}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
WrappedComponent.isExcluded = condition;
|
||||
return ExcludeIf;
|
||||
});
|
||||
|
||||
@@ -71,7 +71,7 @@ class ProfileContainer extends Component {
|
||||
const emailAddress = localProfile && localProfile.id;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='talk-embed-stream-profile-container'>
|
||||
<h2>{user.username}</h2>
|
||||
{emailAddress ? <p>{emailAddress}</p> : null}
|
||||
|
||||
@@ -95,8 +95,9 @@ class ProfileContainer extends Component {
|
||||
const slots = [
|
||||
'profileSections',
|
||||
|
||||
// TODO: This Slot should be included in `talk-plugin-history` instead.
|
||||
// TODO: These Slots should be included in `talk-plugin-history` instead.
|
||||
'commentContent',
|
||||
'historyCommentTimestamp',
|
||||
];
|
||||
|
||||
const CommentFragment = gql`
|
||||
|
||||
@@ -130,59 +130,6 @@
|
||||
background: #00a291;
|
||||
}
|
||||
|
||||
.type--approve {
|
||||
display: block;
|
||||
color: #519954;
|
||||
border: solid 2px rgba(81, 153, 84, 0.75);
|
||||
background: white;
|
||||
padding: 10px 12px;
|
||||
box-sizing: border-box;
|
||||
vertical-align: middle;
|
||||
line-height: 24px;
|
||||
font-size: 17px;
|
||||
height: 47px;
|
||||
border-radius: 3px;
|
||||
text-transform: capitalize;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
|
||||
width: 129px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
color: white;
|
||||
background-color: #519954;
|
||||
}
|
||||
}
|
||||
|
||||
.type--reject, .type--rejected {
|
||||
display: block;
|
||||
color: #D03235;
|
||||
border: solid 1px #D03235;
|
||||
background: white;
|
||||
padding: 10px 11px;
|
||||
box-sizing: border-box;
|
||||
vertical-align: middle;
|
||||
line-height: 24px;
|
||||
font-size: 17px;
|
||||
height: 47px;
|
||||
border-radius: 3px;
|
||||
text-transform: capitalize;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.03), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.09);
|
||||
width: 129px;
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: #D03235;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.type--rejected {
|
||||
color: white;
|
||||
background-color: #D03235;
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.type--ban, .type--actions {
|
||||
display: block;
|
||||
color: #616161;
|
||||
|
||||
@@ -49,27 +49,32 @@
|
||||
|
||||
li {
|
||||
margin-bottom: 5px;
|
||||
|
||||
&:nth-child(1) {
|
||||
color: #5394D7;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
color: #909090;
|
||||
}
|
||||
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.viewLink {
|
||||
color: #5394D7;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.iconView {
|
||||
margin-right: 5px;
|
||||
font-size: 15px;
|
||||
vertical-align: middle;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
.iconDate {
|
||||
margin-right: 5px;
|
||||
font-size: 15px;
|
||||
color: #696969;
|
||||
vertical-align: middle;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
@custom-media --mobile-viewport (max-width: 480px);
|
||||
|
||||
@media (--mobile-viewport) {
|
||||
@@ -92,7 +97,3 @@
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.iconView, .iconDate {
|
||||
vertical-align: middle;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import PropTypes from 'prop-types';
|
||||
import {Icon} from '../coral-ui';
|
||||
import styles from './Comment.css';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import PubDate from '../talk-plugin-pubdate/PubDate';
|
||||
import CommentContent from '../coral-embed-stream/src/components/CommentContent';
|
||||
import CommentTimestamp from 'coral-framework/components/CommentTimestamp';
|
||||
import CommentContent from 'coral-framework/components/CommentContent';
|
||||
import cn from 'classnames';
|
||||
import {getTotalReactionsCount} from 'coral-framework/utils';
|
||||
|
||||
@@ -15,6 +15,7 @@ class Comment extends React.Component {
|
||||
render() {
|
||||
const {comment, link, data, root} = this.props;
|
||||
const reactionCount = getTotalReactionsCount(comment.action_summaries);
|
||||
const queryData = {root, comment, asset: comment.asset};
|
||||
|
||||
return (
|
||||
<div className={styles.myComment}>
|
||||
@@ -24,7 +25,7 @@ class Comment extends React.Component {
|
||||
defaultComponent={CommentContent}
|
||||
className={cn(styles.commentBody, 'my-comment-body')}
|
||||
data={data}
|
||||
queryData={{root, comment, asset: comment.asset}}
|
||||
queryData={queryData}
|
||||
/>
|
||||
<div className={cn(styles.commentSummary, 'comment-summary')}>
|
||||
<span className={cn(styles.commentSummaryReactions, 'comment-summary-reactions', {[styles.countZero]: reactionCount === 0})}>
|
||||
@@ -37,7 +38,7 @@ class Comment extends React.Component {
|
||||
<span className={cn('comment-summary-replies', {[styles.countZero]: comment.replyCount === 0})}>
|
||||
<Icon name="reply" />
|
||||
<span className={cn(styles.replyCount, 'comment-summary-reply-count')}>
|
||||
{comment.replyCount}
|
||||
{comment.replyCount}
|
||||
</span>
|
||||
{comment.replyCount === 1 ? t('common.reply') : t('common.replies')}
|
||||
</span>
|
||||
@@ -53,15 +54,20 @@ class Comment extends React.Component {
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
<li>
|
||||
<a onClick={link(`${comment.asset.url}?commentId=${comment.id}`)}>
|
||||
<a onClick={link(`${comment.asset.url}?commentId=${comment.id}`)} className={styles.viewLink}>
|
||||
<Icon name="open_in_new" className={styles.iconView}/>{t('view_conversation')}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<Icon name="schedule" className={styles.iconDate} />
|
||||
<PubDate
|
||||
className={styles.pubdate}
|
||||
<Slot
|
||||
fill="historyCommentTimestamp"
|
||||
defaultComponent={CommentTimestamp}
|
||||
className={'talk-history-comment-published-date'}
|
||||
created_at={comment.created_at}
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
inline
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './styles.css';
|
||||
import cn from 'classnames';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import {BASE_PATH} from 'coral-framework/constants/url';
|
||||
|
||||
const ModerationLink = (props) => props.isAdmin ? (
|
||||
<div className={styles.moderationLink}>
|
||||
<a href={`${BASE_PATH}admin/moderate/${props.assetId}`} target="_blank">
|
||||
<div className={cn(styles.moderationLink, 'talk-embed-stream-moderation-container')}>
|
||||
<a className='talk-embed-stream-moderation-link' href={`${BASE_PATH}admin/moderate/${props.assetId}`} target="_blank">
|
||||
{t('moderate_this_stream')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from 'react';
|
||||
import {timeago} from 'coral-framework/services/i18n';
|
||||
|
||||
const name = 'talk-plugin-pubdate';
|
||||
|
||||
const PubDate = ({created_at}) => <div className={`${name}-text`}>
|
||||
{timeago(created_at)}
|
||||
</div>;
|
||||
|
||||
export default PubDate;
|
||||
@@ -94,14 +94,17 @@ const CONFIG = {
|
||||
MONGO_URL: process.env.TALK_MONGO_URL,
|
||||
REDIS_URL: process.env.TALK_REDIS_URL,
|
||||
|
||||
// REDIS_RECONNECTION_MAX_ATTEMPTS is the amount of attempts that a redis
|
||||
// connection will attempt to reconnect before aborting with an error.
|
||||
REDIS_RECONNECTION_MAX_ATTEMPTS: parseInt(process.env.TALK_REDIS_RECONNECTION_MAX_ATTEMPTS || '100'),
|
||||
// REDIS_CLIENT_CONFIG is the optional configuration that is merged with the
|
||||
// function config to provide deep control of the redis connection beheviour.
|
||||
REDIS_CLIENT_CONFIG: process.env.TALK_REDIS_CLIENT_CONFIGURATION || '{}',
|
||||
|
||||
// REDIS_RECONNECTION_MAX_RETRY_TIME is the time in string format for the
|
||||
// maximum amount of time that a client can be considered "connecting" before
|
||||
// attempts at reconnection are aborted with an error.
|
||||
REDIS_RECONNECTION_MAX_RETRY_TIME: ms(process.env.TALK_REDIS_RECONNECTION_MAX_RETRY_TIME || '1 min'),
|
||||
// REDIS_CLUSTER_MODE allows configuration on the type of cluster mode enabled
|
||||
// on the redis client. Can be either `NONE` or `CLUSTER`.
|
||||
REDIS_CLUSTER_MODE: process.env.TALK_REDIS_CLUSTER_MODE || 'NONE',
|
||||
|
||||
// REDIS_CLUSTER_CONFIGURATION contains the json string for the redis cluster
|
||||
// configuration.
|
||||
REDIS_CLUSTER_CONFIGURATION: process.env.TALK_REDIS_CLUSTER_CONFIGURATION || '[]',
|
||||
|
||||
// REDIS_RECONNECTION_BACKOFF_FACTOR is the factor that will be multiplied
|
||||
// against the current attempt count inbetween attempts to connect to redis.
|
||||
@@ -246,6 +249,26 @@ if (process.env.NODE_ENV === 'test' && !CONFIG.REDIS_URL) {
|
||||
CONFIG.REDIS_URL = 'redis://localhost/1';
|
||||
}
|
||||
|
||||
// REDIS_CLUSTER_CONFIGURATION should be parsed when the cluster mode !== none.
|
||||
if (CONFIG.REDIS_CLUSTER_MODE === 'CLUSTER') {
|
||||
try {
|
||||
CONFIG.REDIS_CLUSTER_CONFIGURATION = JSON.parse(CONFIG.REDIS_CLUSTER_CONFIGURATION);
|
||||
} catch (err) {
|
||||
throw new Error('TALK_REDIS_CLUSTER_CONFIGURATION is not valid JSON, see https://github.com/luin/ioredis#cluster for valid syntax of the list of cluster nodes');
|
||||
}
|
||||
|
||||
if (!Array.isArray(CONFIG.REDIS_CLUSTER_CONFIGURATION)) {
|
||||
throw new Error('TALK_REDIS_CLUSTER_MODE is CLUSTER, but the TALK_REDIS_CLUSTER_CONFIGURATION is invalid, see https://github.com/luin/ioredis#cluster for valid syntax of the list of cluster nodes');
|
||||
}
|
||||
|
||||
if (CONFIG.REDIS_CLUSTER_CONFIGURATION.length === 0) {
|
||||
throw new Error('TALK_REDIS_CLUSTER_CONFIGURATION must have at least one node specified in the cluster, see https://github.com/luin/ioredis#cluster for valid syntax of the list of cluster nodes');
|
||||
}
|
||||
}
|
||||
|
||||
// Client config is a JSON encoded string, defaulting to `{}`.
|
||||
CONFIG.REDIS_CLIENT_CONFIG = JSON.parse(CONFIG.REDIS_CLIENT_CONFIG);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Recaptcha configuration
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
+2
-5
@@ -1,6 +1,3 @@
|
||||
_site/
|
||||
.sass-cache/
|
||||
_site
|
||||
.sass-cache
|
||||
.jekyll-metadata
|
||||
_pdf
|
||||
.idea/
|
||||
.DS_Store
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
layout: doc
|
||||
---
|
||||
|
||||
<style type="text/css" media="screen">
|
||||
.container {
|
||||
margin: 10px auto;
|
||||
}
|
||||
h1 {
|
||||
margin: 30px 0;
|
||||
font-size: 4em;
|
||||
line-height: 1;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container">
|
||||
<h1>404</h1>
|
||||
|
||||
<p><strong>Page not found :(</strong></p>
|
||||
<p>The requested page could not be found. Try searching above for your page.</p>
|
||||
</div>
|
||||
Executable → Regular
+20
-7
@@ -1,11 +1,24 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "github-pages", group: :jekyll_plugins
|
||||
# Hello! This is where you manage which Jekyll version is used to run.
|
||||
# When you want to use a different version, change it below, save the
|
||||
# file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
|
||||
#
|
||||
# bundle exec jekyll serve
|
||||
#
|
||||
# This will help ensure the proper Jekyll version is running.
|
||||
# Happy Jekylling!
|
||||
gem "jekyll", "3.5.2"
|
||||
|
||||
# If you want to use GitHub Pages, remove the "gem "jekyll"" above and
|
||||
# uncomment the line below. To upgrade, run `bundle update github-pages`.
|
||||
# gem "github-pages", group: :jekyll_plugins
|
||||
|
||||
# If you have any plugins, put them here!
|
||||
group :jekyll_plugins do
|
||||
gem "jekyll-paginate"
|
||||
gem "jekyll-sitemap"
|
||||
gem "jekyll-gist"
|
||||
gem "jekyll-feed"
|
||||
gem "jemoji"
|
||||
end
|
||||
gem "jekyll-seo-tag", "~> 2.1"
|
||||
end
|
||||
|
||||
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
|
||||
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
|
||||
|
||||
|
||||
+25
-167
@@ -1,176 +1,45 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
activesupport (4.2.8)
|
||||
i18n (~> 0.7)
|
||||
minitest (~> 5.1)
|
||||
thread_safe (~> 0.3, >= 0.3.4)
|
||||
tzinfo (~> 1.1)
|
||||
addressable (2.5.1)
|
||||
public_suffix (~> 2.0, >= 2.0.2)
|
||||
coffee-script (2.4.1)
|
||||
coffee-script-source
|
||||
execjs
|
||||
coffee-script-source (1.11.1)
|
||||
addressable (2.5.2)
|
||||
public_suffix (>= 2.0.2, < 4.0)
|
||||
colorator (1.1.0)
|
||||
ethon (0.10.1)
|
||||
ffi (>= 1.3.0)
|
||||
cssminify2 (2.0.1)
|
||||
execjs (2.7.0)
|
||||
faraday (0.12.1)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
ffi (1.9.18)
|
||||
forwardable-extended (2.6.0)
|
||||
gemoji (3.0.0)
|
||||
github-pages (145)
|
||||
activesupport (= 4.2.8)
|
||||
github-pages-health-check (= 1.3.4)
|
||||
jekyll (= 3.4.5)
|
||||
jekyll-avatar (= 0.4.2)
|
||||
jekyll-coffeescript (= 1.0.1)
|
||||
jekyll-default-layout (= 0.1.4)
|
||||
jekyll-feed (= 0.9.2)
|
||||
jekyll-gist (= 1.4.0)
|
||||
jekyll-github-metadata (= 2.5.1)
|
||||
jekyll-mentions (= 1.2.0)
|
||||
jekyll-optional-front-matter (= 0.2.0)
|
||||
jekyll-paginate (= 1.1.0)
|
||||
jekyll-readme-index (= 0.1.0)
|
||||
jekyll-redirect-from (= 0.12.1)
|
||||
jekyll-relative-links (= 0.4.1)
|
||||
jekyll-sass-converter (= 1.5.0)
|
||||
jekyll-seo-tag (= 2.2.3)
|
||||
jekyll-sitemap (= 1.0.0)
|
||||
jekyll-swiss (= 0.4.0)
|
||||
jekyll-theme-architect (= 0.0.4)
|
||||
jekyll-theme-cayman (= 0.0.4)
|
||||
jekyll-theme-dinky (= 0.0.4)
|
||||
jekyll-theme-hacker (= 0.0.4)
|
||||
jekyll-theme-leap-day (= 0.0.4)
|
||||
jekyll-theme-merlot (= 0.0.4)
|
||||
jekyll-theme-midnight (= 0.0.4)
|
||||
jekyll-theme-minimal (= 0.0.4)
|
||||
jekyll-theme-modernist (= 0.0.4)
|
||||
jekyll-theme-primer (= 0.3.1)
|
||||
jekyll-theme-slate (= 0.0.4)
|
||||
jekyll-theme-tactile (= 0.0.4)
|
||||
jekyll-theme-time-machine (= 0.0.4)
|
||||
jekyll-titles-from-headings (= 0.2.0)
|
||||
jemoji (= 0.8.0)
|
||||
kramdown (= 1.13.2)
|
||||
liquid (= 3.0.6)
|
||||
listen (= 3.0.6)
|
||||
mercenary (~> 0.3)
|
||||
minima (= 2.1.1)
|
||||
rouge (= 1.11.1)
|
||||
terminal-table (~> 1.4)
|
||||
github-pages-health-check (1.3.4)
|
||||
addressable (~> 2.3)
|
||||
net-dns (~> 0.8)
|
||||
octokit (~> 4.0)
|
||||
public_suffix (~> 2.0)
|
||||
typhoeus (~> 0.7)
|
||||
html-pipeline (2.6.0)
|
||||
activesupport (>= 2)
|
||||
nokogiri (>= 1.4)
|
||||
i18n (0.8.6)
|
||||
jekyll (3.4.5)
|
||||
htmlcompressor (0.3.1)
|
||||
jekyll (3.5.2)
|
||||
addressable (~> 2.4)
|
||||
colorator (~> 1.0)
|
||||
jekyll-sass-converter (~> 1.0)
|
||||
jekyll-watch (~> 1.1)
|
||||
kramdown (~> 1.3)
|
||||
liquid (~> 3.0)
|
||||
liquid (~> 4.0)
|
||||
mercenary (~> 0.3.3)
|
||||
pathutil (~> 0.9)
|
||||
rouge (~> 1.7)
|
||||
safe_yaml (~> 1.0)
|
||||
jekyll-avatar (0.4.2)
|
||||
jekyll (~> 3.0)
|
||||
jekyll-coffeescript (1.0.1)
|
||||
coffee-script (~> 2.2)
|
||||
jekyll-default-layout (0.1.4)
|
||||
jekyll (~> 3.0)
|
||||
jekyll-feed (0.9.2)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-gist (1.4.0)
|
||||
octokit (~> 4.2)
|
||||
jekyll-github-metadata (2.5.1)
|
||||
jekyll (~> 3.1)
|
||||
octokit (~> 4.0, != 4.4.0)
|
||||
jekyll-mentions (1.2.0)
|
||||
activesupport (~> 4.0)
|
||||
html-pipeline (~> 2.3)
|
||||
jekyll (~> 3.0)
|
||||
jekyll-optional-front-matter (0.2.0)
|
||||
jekyll (~> 3.0)
|
||||
jekyll-paginate (1.1.0)
|
||||
jekyll-readme-index (0.1.0)
|
||||
jekyll (~> 3.0)
|
||||
jekyll-redirect-from (0.12.1)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-relative-links (0.4.1)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-minifier (0.1.4)
|
||||
cssminify2 (~> 2.0)
|
||||
htmlcompressor (~> 0.3)
|
||||
jekyll (~> 3.5)
|
||||
uglifier (~> 3.2)
|
||||
jekyll-sass-converter (1.5.0)
|
||||
sass (~> 3.4)
|
||||
jekyll-seo-tag (2.2.3)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-sitemap (1.0.0)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-swiss (0.4.0)
|
||||
jekyll-theme-architect (0.0.4)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-theme-cayman (0.0.4)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-theme-dinky (0.0.4)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-theme-hacker (0.0.4)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-theme-leap-day (0.0.4)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-theme-merlot (0.0.4)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-theme-midnight (0.0.4)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-theme-minimal (0.0.4)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-theme-modernist (0.0.4)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-theme-primer (0.3.1)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-theme-slate (0.0.4)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-theme-tactile (0.0.4)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-theme-time-machine (0.0.4)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-titles-from-headings (0.2.0)
|
||||
jekyll-seo-tag (2.3.0)
|
||||
jekyll (~> 3.3)
|
||||
jekyll-watch (1.5.0)
|
||||
listen (~> 3.0, < 3.1)
|
||||
jemoji (0.8.0)
|
||||
activesupport (~> 4.0)
|
||||
gemoji (~> 3.0)
|
||||
html-pipeline (~> 2.2)
|
||||
jekyll (>= 3.0)
|
||||
kramdown (1.13.2)
|
||||
liquid (3.0.6)
|
||||
listen (3.0.6)
|
||||
rb-fsevent (>= 0.9.3)
|
||||
rb-inotify (>= 0.9.7)
|
||||
kramdown (1.15.0)
|
||||
liquid (4.0.0)
|
||||
listen (3.0.8)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
mercenary (0.3.6)
|
||||
mini_portile2 (2.2.0)
|
||||
minima (2.1.1)
|
||||
jekyll (~> 3.3)
|
||||
minitest (5.10.2)
|
||||
multipart-post (2.0.0)
|
||||
net-dns (0.8.0)
|
||||
nokogiri (1.8.0)
|
||||
mini_portile2 (~> 2.2.0)
|
||||
octokit (4.7.0)
|
||||
sawyer (~> 0.8.0, >= 0.5.3)
|
||||
pathutil (0.14.0)
|
||||
forwardable-extended (~> 2.6)
|
||||
public_suffix (2.0.5)
|
||||
public_suffix (3.0.0)
|
||||
rb-fsevent (0.10.2)
|
||||
rb-inotify (0.9.10)
|
||||
ffi (>= 0.5.0, < 2)
|
||||
@@ -181,28 +50,17 @@ GEM
|
||||
sass-listen (4.0.0)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
sawyer (0.8.1)
|
||||
addressable (>= 2.3.5, < 2.6)
|
||||
faraday (~> 0.8, < 1.0)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
thread_safe (0.3.6)
|
||||
typhoeus (0.8.0)
|
||||
ethon (>= 0.8.0)
|
||||
tzinfo (1.2.3)
|
||||
thread_safe (~> 0.1)
|
||||
unicode-display_width (1.3.0)
|
||||
uglifier (3.2.0)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
github-pages
|
||||
jekyll-feed
|
||||
jekyll-gist
|
||||
jekyll-paginate
|
||||
jekyll-sitemap
|
||||
jemoji
|
||||
jekyll (= 3.5.2)
|
||||
jekyll-minifier (~> 0.1.4)
|
||||
jekyll-seo-tag (~> 2.1)
|
||||
tzinfo-data
|
||||
|
||||
BUNDLED WITH
|
||||
1.15.2
|
||||
1.15.4
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017 Michael Rose
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,75 +0,0 @@
|
||||
require "bundler/gem_tasks"
|
||||
require "jekyll"
|
||||
require "listen"
|
||||
|
||||
def listen_ignore_paths(base, options)
|
||||
[
|
||||
/_config\.ya?ml/,
|
||||
/_site/,
|
||||
/\.jekyll-metadata/
|
||||
]
|
||||
end
|
||||
|
||||
def listen_handler(base, options)
|
||||
site = Jekyll::Site.new(options)
|
||||
Jekyll::Command.process_site(site)
|
||||
proc do |modified, added, removed|
|
||||
t = Time.now
|
||||
c = modified + added + removed
|
||||
n = c.length
|
||||
relative_paths = c.map{ |p| Pathname.new(p).relative_path_from(base).to_s }
|
||||
print Jekyll.logger.message("Regenerating:", "#{relative_paths.join(", ")} changed... ")
|
||||
begin
|
||||
Jekyll::Command.process_site(site)
|
||||
puts "regenerated in #{Time.now - t} seconds."
|
||||
rescue => e
|
||||
puts "error:"
|
||||
Jekyll.logger.warn "Error:", e.message
|
||||
Jekyll.logger.warn "Error:", "Run jekyll build --trace for more information."
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
task :preview do
|
||||
base = Pathname.new('.').expand_path
|
||||
options = {
|
||||
"source" => base.join('test').to_s,
|
||||
"destination" => base.join('test/_site').to_s,
|
||||
"force_polling" => false,
|
||||
"serving" => true,
|
||||
"theme" => "minimal-mistakes-jekyll"
|
||||
}
|
||||
|
||||
options = Jekyll.configuration(options)
|
||||
|
||||
ENV["LISTEN_GEM_DEBUGGING"] = "1"
|
||||
listener = Listen.to(
|
||||
base.join("_includes"),
|
||||
base.join("_layouts"),
|
||||
base.join("_sass"),
|
||||
base.join("assets"),
|
||||
options["source"],
|
||||
:ignore => listen_ignore_paths(base, options),
|
||||
:force_polling => options['force_polling'],
|
||||
&(listen_handler(base, options))
|
||||
)
|
||||
|
||||
begin
|
||||
listener.start
|
||||
Jekyll.logger.info "Auto-regeneration:", "enabled for '#{options["source"]}'"
|
||||
|
||||
unless options['serving']
|
||||
trap("INT") do
|
||||
listener.stop
|
||||
puts " Halting auto-regeneration."
|
||||
exit 0
|
||||
end
|
||||
|
||||
loop { sleep 1000 }
|
||||
end
|
||||
rescue ThreadError
|
||||
# You pressed Ctrl-C, oh my!
|
||||
end
|
||||
|
||||
Jekyll::Commands::Serve.process(options)
|
||||
end
|
||||
Executable → Regular
+43
-226
@@ -1,240 +1,63 @@
|
||||
# Welcome to Jekyll!
|
||||
#
|
||||
# This config file is meant for settings that affect your entire site, values
|
||||
# which you are expected to set up once and rarely need to edit after that.
|
||||
# This config file is meant for settings that affect your whole blog, values
|
||||
# which you are expected to set up once and rarely edit after that. If you find
|
||||
# yourself editing this file very often, consider using Jekyll's data files
|
||||
# feature for the data you need to update frequently.
|
||||
#
|
||||
# For technical reasons, this file is *NOT* reloaded automatically when you use
|
||||
# `jekyll serve`. If you change this file, please restart the server process.
|
||||
# 'bundle exec jekyll serve'. If you change this file, please restart the server process.
|
||||
|
||||
# Site Settings
|
||||
locale : "en"
|
||||
title : "Coral Talk Documentation"
|
||||
title_separator : "-"
|
||||
name : "The Coral Project"
|
||||
description : "Documentation and guides for Coral Talk."
|
||||
url : https://coralproject.github.io # the base hostname & protocol for your site e.g. "https://mmistakes.github.io"
|
||||
baseurl : /talk # the subpath of your site, e.g. "/blog"
|
||||
repository : "coralproject/talk" # GitHub username/repo-name e.g. "mmistakes/minimal-mistakes"
|
||||
teaser : # path of fallback teaser image, e.g. "/assets/images/500x300.png"
|
||||
# breadcrumbs : false # true, false (default)
|
||||
words_per_minute : 200
|
||||
comments:
|
||||
provider : # false (default), "disqus", "discourse", "facebook", "google-plus", "staticman", "custom"
|
||||
disqus:
|
||||
shortname : # https://help.disqus.com/customer/portal/articles/466208-what-s-a-shortname-
|
||||
discourse:
|
||||
server : # https://meta.discourse.org/t/embedding-discourse-comments-via-javascript/31963 , e.g.: meta.discourse.org
|
||||
facebook:
|
||||
# https://developers.facebook.com/docs/plugins/comments
|
||||
appid :
|
||||
num_posts : # 5 (default)
|
||||
colorscheme : # "light" (default), "dark"
|
||||
staticman:
|
||||
allowedFields : ['name', 'email', 'url', 'message']
|
||||
branch : "master"
|
||||
commitMessage : "New comment."
|
||||
filename : comment-{@timestamp}
|
||||
format : "yml"
|
||||
moderation : true
|
||||
path : "docs/_data/comments/{options.slug}" # "/_data/comments/{options.slug}" (default)
|
||||
requiredFields : ['name', 'email', 'message']
|
||||
transforms:
|
||||
email : "md5"
|
||||
generatedFields:
|
||||
date:
|
||||
type : "date"
|
||||
options:
|
||||
format : "iso8601" # "iso8601" (default), "timestamp-seconds", "timestamp-milliseconds"
|
||||
atom_feed:
|
||||
path : # blank (default) uses feed.xml
|
||||
# Site settings
|
||||
# These are used to personalize your new site. If you look in the HTML files,
|
||||
# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
|
||||
# You can create any custom variable you would like, and they will be accessible
|
||||
# in the templates via {{ site.myvariable }}.
|
||||
title: Talk Documentation
|
||||
description: > # this means to ignore newlines until "baseurl:"
|
||||
Documentation and guides for Talk from The Coral Project.
|
||||
baseurl: "/talk" # the subpath of your site, e.g. /blog
|
||||
url: "https://coralproject.github.io" # the base hostname & protocol for your site, e.g. http://example.com
|
||||
google_analytics: UA-73335347-5
|
||||
versions:
|
||||
node: 8+
|
||||
yarn: 1.0.1+
|
||||
mongodb: 3.2+
|
||||
redis: 3.2.5+
|
||||
docker: 17.06.2+
|
||||
docker_compose: 1.14.0+
|
||||
|
||||
# SEO Related
|
||||
google_site_verification :
|
||||
bing_site_verification :
|
||||
alexa_site_verification :
|
||||
yandex_site_verification :
|
||||
|
||||
# Social Sharing
|
||||
twitter:
|
||||
username :
|
||||
facebook:
|
||||
username :
|
||||
app_id :
|
||||
publisher :
|
||||
og_image : # Open Graph/Twitter default site image
|
||||
# For specifying social profiles
|
||||
# - https://developers.google.com/structured-data/customize/social-profiles
|
||||
social:
|
||||
type : # Person or Organization (defaults to Person)
|
||||
name : # If the user or organization name differs from the site's name
|
||||
links: # An array of links to social media profiles
|
||||
|
||||
# Analytics
|
||||
analytics:
|
||||
provider : false # false (default), "google", "google-universal", "custom"
|
||||
google:
|
||||
tracking_id :
|
||||
|
||||
|
||||
# Site Author
|
||||
author:
|
||||
name : "Your Name"
|
||||
avatar : # path of avatar image, e.g. "/assets/images/bio-photo.jpg"
|
||||
bio : "I am an amazing person."
|
||||
location : "Somewhere"
|
||||
email :
|
||||
uri :
|
||||
bitbucket :
|
||||
codepen :
|
||||
dribbble :
|
||||
flickr :
|
||||
facebook :
|
||||
foursquare :
|
||||
github :
|
||||
gitlab :
|
||||
google_plus :
|
||||
keybase :
|
||||
instagram :
|
||||
lastfm :
|
||||
linkedin :
|
||||
pinterest :
|
||||
soundcloud :
|
||||
stackoverflow : # "123456/username" (the last part of your profile url, e.g. http://stackoverflow.com/users/123456/username)
|
||||
steam :
|
||||
tumblr :
|
||||
twitter :
|
||||
vine :
|
||||
weibo :
|
||||
xing :
|
||||
youtube : # "https://youtube.com/c/MichaelRoseDesign"
|
||||
|
||||
|
||||
# Reading Files
|
||||
include:
|
||||
- .htaccess
|
||||
- swagger.yml
|
||||
- _pages
|
||||
exclude:
|
||||
- "*.sublime-project"
|
||||
- "*.sublime-workspace"
|
||||
- vendor
|
||||
- .asset-cache
|
||||
- .bundle
|
||||
- .jekyll-assets-cache
|
||||
- .sass-cache
|
||||
- assets/js/plugins
|
||||
- assets/js/_main.js
|
||||
- assets/js/vendor
|
||||
- Capfile
|
||||
- CHANGELOG
|
||||
- config
|
||||
- Gemfile
|
||||
- Gruntfile.js
|
||||
- gulpfile.js
|
||||
- LICENSE
|
||||
- log
|
||||
- node_modules
|
||||
- package.json
|
||||
- Rakefile
|
||||
- README.md
|
||||
- tmp
|
||||
keep_files:
|
||||
- .git
|
||||
- .svn
|
||||
encoding: "utf-8"
|
||||
markdown_ext: "markdown,mkdown,mkdn,mkd,md"
|
||||
|
||||
|
||||
# Conversion
|
||||
# Build settings
|
||||
markdown: kramdown
|
||||
highlighter: rouge
|
||||
lsi: false
|
||||
excerpt_separator: "\n\n"
|
||||
incremental: false
|
||||
|
||||
|
||||
# Markdown Processing
|
||||
kramdown:
|
||||
input: GFM
|
||||
hard_wrap: false
|
||||
auto_ids: true
|
||||
footnote_nr: 1
|
||||
entity_output: as_char
|
||||
toc_levels: 1..6
|
||||
smart_quotes: lsquo,rsquo,ldquo,rdquo
|
||||
enable_coderay: false
|
||||
syntax_highlighter_opts:
|
||||
disable : true
|
||||
|
||||
plugins:
|
||||
- jekyll-seo-tag
|
||||
|
||||
# Exclude from processing.
|
||||
# The following items will not be processed, by default. Create a custom list
|
||||
# to override the default setting.
|
||||
# exclude:
|
||||
# - Gemfile
|
||||
# - Gemfile.lock
|
||||
# - node_modules
|
||||
# - vendor/bundle/
|
||||
# - vendor/cache/
|
||||
# - vendor/gems/
|
||||
# - vendor/ruby/
|
||||
|
||||
# Sass/SCSS
|
||||
sass:
|
||||
sass_dir: _sass
|
||||
style: compressed # http://sass-lang.com/documentation/file.SASS_REFERENCE.html#output_style
|
||||
|
||||
|
||||
# Outputting
|
||||
permalink: /:categories/:title/
|
||||
paginate: 5 # amount of posts to show
|
||||
paginate_path: /page:num/
|
||||
timezone: # http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
|
||||
|
||||
# Plugins (previously gems:)
|
||||
plugins:
|
||||
- jekyll-paginate
|
||||
- jekyll-sitemap
|
||||
- jekyll-gist
|
||||
- jekyll-feed
|
||||
- jemoji
|
||||
|
||||
# mimic GitHub Pages with --safe
|
||||
whitelist:
|
||||
- jekyll-paginate
|
||||
- jekyll-sitemap
|
||||
- jekyll-gist
|
||||
- jekyll-feed
|
||||
- jemoji
|
||||
|
||||
|
||||
# Archives
|
||||
# Type
|
||||
# - GitHub Pages compatible archive pages built with Liquid ~> type: liquid (default)
|
||||
# - Jekyll Archives plugin archive pages ~> type: jekyll-archives
|
||||
# Path (examples)
|
||||
# - Archive page should exist at path when using Liquid method or you can
|
||||
# expect broken links (especially with breadcrumbs enabled)
|
||||
# - <base_path>/tags/my-awesome-tag/index.html ~> path: /tags/
|
||||
# - <base_path/categories/my-awesome-category/index.html ~> path: /categories/
|
||||
# - <base_path/my-awesome-category/index.html ~> path: /
|
||||
# category_archive:
|
||||
# type: liquid
|
||||
# path: /categories/
|
||||
# tag_archive:
|
||||
# type: liquid
|
||||
# path: /tags/
|
||||
# https://github.com/jekyll/jekyll-archives
|
||||
# jekyll-archives:
|
||||
# enabled:
|
||||
# - categories
|
||||
# - tags
|
||||
# layouts:
|
||||
# category: archive-taxonomy
|
||||
# tag: archive-taxonomy
|
||||
# permalinks:
|
||||
# category: /categories/:name/
|
||||
# tag: /tags/:name/
|
||||
|
||||
|
||||
# HTML Compression
|
||||
# - http://jch.penibelst.de/
|
||||
compress_html:
|
||||
clippings: all
|
||||
ignore:
|
||||
envs: development
|
||||
|
||||
# Collections
|
||||
collections:
|
||||
docs:
|
||||
output: true
|
||||
permalink: /:collection/:path/
|
||||
permalink: /:path/
|
||||
|
||||
# Defaults
|
||||
defaults:
|
||||
@@ -243,10 +66,4 @@ defaults:
|
||||
path: ""
|
||||
type: docs
|
||||
values:
|
||||
layout: single
|
||||
read_time: false
|
||||
author_profile: false
|
||||
share: false
|
||||
comments: false
|
||||
sidebar:
|
||||
nav: "docs"
|
||||
layout: doc
|
||||
@@ -0,0 +1,13 @@
|
||||
items:
|
||||
- title: GitHub release
|
||||
img: https://img.shields.io/github/release/coralproject/talk.svg
|
||||
link: https://github.com/coralproject/talk/releases
|
||||
- title: ImageLayers Size
|
||||
img: https://img.shields.io/imagelayers/image-size/coralproject/talk/latest.svg
|
||||
link: https://hub.docker.com/r/coralproject/talk/
|
||||
- title: Docker Pulls
|
||||
img: https://img.shields.io/docker/pulls/coralproject/talk.svg
|
||||
link: https://hub.docker.com/r/coralproject/talk/
|
||||
- title: CircleCI
|
||||
img: https://img.shields.io/circleci/project/github/coralproject/talk.svg
|
||||
link: https://circleci.com/gh/coralproject/talk
|
||||
@@ -0,0 +1,19 @@
|
||||
items:
|
||||
- title: Installation
|
||||
children:
|
||||
- title: Talk Quickstart
|
||||
url: /
|
||||
- title: Installation from Docker
|
||||
url: /installation-from-docker/
|
||||
- title: Installation from Source
|
||||
url: /installation-from-source/
|
||||
- title: Configuration
|
||||
children:
|
||||
- title: Required Configuration
|
||||
url: /configuration/
|
||||
- title: Advanced Configuration
|
||||
url: /advanced-configuration/
|
||||
- title: Plugins
|
||||
children:
|
||||
- title: Plugins Overview
|
||||
url: /plugins/
|
||||
@@ -1,74 +0,0 @@
|
||||
# main links
|
||||
main:
|
||||
- title: "GitHub"
|
||||
url: https://github.com/coralproject/talk/
|
||||
- title: "CircleCI"
|
||||
url: https://circleci.com/gh/coralproject/talk/
|
||||
- title: "Docker"
|
||||
url: https://hub.docker.com/r/coralproject/talk/
|
||||
- title: "Roadmap"
|
||||
url: https://www.pivotaltracker.com/n/projects/1863625
|
||||
- title: "Blog"
|
||||
url: https://coralproject.net/
|
||||
|
||||
|
||||
docs:
|
||||
- title: "FAQ"
|
||||
url: /docs/faq
|
||||
- title: "Installation"
|
||||
url: /docs/install/
|
||||
children:
|
||||
- title: "Source"
|
||||
url: /docs/install/source/
|
||||
- title: "Docker"
|
||||
url: /docs/install/docker/
|
||||
- title: "First Setup"
|
||||
url: /docs/install/setup/
|
||||
- title: "Microservices"
|
||||
url: /docs/install/microservices/
|
||||
- title: "Troubleshooting"
|
||||
url: /docs/install/troubleshooting/
|
||||
- title: "Running"
|
||||
children:
|
||||
- title: "Configuration"
|
||||
url: /docs/running/configuration/
|
||||
- title: "Secrets"
|
||||
url: /docs/running/secrets/
|
||||
- title: "Plugins"
|
||||
url: /docs/running/plugins/
|
||||
- title: "Database Migrations"
|
||||
url: /docs/running/migrations/
|
||||
- title: "Persistence"
|
||||
url: /docs/running/persistence/
|
||||
- title: "Architecture"
|
||||
url: /docs/architecture/
|
||||
children:
|
||||
- title: "Tags"
|
||||
url: /docs/architecture/tags/
|
||||
- title: "Metadata"
|
||||
url: /docs/architecture/metadata/
|
||||
- title: "Command Line Interface"
|
||||
url: /docs/architecture/cli/
|
||||
- title: "Client"
|
||||
url: /docs/architecture/client
|
||||
- title: "Plugins"
|
||||
url: /docs/plugins/
|
||||
children:
|
||||
- title: "Quickstart"
|
||||
url: /docs/plugins/quickstart/
|
||||
- title: "Client API"
|
||||
url: /docs/plugins/client/
|
||||
- title: "Server API"
|
||||
url: /docs/plugins/server/
|
||||
- title: "Experimental"
|
||||
url: /docs/plugins/experimental/
|
||||
- title: "Development"
|
||||
children:
|
||||
- title: "Tools"
|
||||
url: /docs/development/tools/
|
||||
- title: "Contributor's Guide"
|
||||
url: /docs/development/contributing/
|
||||
- title: "Code of Conduct"
|
||||
url: /docs/development/code-of-conduct/
|
||||
- title: "REST API"
|
||||
url: /docs/development/rest/
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user