mirror of
https://github.com/wassname/talk.git
synced 2026-07-02 18:49:28 +08:00
Merge branch 'master' into auth
This commit is contained in:
@@ -1,2 +1 @@
|
||||
client
|
||||
dist
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"extends": "../.eslintrc.json",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
},
|
||||
"sourceType": "module"
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error"
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react'
|
||||
import { Router, Route, Redirect, IndexRoute, browserHistory } from 'react-router'
|
||||
import React from 'react';
|
||||
import {Router, Route, IndexRoute, browserHistory} from 'react-router';
|
||||
|
||||
import ModerationQueue from 'containers/ModerationQueue'
|
||||
import CommentStream from 'containers/CommentStream'
|
||||
import EmbedLink from 'components/EmbedLink'
|
||||
import Configure from 'containers/Configure'
|
||||
import CommunityContainer from 'containers/CommunityContainer'
|
||||
import LayoutContainer from 'containers/LayoutContainer'
|
||||
import ModerationQueue from 'containers/ModerationQueue';
|
||||
import CommentStream from 'containers/CommentStream';
|
||||
import EmbedLink from 'components/EmbedLink';
|
||||
import Configure from 'containers/Configure';
|
||||
import CommunityContainer from 'containers/CommunityContainer';
|
||||
import LayoutContainer from 'containers/LayoutContainer';
|
||||
|
||||
const routes = (
|
||||
<Route path='admin' component={LayoutContainer}>
|
||||
@@ -16,8 +16,8 @@ const routes = (
|
||||
<Route path='community' component={CommunityContainer} />
|
||||
<Route path='configure' component={Configure} />
|
||||
</Route>
|
||||
)
|
||||
);
|
||||
|
||||
const AppRouter = () => <Router history={browserHistory} routes={routes} />
|
||||
const AppRouter = () => <Router history={browserHistory} routes={routes} />;
|
||||
|
||||
export default AppRouter
|
||||
export default AppRouter;
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
*/
|
||||
|
||||
export const updateStatus = (status, id) => (dispatch, getState) => {
|
||||
dispatch({ type: 'COMMENT_STATUS_UPDATE', id, status })
|
||||
dispatch({ type: 'COMMENT_UPDATE', comment: getState().comments.get('byId').get(id) })
|
||||
}
|
||||
dispatch({type: 'COMMENT_STATUS_UPDATE', id, status});
|
||||
dispatch({type: 'COMMENT_UPDATE', comment: getState().comments.get('byId').get(id)});
|
||||
};
|
||||
|
||||
export const flagComment = id => (dispatch, getState) => {
|
||||
dispatch({ type: 'COMMENT_FLAG', id })
|
||||
dispatch({ type: 'COMMENT_UPDATE', comment: getState().comments.get('byId').get(id) })
|
||||
}
|
||||
dispatch({type: 'COMMENT_FLAG', id});
|
||||
dispatch({type: 'COMMENT_UPDATE', comment: getState().comments.get('byId').get(id)});
|
||||
};
|
||||
|
||||
export const createComment = (name, body) => dispatch => {
|
||||
dispatch({ type: 'COMMENT_CREATE', name, body })
|
||||
}
|
||||
dispatch({type: 'COMMENT_CREATE', name, body});
|
||||
};
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import React from 'react'
|
||||
import { Provider } from 'react-redux'
|
||||
import 'material-design-lite'
|
||||
import { Layout } from 'react-mdl'
|
||||
import store from 'services/store'
|
||||
import React from 'react';
|
||||
import {Provider} from 'react-redux';
|
||||
import 'material-design-lite';
|
||||
import store from 'services/store';
|
||||
|
||||
import AppRouter from '../AppRouter'
|
||||
import AppRouter from '../AppRouter';
|
||||
|
||||
export default class App extends React.Component {
|
||||
render (props) {
|
||||
render () {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<AppRouter store={store} />
|
||||
</Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
|
||||
import React from 'react'
|
||||
import { Button, Icon } from 'react-mdl'
|
||||
import timeago from 'timeago.js'
|
||||
import styles from './CommentList.css'
|
||||
import I18n from 'coral-framework/i18n/i18n'
|
||||
import translations from '../translations'
|
||||
import React from 'react';
|
||||
import {Button, Icon} from 'react-mdl';
|
||||
import timeago from 'timeago.js';
|
||||
import styles from './CommentList.css';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
|
||||
// Render a single comment for the list
|
||||
export default props => (
|
||||
@@ -30,17 +30,17 @@ export default props => (
|
||||
<span className={styles.body}>{props.comment.get('body')}</span>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
);
|
||||
|
||||
// Check if an action can be performed over a comment
|
||||
const canShowAction = (action, comment) => {
|
||||
const status = comment.get('status')
|
||||
const flagged = comment.get('flagged')
|
||||
const status = comment.get('status');
|
||||
const flagged = comment.get('flagged');
|
||||
|
||||
if (action === 'flag' && (status || flagged === true)) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
return true
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const lang = new I18n(translations)
|
||||
const lang = new I18n(translations);
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
|
||||
import React from 'react'
|
||||
import styles from './CommentBox.css'
|
||||
import { Button } from 'react-mdl'
|
||||
import React from 'react';
|
||||
import styles from './CommentBox.css';
|
||||
import {Button} from 'react-mdl';
|
||||
|
||||
// Renders a comment box for creating a new comment
|
||||
export default class CommentBox extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = { name: '', body: '' }
|
||||
this.onSubmit = this.onSubmit.bind(this)
|
||||
super(props);
|
||||
this.state = {name: '', body: ''};
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
}
|
||||
|
||||
onSubmit () {
|
||||
const { name, body } = this.state
|
||||
this.props.onSubmit({ name, body })
|
||||
this.setState({ body: '', name: '' })
|
||||
const {name, body} = this.state;
|
||||
this.props.onSubmit({name, body});
|
||||
this.setState({body: '', name: ''});
|
||||
}
|
||||
|
||||
render (props, { name, body }) {
|
||||
render (props, {name, body}) {
|
||||
return (
|
||||
<div>
|
||||
<div class={`${styles.textareaContainer} mdl-textfield mdl-js-textfield`}>
|
||||
@@ -30,6 +30,6 @@ export default class CommentBox extends React.Component {
|
||||
</div>
|
||||
<Button onClick={this.onSubmit} raised>Post</Button>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,99 +1,99 @@
|
||||
|
||||
import React from 'react'
|
||||
import styles from './CommentList.css'
|
||||
import key from 'keymaster'
|
||||
import Hammer from 'hammerjs'
|
||||
import Comment from 'components/Comment'
|
||||
import React from 'react';
|
||||
import styles from './CommentList.css';
|
||||
import key from 'keymaster';
|
||||
import Hammer from 'hammerjs';
|
||||
import Comment from 'components/Comment';
|
||||
|
||||
// Each action has different meaning and configuration
|
||||
const actions = {
|
||||
'reject': { status: 'Rejected', icon: 'close', key: 'r' },
|
||||
'approve': { status: 'Approved', icon: 'done', key: 't' },
|
||||
'flag': { status: 'flagged', icon: 'flag', filter: 'Untouched' }
|
||||
}
|
||||
'reject': {status: 'Rejected', icon: 'close', key: 'r'},
|
||||
'approve': {status: 'Approved', icon: 'done', key: 't'},
|
||||
'flag': {status: 'flagged', icon: 'flag', filter: 'Untouched'}
|
||||
};
|
||||
|
||||
// Renders a comment list and allow performing actions
|
||||
export default class CommentList extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
super(props);
|
||||
|
||||
this.state = { active: null }
|
||||
this.onClickAction = this.onClickAction.bind(this)
|
||||
this.state = {active: null};
|
||||
this.onClickAction = this.onClickAction.bind(this);
|
||||
}
|
||||
|
||||
// remove key handlers before leaving
|
||||
componentWillUnmount () {
|
||||
this.unbindKeyHandlers()
|
||||
this.unbindKeyHandlers();
|
||||
}
|
||||
|
||||
// add key handlers and gestures
|
||||
componentDidMount () {
|
||||
this.bindKeyHandlers()
|
||||
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.get(0) })
|
||||
this.setState({active: nextProps.commentIds.get(0)});
|
||||
}
|
||||
}
|
||||
|
||||
// Add swipe to approve or reject
|
||||
bindGestures () {
|
||||
const { actions } = this.props
|
||||
this._hammer = new Hammer(this.base)
|
||||
this._hammer.get('swipe').set({ direction: Hammer.DIRECTION_HORIZONTAL })
|
||||
const {actions} = this.props;
|
||||
this._hammer = new Hammer(this.base);
|
||||
this._hammer.get('swipe').set({direction: Hammer.DIRECTION_HORIZONTAL});
|
||||
|
||||
if (actions.indexOf('reject') !== -1) {
|
||||
this._hammer.on('swipeleft', () => this.props.singleView && this.actionKeyHandler('Rejected'))
|
||||
this._hammer.on('swipeleft', () => this.props.singleView && this.actionKeyHandler('Rejected'));
|
||||
}
|
||||
if (actions.indexOf('approve') !== -1) {
|
||||
this._hammer.on('swiperight', () => this.props.singleView && this.actionKeyHandler('Approved'))
|
||||
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 () {
|
||||
this.props.actions.filter(action => actions[action].key).forEach(action => {
|
||||
key(actions[action].key, 'commentList', () => this.props.isActive && this.actionKeyHandler(actions[action].status))
|
||||
})
|
||||
key('j', 'commentList', () => this.props.isActive && this.moveKeyHandler('down'))
|
||||
key('k', 'commentList', () => this.props.isActive && this.moveKeyHandler('up'))
|
||||
key.setScope('commentList')
|
||||
key(actions[action].key, 'commentList', () => this.props.isActive && this.actionKeyHandler(actions[action].status));
|
||||
});
|
||||
key('j', 'commentList', () => this.props.isActive && this.moveKeyHandler('down'));
|
||||
key('k', 'commentList', () => this.props.isActive && this.moveKeyHandler('up'));
|
||||
key.setScope('commentList');
|
||||
}
|
||||
|
||||
// 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)
|
||||
this.onClickAction(action, this.state.active);
|
||||
}
|
||||
}
|
||||
|
||||
// move around with j/k
|
||||
moveKeyHandler (direction) {
|
||||
if (!this.props.isActive) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const { commentIds } = this.props
|
||||
const { active } = this.state
|
||||
const {commentIds} = this.props;
|
||||
const {active} = this.state;
|
||||
// check boundaries
|
||||
if (active == null || !commentIds.size) {
|
||||
this.setState({ active: commentIds.get(0) })
|
||||
if (active === null || !commentIds.size) {
|
||||
this.setState({active: commentIds.get(0)});
|
||||
} else if (direction === 'up' && active !== commentIds.first()) {
|
||||
this.setState({ active: commentIds.get(commentIds.indexOf(active) - 1) })
|
||||
this.setState({active: commentIds.get(commentIds.indexOf(active) - 1)});
|
||||
} else if (direction === 'down' && active !== commentIds.last()) {
|
||||
this.setState({ active: commentIds.get(commentIds.indexOf(active) + 1) })
|
||||
this.setState({active: commentIds.get(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()
|
||||
const index = Math.max(commentIds.indexOf(this.state.active), 0);
|
||||
this.base.childNodes[index] && this.base.childNodes[index].focus();
|
||||
}
|
||||
|
||||
unbindKeyHandlers () {
|
||||
key.deleteScope('commentList')
|
||||
key.deleteScope('commentList');
|
||||
}
|
||||
|
||||
// If we are performing an action over a comment (aka removing from the list) we need to select a new active.
|
||||
@@ -101,26 +101,26 @@ export default class CommentList extends React.Component {
|
||||
// resolve since the content of the list could change externally. For now it works as expected
|
||||
onClickAction (action, id) {
|
||||
if (id === this.state.active) {
|
||||
const { commentIds } = this.props
|
||||
const {commentIds} = this.props;
|
||||
if (commentIds.last() === this.state.active) {
|
||||
this.setState({ active: commentIds.get(commentIds.size - 2) })
|
||||
this.setState({active: commentIds.get(commentIds.size - 2)});
|
||||
} else {
|
||||
this.setState({ active: commentIds.get(Math.min(commentIds.indexOf(this.state.active) + 1, commentIds.size - 1)) })
|
||||
this.setState({active: commentIds.get(Math.min(commentIds.indexOf(this.state.active) + 1, commentIds.size - 1))});
|
||||
}
|
||||
}
|
||||
this.props.onClickAction(action, id)
|
||||
this.props.onClickAction(action, id);
|
||||
}
|
||||
|
||||
render () {
|
||||
const {singleView, commentIds, comments, hideActive} = this.props
|
||||
const {active} = this.state
|
||||
const {singleView, commentIds, comments, hideActive} = this.props;
|
||||
const {active} = this.state;
|
||||
|
||||
return (
|
||||
<ul className={`${styles.list} ${singleView ? styles.singleView : ''}`}>
|
||||
{commentIds.map((commentId, index) => (
|
||||
<Comment comment={comments.get(commentId)}
|
||||
ref={el => { if (el && commentId === active) { this._active = el } }}
|
||||
key={index + 'comment'}
|
||||
ref={el => { if (el && commentId === active) { this._active = el; } }}
|
||||
key={`${index }comment`}
|
||||
index={index}
|
||||
onClickAction={this.onClickAction}
|
||||
actions={this.props.actions}
|
||||
@@ -129,6 +129,6 @@ export default class CommentList extends React.Component {
|
||||
hideActive={hideActive} />
|
||||
)).toArray()}
|
||||
</ul>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import React from 'react'
|
||||
import styles from './EmbedLink.css'
|
||||
import I18n from 'coral-framework/i18n/i18n'
|
||||
import translations from '../translations'
|
||||
import { Button } from 'react-mdl'
|
||||
import React from 'react';
|
||||
import styles from './EmbedLink.css';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
import {Button} from 'react-mdl';
|
||||
|
||||
const embedText =
|
||||
`<div id='coralStreamEmbed'></div><script type='text/javascript' src='http://pym.nprapps.org/pym.v1.min.js'></script><script>var pymParent = new pym.Parent('coralStreamEmbed', '${window.location.protocol}//${window.location.host}/embedScript/index.html', {});</script>`
|
||||
`<div id='coralStreamEmbed'></div><script type='text/javascript' src='http://pym.nprapps.org/pym.v1.min.js'></script><script>var pymParent = new pym.Parent('coralStreamEmbed', '${window.location.protocol}//${window.location.host}/embedScript/index.html', {});</script>`;
|
||||
|
||||
const copyToClipBoard = event => {
|
||||
const copyTextarea = document.querySelector('.' + styles.embedTextarea)
|
||||
copyTextarea.select()
|
||||
const copyToClipBoard = () => {
|
||||
const copyTextarea = document.querySelector(`.${ styles.embedTextarea}`);
|
||||
copyTextarea.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy')
|
||||
document.execCommand('copy');
|
||||
} catch (err) {
|
||||
console.error('Unable to copy')
|
||||
console.error('Unable to copy');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const EmbedLink = () => <div id={styles.embedLink}>
|
||||
<h3>Embed Comment Stream</h3>
|
||||
@@ -30,8 +30,8 @@ const EmbedLink = () => <div id={styles.embedLink}>
|
||||
{lang.t('embedlink.copy')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
export default EmbedLink
|
||||
export default EmbedLink;
|
||||
|
||||
const lang = new I18n(translations)
|
||||
const lang = new I18n(translations);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import {Layout, Navigation, Drawer, Header} from 'react-mdl';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './Header.css';
|
||||
|
||||
// App header. If we add a navbar it should be here
|
||||
export default (props) => (
|
||||
<Layout fixedDrawer>
|
||||
<Header title='Talk'>
|
||||
<Navigation>
|
||||
<Link className={styles.navLink} to={'/admin/'}>Moderate</Link>
|
||||
<Link className={styles.navLink} to={'/admin/configure'}>Configure</Link>
|
||||
</Navigation>
|
||||
</Header>
|
||||
<Drawer>
|
||||
<Navigation>
|
||||
<Link className={styles.navLink} to={'/admin/'}>Moderate</Link>
|
||||
<Link className={styles.navLink} to={'/admin/configure'}>Configure</Link>
|
||||
</Navigation>
|
||||
</Drawer>
|
||||
{props.children}
|
||||
</Layout>
|
||||
);
|
||||
@@ -1,13 +1,13 @@
|
||||
|
||||
import React from 'react'
|
||||
import { Button, Icon } from 'react-mdl'
|
||||
import styles from './Modal.css'
|
||||
import React from 'react';
|
||||
import {Button, Icon} from 'react-mdl';
|
||||
import styles from './Modal.css';
|
||||
|
||||
export default ({ open, children, onClose }) => (
|
||||
export default ({open, children, onClose}) => (
|
||||
<div className={`${styles.container} ${!open ? styles.hide : ''}`}>
|
||||
<div className={styles.inner}>
|
||||
<Button className={styles.close} onClick={onClose}><Icon name='close' /></Button>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import I18n from 'coral-framework/i18n/i18n'
|
||||
import translations from '../translations'
|
||||
import React from 'react'
|
||||
import Modal from 'components/Modal'
|
||||
import styles from './ModerationKeysModal.css'
|
||||
import { Map } from 'immutable'
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
import React from 'react';
|
||||
import Modal from 'components/Modal';
|
||||
import styles from './ModerationKeysModal.css';
|
||||
|
||||
const shortcuts = [
|
||||
{
|
||||
@@ -22,9 +21,9 @@ const shortcuts = [
|
||||
'r': 'modqueue.reject'
|
||||
}
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
export default ({ open, onClose }) => (
|
||||
export default ({open, onClose}) => (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<h3>{lang.t('modqueue.shortcuts')}</h3>
|
||||
<div className={styles.container}>
|
||||
@@ -37,7 +36,7 @@ export default ({ open, onClose }) => (
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.keys(shortcut.shortcuts).map(key => (
|
||||
<tr key={key + 'tr'}>
|
||||
<tr key={`${key }tr`}>
|
||||
<td className={styles.shortcut}><span className={styles.key}>{key}</span></td>
|
||||
<td>{lang.t(shortcut.shortcuts[key])}</td>
|
||||
</tr>
|
||||
@@ -47,6 +46,6 @@ export default ({ open, onClose }) => (
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
);
|
||||
|
||||
const lang = new I18n(translations)
|
||||
const lang = new I18n(translations);
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import {Layout} from 'react-mdl';
|
||||
import 'material-design-lite';
|
||||
import Header from 'components/Header';
|
||||
|
||||
export default (props) => (
|
||||
<Layout>
|
||||
<Header>
|
||||
{props.children}
|
||||
</Header>
|
||||
</Layout>
|
||||
);
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { Navigation, Drawer } from 'react-mdl'
|
||||
import { Link } from 'react-router'
|
||||
import styles from './Header.css'
|
||||
import React from 'react';
|
||||
import {Navigation, Drawer} from 'react-mdl';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './Header.css';
|
||||
|
||||
export default () => (
|
||||
<Drawer>
|
||||
@@ -11,4 +11,4 @@ export default () => (
|
||||
<Link className={styles.navLink} to="/admin/configure">Configure</Link>
|
||||
</Navigation>
|
||||
</Drawer>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { Navigation, Header } from 'react-mdl'
|
||||
import { Link } from 'react-router'
|
||||
import styles from './Header.css'
|
||||
import React from 'react';
|
||||
import {Navigation, Header} from 'react-mdl';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './Header.css';
|
||||
|
||||
export default () => (
|
||||
<Header title='Talk'>
|
||||
@@ -11,4 +11,4 @@ export default () => (
|
||||
<Link className={styles.navLink} to="/admin/configure">Configure</Link>
|
||||
</Navigation>
|
||||
</Header>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react'
|
||||
import { Layout as LayoutMDL} from 'react-mdl'
|
||||
import Header from './Header'
|
||||
import Drawer from './Drawer'
|
||||
import React from 'react';
|
||||
import {Layout as LayoutMDL} from 'react-mdl';
|
||||
import Header from './Header';
|
||||
import Drawer from './Drawer';
|
||||
|
||||
export const Layout = ({ children }) => (
|
||||
export const Layout = ({children}) => (
|
||||
<LayoutMDL fixedDrawer>
|
||||
<Header />
|
||||
<Drawer />
|
||||
{children}
|
||||
</LayoutMDL>
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
|
||||
import React from 'react'
|
||||
import styles from './CommentStream.css'
|
||||
import { Snackbar } from 'react-mdl'
|
||||
import { connect } from 'react-redux'
|
||||
import { createComment, flagComment } from 'actions/comments'
|
||||
import CommentList from 'components/CommentList'
|
||||
import CommentBox from 'components/CommentBox'
|
||||
import React from 'react';
|
||||
import styles from './CommentStream.css';
|
||||
import {Snackbar} from 'react-mdl';
|
||||
import {connect} from 'react-redux';
|
||||
import {createComment, flagComment} from 'actions/comments';
|
||||
import CommentList from 'components/CommentList';
|
||||
import CommentBox from 'components/CommentBox';
|
||||
|
||||
/**
|
||||
* Renders a comment stream using a CommentList component
|
||||
@@ -14,34 +13,34 @@ import CommentBox from 'components/CommentBox'
|
||||
|
||||
class CommentStream extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = { snackbar: false, snackbarMsg: '' }
|
||||
this.onSubmit = this.onSubmit.bind(this)
|
||||
this.onClickAction = this.onClickAction.bind(this)
|
||||
super(props);
|
||||
this.state = {snackbar: false, snackbarMsg: ''};
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
this.onClickAction = this.onClickAction.bind(this);
|
||||
}
|
||||
|
||||
// Fetch the comments before mounting
|
||||
componentWillMount () {
|
||||
this.props.dispatch({ type: 'COMMENT_STREAM_FETCH' })
|
||||
this.props.dispatch({type: 'COMMENT_STREAM_FETCH'});
|
||||
}
|
||||
|
||||
// Submit the new comment
|
||||
onSubmit (comment) {
|
||||
this.props.dispatch(createComment(comment.name, comment.body))
|
||||
this.props.dispatch(createComment(comment.name, comment.body));
|
||||
}
|
||||
|
||||
// The only action for now is flagging
|
||||
onClickAction (action, id) {
|
||||
if (action === 'flagged') {
|
||||
this.props.dispatch(flagComment(id))
|
||||
clearTimeout(this._snackTimeout)
|
||||
this.setState({ snackbar: true, snackbarMsg: 'Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.' })
|
||||
this._snackTimeout = setTimeout(() => this.setState({ snackbar: false }), 30000)
|
||||
this.props.dispatch(flagComment(id));
|
||||
clearTimeout(this._snackTimeout);
|
||||
this.setState({snackbar: true, snackbarMsg: 'Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.'});
|
||||
this._snackTimeout = setTimeout(() => this.setState({snackbar: false}), 30000);
|
||||
}
|
||||
}
|
||||
|
||||
// Render the comment box along with the CommentList
|
||||
render ({ comments }, { snackbar, snackbarMsg }) {
|
||||
render ({comments}, {snackbar, snackbarMsg}) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<CommentBox onSubmit={this.onSubmit} />
|
||||
@@ -54,8 +53,8 @@ class CommentStream extends React.Component {
|
||||
loading={comments.loading} />
|
||||
<Snackbar active={snackbar}>{snackbarMsg}</Snackbar>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(({ comments }) => ({ comments }))(CommentStream)
|
||||
export default connect(({comments}) => ({comments}))(CommentStream);
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import React, { Component } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import I18n from 'coral-framework/i18n/i18n'
|
||||
import translations from '../translations'
|
||||
import React, {Component} from 'react';
|
||||
|
||||
export default class CommunityContainer extends Component {
|
||||
render() {
|
||||
@@ -9,6 +6,6 @@ export default class CommunityContainer extends Component {
|
||||
<div>
|
||||
<h1>Community</h1>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,19 @@
|
||||
.configSettingEmbed {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
height: 90px;
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
height: 170px;
|
||||
}
|
||||
|
||||
.copiedText {
|
||||
color: #008000;
|
||||
float: right;
|
||||
padding: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.copyButton {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.embedInput {
|
||||
@@ -40,6 +49,6 @@
|
||||
margin-bottom: 10px;
|
||||
color: #555;
|
||||
padding: 14px;
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
@@ -9,15 +9,16 @@ import {
|
||||
Checkbox,
|
||||
Button,
|
||||
Icon
|
||||
} from 'react-mdl'
|
||||
import styles from './Configure.css'
|
||||
import I18n from 'coral-framework/i18n/i18n'
|
||||
import translations from '../translations'
|
||||
} from 'react-mdl';
|
||||
import styles from './Configure.css';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
|
||||
class Configure extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {activeSection: 'comments'}
|
||||
super(props);
|
||||
this.state = {activeSection: 'comments', copied: false};
|
||||
this.copyToClipBoard = this.copyToClipBoard.bind(this);
|
||||
}
|
||||
|
||||
getCommentSettings () {
|
||||
@@ -38,43 +39,45 @@ class Configure extends React.Component {
|
||||
error='Input is not a number!'
|
||||
label='Maximum Characters' />
|
||||
</ListItem>
|
||||
</List>
|
||||
</List>;
|
||||
}
|
||||
|
||||
copyToClipBoard (event) {
|
||||
const copyTextarea = document.querySelector('.' + styles.embedInput)
|
||||
copyTextarea.select()
|
||||
copyToClipBoard () {
|
||||
const copyTextarea = document.querySelector(`.${ styles.embedInput}`);
|
||||
copyTextarea.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy')
|
||||
document.execCommand('copy');
|
||||
this.setState({copied: true});
|
||||
} catch (err) {
|
||||
console.error('Unable to copy')
|
||||
console.error('Unable to copy', err);
|
||||
}
|
||||
}
|
||||
|
||||
getEmbed () {
|
||||
const embedText =
|
||||
`<div id='coralStreamEmbed'></div><script type='text/javascript' src='https://pym.nprapps.org/pym.v1.min.js'></script><script>var pymParent = new pym.Parent('coralStreamEmbed', '${window.location.protocol}//${window.location.host}/client/coral-embed-stream/', {title: 'comments'});</script>`
|
||||
`<div id='coralStreamEmbed'></div><script type='text/javascript' src='https://pym.nprapps.org/pym.v1.min.js'></script><script>var pymParent = new pym.Parent('coralStreamEmbed', '${window.location.protocol}//${window.location.host}/client/embed/stream/bundle.js', {title: 'comments'});</script>`;
|
||||
|
||||
return <List>
|
||||
<ListItem className={styles.configSettingEmbed}>
|
||||
<p>Copy and paste code below into your CMS to embed your comment box in your articles</p>
|
||||
<textarea type='text' className={styles.embedInput}>
|
||||
{embedText}
|
||||
</textarea>
|
||||
<Button raised colored>{lang.t('embedlink.copy')}</Button>
|
||||
<textarea rows={5} type='text' className={styles.embedInput} value={embedText} readOnly={true}/>
|
||||
<Button raised colored className={styles.copyButton} onClick={this.copyToClipBoard}>
|
||||
{lang.t('embedlink.copy')}
|
||||
</Button>
|
||||
<div className={styles.copiedText}>{this.state.copied && 'Copied!'}</div>
|
||||
</ListItem>
|
||||
</List>
|
||||
</List>;
|
||||
}
|
||||
|
||||
changeSection (activeSection) {
|
||||
this.setState({activeSection})
|
||||
this.setState({activeSection});
|
||||
}
|
||||
|
||||
render () {
|
||||
const pageTitle = this.state.activeSection === 'comments'
|
||||
? 'Comment Settings'
|
||||
: 'Embed Comment Stream'
|
||||
: 'Embed Comment Stream';
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
@@ -104,10 +107,10 @@ class Configure extends React.Component {
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(x => x)(Configure)
|
||||
export default connect(x => x)(Configure);
|
||||
|
||||
const lang = new I18n(translations)
|
||||
const lang = new I18n(translations);
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import React, { Component }from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import React, {Component}from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import { Layout } from '../components/ui/Layout'
|
||||
import {Layout} from '../components/ui/Layout';
|
||||
|
||||
class LayoutContainer extends Component {
|
||||
render () {
|
||||
return <Layout { ...this.props } />
|
||||
return <Layout { ...this.props } />;
|
||||
}
|
||||
}
|
||||
|
||||
LayoutContainer.propTypes = {}
|
||||
LayoutContainer.propTypes = {};
|
||||
|
||||
const mapStateToProps = state => ({ data: {} })
|
||||
const mapStateToProps = () => ({data: {}});
|
||||
|
||||
const mapDispatchToProps = (dispatch, ownProps) => ({ dispatch })
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer)
|
||||
const mapDispatchToProps = (dispatch) => ({dispatch});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer);
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import ModerationKeysModal from 'components/ModerationKeysModal'
|
||||
import CommentList from 'components/CommentList'
|
||||
import { updateStatus } from 'actions/comments'
|
||||
import styles from './ModerationQueue.css'
|
||||
import key from 'keymaster'
|
||||
import I18n from 'coral-framework/i18n/i18n'
|
||||
import translations from '../translations'
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import ModerationKeysModal from 'components/ModerationKeysModal';
|
||||
import CommentList from 'components/CommentList';
|
||||
import {updateStatus} from 'actions/comments';
|
||||
import styles from './ModerationQueue.css';
|
||||
import key from 'keymaster';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
|
||||
/*
|
||||
* Renders the moderation queue as a tabbed layout with 3 moderation
|
||||
@@ -17,46 +16,47 @@ import translations from '../translations'
|
||||
class ModerationQueue extends React.Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
super(props);
|
||||
|
||||
this.state = { activeTab: 'pending', singleView: false, modalOpen: false }
|
||||
this.state = {activeTab: 'pending', singleView: false, modalOpen: false};
|
||||
}
|
||||
|
||||
// Fetch comments and bind singleView key before render
|
||||
componentWillMount () {
|
||||
this.props.dispatch({ type: 'COMMENTS_MODERATION_QUEUE_FETCH' })
|
||||
key('s', () => this.setState({ singleView: !this.state.singleView }))
|
||||
key('shift+/', () => this.setState({ modalOpen: true }))
|
||||
key('esc', () => this.setState({ modalOpen: false }))
|
||||
this.props.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH'});
|
||||
key('s', () => this.setState({singleView: !this.state.singleView}));
|
||||
key('shift+/', () => this.setState({modalOpen: true}));
|
||||
key('esc', () => this.setState({modalOpen: false}));
|
||||
}
|
||||
|
||||
// Unbind singleView key before unmount
|
||||
componentWillUnmount () {
|
||||
key.unbind('s')
|
||||
key.unbind('shift+/')
|
||||
key.unbind('esc')
|
||||
key.unbind('s');
|
||||
key.unbind('shift+/');
|
||||
key.unbind('esc');
|
||||
}
|
||||
|
||||
// Hack for dynamic mdl tabs
|
||||
componentDidMount () {
|
||||
if (typeof componentHandler !== 'undefined') {
|
||||
componentHandler.upgradeAllRegistered()
|
||||
// FIXME: fix this hack
|
||||
componentHandler.upgradeAllRegistered(); // eslint-disable-line no-undef
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch the update status action
|
||||
onCommentAction (status, id) {
|
||||
this.props.dispatch(updateStatus(status, id))
|
||||
this.props.dispatch(updateStatus(status, id));
|
||||
}
|
||||
|
||||
onTabClick (activeTab) {
|
||||
this.setState({ activeTab })
|
||||
this.setState({activeTab});
|
||||
}
|
||||
|
||||
// Render the tabbed lists moderation queues
|
||||
render () {
|
||||
const { comments } = this.props
|
||||
const { activeTab, singleView, modalOpen } = this.state
|
||||
const {comments} = this.props;
|
||||
const {activeTab, singleView, modalOpen} = this.state;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -94,8 +94,8 @@ class ModerationQueue extends React.Component {
|
||||
isActive={activeTab === 'rejected'}
|
||||
singleView={singleView}
|
||||
commentIds={comments.get('ids').filter(id => {
|
||||
const data = comments.get('byId').get(id)
|
||||
return !data.get('status') && data.get('flagged') === true
|
||||
const data = comments.get('byId').get(id);
|
||||
return !data.get('status') && data.get('flagged') === true;
|
||||
})}
|
||||
comments={comments.get('byId')}
|
||||
onClickAction={(action, id) => this.onCommentAction(action, id)}
|
||||
@@ -103,13 +103,13 @@ class ModerationQueue extends React.Component {
|
||||
loading={comments.loading} />
|
||||
</div>
|
||||
<ModerationKeysModal open={modalOpen}
|
||||
onClose={() => this.setState({ modalOpen: false })} />
|
||||
onClose={() => this.setState({modalOpen: false})} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(({ comments }) => ({ comments }))(ModerationQueue)
|
||||
export default connect(({comments}) => ({comments}))(ModerationQueue);
|
||||
|
||||
const lang = new I18n(translations)
|
||||
const lang = new I18n(translations);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import App from './components/App'
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './components/App';
|
||||
|
||||
// Render the application into the DOM
|
||||
ReactDOM.render(<App />, document.querySelector('#root'))
|
||||
ReactDOM.render(<App />, document.querySelector('#root'));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import { Map, List, fromJS } from 'immutable'
|
||||
import {Map, List, fromJS} from 'immutable';
|
||||
|
||||
/**
|
||||
* Comments state is stored using 2 structures:
|
||||
@@ -12,48 +12,48 @@ const initialState = Map({
|
||||
byId: Map(),
|
||||
ids: List(),
|
||||
loading: false
|
||||
})
|
||||
});
|
||||
|
||||
// Handle the comment actions
|
||||
export default (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case 'COMMENTS_MODERATION_QUEUE_FETCH': return state.set('loading', true)
|
||||
case 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS': return replaceComments(action, state)
|
||||
case 'COMMENTS_MODERATION_QUEUE_FAILED': return state.set('loading', false)
|
||||
case 'COMMENT_STATUS_UPDATE': return updateStatus(state, action)
|
||||
case 'COMMENT_FLAG': return flag(state, action)
|
||||
case 'COMMENT_CREATE_SUCCESS': return addComment(state, action)
|
||||
case 'COMMENT_STREAM_FETCH_SUCCESS': return replaceComments(action, state)
|
||||
default: return state
|
||||
case 'COMMENTS_MODERATION_QUEUE_FETCH': return state.set('loading', true);
|
||||
case 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS': return replaceComments(action, state);
|
||||
case 'COMMENTS_MODERATION_QUEUE_FAILED': return state.set('loading', false);
|
||||
case 'COMMENT_STATUS_UPDATE': return updateStatus(state, action);
|
||||
case 'COMMENT_FLAG': return flag(state, action);
|
||||
case 'COMMENT_CREATE_SUCCESS': return addComment(state, action);
|
||||
case 'COMMENT_STREAM_FETCH_SUCCESS': return replaceComments(action, state);
|
||||
default: return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Update a comment status
|
||||
const updateStatus = (state, action) => {
|
||||
const byId = state.get('byId')
|
||||
const data = byId.get(action.id).get('data').set('status', action.status)
|
||||
const comment = byId.get(action.id).set('data', data)
|
||||
return state.set('byId', byId.set(action.id, comment))
|
||||
}
|
||||
const byId = state.get('byId');
|
||||
const data = byId.get(action.id).get('data').set('status', action.status);
|
||||
const comment = byId.get(action.id).set('data', data);
|
||||
return state.set('byId', byId.set(action.id, comment));
|
||||
};
|
||||
|
||||
// Flag a comment
|
||||
const flag = (state, action) => {
|
||||
const byId = state.get('byId')
|
||||
const data = byId.get(action.id).get('data').set('flagged', true)
|
||||
const comment = byId.get(action.id).set('data', data)
|
||||
return state.set('byId', byId.set(action.id, comment))
|
||||
}
|
||||
const byId = state.get('byId');
|
||||
const data = byId.get(action.id).get('data').set('flagged', true);
|
||||
const comment = byId.get(action.id).set('data', data);
|
||||
return state.set('byId', byId.set(action.id, comment));
|
||||
};
|
||||
|
||||
// Replace the comment list with a new one
|
||||
const replaceComments = (action, state) => {
|
||||
const comments = fromJS(action.comments.reduce((prev, curr) => { prev[curr._id] = curr; return prev }, {}))
|
||||
const comments = fromJS(action.comments.reduce((prev, curr) => { prev[curr._id] = curr; return prev; }, {}));
|
||||
return state.set('byId', comments).set('loading', false)
|
||||
.set('ids', List(comments.keys()))
|
||||
}
|
||||
.set('ids', List(comments.keys()));
|
||||
};
|
||||
|
||||
// Add a new comment
|
||||
const addComment = (state, action) => {
|
||||
const comment = fromJS(action.comment)
|
||||
const comment = fromJS(action.comment);
|
||||
return state.set('byId', state.get('byId').set(comment.get('item_id'), comment))
|
||||
.set('ids', state.get('ids').unshift(comment.get('item_id')))
|
||||
}
|
||||
.set('ids', state.get('ids').unshift(comment.get('item_id')));
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
|
||||
import { combineReducers } from 'redux'
|
||||
import comments from 'reducers/comments'
|
||||
import {combineReducers} from 'redux';
|
||||
import comments from 'reducers/comments';
|
||||
|
||||
// Combine all reducers into a main one
|
||||
export default combineReducers({
|
||||
comments
|
||||
})
|
||||
});
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
*/
|
||||
|
||||
try {
|
||||
module.exports = require('../../config.json')
|
||||
module.exports = require('../../config.json');
|
||||
} catch (error) {
|
||||
const message = `The config.json file under the root directory is missing
|
||||
or invalid Please add one to use this app. You can use config.sample.json as a guide.`
|
||||
window.alert(message)
|
||||
throw new Error(message)
|
||||
or invalid Please add one to use this app. You can use config.sample.json as a guide.`;
|
||||
window.alert(message);
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
|
||||
import { createStore, applyMiddleware } from 'redux'
|
||||
import thunk from 'redux-thunk'
|
||||
import mainReducer from 'reducers'
|
||||
import talkAdapter from 'services/talk-adapter'
|
||||
import {createStore, applyMiddleware} from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import mainReducer from 'reducers';
|
||||
import talkAdapter from 'services/talk-adapter';
|
||||
|
||||
/**
|
||||
* Create the store by merging the app reducers with
|
||||
@@ -15,4 +15,4 @@ export default createStore(
|
||||
mainReducer,
|
||||
window.devToolsExtension && window.devToolsExtension(),
|
||||
applyMiddleware(thunk, talkAdapter)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -10,44 +10,44 @@
|
||||
// Intercept redux actions and act over the ones we are interested
|
||||
export default store => next => action => {
|
||||
switch (action.type) {
|
||||
case 'COMMENTS_MODERATION_QUEUE_FETCH':
|
||||
fetchModerationQueueComments(store)
|
||||
break
|
||||
case 'COMMENT_STREAM_FETCH':
|
||||
fetchCommentStream(store)
|
||||
break
|
||||
case 'COMMENT_UPDATE':
|
||||
updateComment(store, action.comment)
|
||||
break
|
||||
case 'COMMENT_CREATE':
|
||||
createComment(store, action.name, action.body)
|
||||
break
|
||||
case 'COMMENTS_MODERATION_QUEUE_FETCH':
|
||||
fetchModerationQueueComments(store);
|
||||
break;
|
||||
// case 'COMMENT_STREAM_FETCH':
|
||||
// fetchCommentStream(store);
|
||||
// break;
|
||||
case 'COMMENT_UPDATE':
|
||||
updateComment(store, action.comment);
|
||||
break;
|
||||
case 'COMMENT_CREATE':
|
||||
createComment(store, action.name, action.body);
|
||||
break;
|
||||
}
|
||||
|
||||
next(action)
|
||||
}
|
||||
next(action);
|
||||
};
|
||||
|
||||
// Get comments to fill each of the three lists on the mod queue
|
||||
const fetchModerationQueueComments = store =>
|
||||
fetch(`/api/v1/queue`)
|
||||
fetch('/api/v1/queue')
|
||||
.then(res => res.json())
|
||||
.then(res => store.dispatch({ type: 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS',
|
||||
comments: res }))
|
||||
.catch(error => store.dispatch({ type: 'COMMENTS_MODERATION_QUEUE_FETCH_FAILED', error }))
|
||||
.then(res => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS',
|
||||
comments: res}))
|
||||
.catch(error => store.dispatch({type: 'COMMENTS_MODERATION_QUEUE_FETCH_FAILED', error}));
|
||||
|
||||
// Update a comment. Now to update a comment we need to send back the whole object
|
||||
const updateComment = (store, comment) =>
|
||||
fetch(`/api/v1/comments/${comment._id}/status`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ status: comment.status })
|
||||
body: JSON.stringify({status: comment.status})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => store.dispatch({ type: 'COMMENT_UPDATE_SUCCESS', res }))
|
||||
.catch(error => store.dispatch({ type: 'COMMENT_UPDATE_FAILED', error }))
|
||||
.then(res => store.dispatch({type: 'COMMENT_UPDATE_SUCCESS', res}))
|
||||
.catch(error => store.dispatch({type: 'COMMENT_UPDATE_FAILED', error}));
|
||||
|
||||
// Create a new comment
|
||||
const createComment = (store, name, comment) =>
|
||||
fetch(`/api/v1/comments`, {
|
||||
fetch('/api/v1/comments', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
status: 'Untouched',
|
||||
@@ -56,5 +56,5 @@ fetch(`/api/v1/comments`, {
|
||||
createdAt: Date.now()
|
||||
})
|
||||
}).then(res => res.json())
|
||||
.then(res => store.dispatch({ type: 'COMMENT_CREATE_SUCCESS', comment: res }))
|
||||
.catch(error => store.dispatch({ type: 'COMMENT_CREATE_FAILED', error }))
|
||||
.then(res => store.dispatch({type: 'COMMENT_CREATE_SUCCESS', comment: res}))
|
||||
.catch(error => store.dispatch({type: 'COMMENT_CREATE_FAILED', error}));
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
export default {
|
||||
en: {
|
||||
"modqueue": {
|
||||
"pending": "pending",
|
||||
"rejected": "rejected",
|
||||
"flagged": "flagged",
|
||||
"shortcuts": "Shortcuts",
|
||||
"close": "Close",
|
||||
"actions": "Actions",
|
||||
"navigation": "Navigation",
|
||||
"approve": "Approve comment",
|
||||
"reject": "Reject comment",
|
||||
"nextcomment": "Go to the next comment",
|
||||
"prevcomment": "Go to the previous comment",
|
||||
"singleview": "Toggle single comment edit view",
|
||||
"thismenu": "Open this menu"
|
||||
'modqueue': {
|
||||
'pending': 'pending',
|
||||
'rejected': 'rejected',
|
||||
'flagged': 'flagged',
|
||||
'shortcuts': 'Shortcuts',
|
||||
'close': 'Close',
|
||||
'actions': 'Actions',
|
||||
'navigation': 'Navigation',
|
||||
'approve': 'Approve comment',
|
||||
'reject': 'Reject comment',
|
||||
'nextcomment': 'Go to the next comment',
|
||||
'prevcomment': 'Go to the previous comment',
|
||||
'singleview': 'Toggle single comment edit view',
|
||||
'thismenu': 'Open this menu'
|
||||
},
|
||||
"comment": {
|
||||
"flagged": "flagged",
|
||||
"anon": "Anonymous"
|
||||
'comment': {
|
||||
'flagged': 'flagged',
|
||||
'anon': 'Anonymous'
|
||||
},
|
||||
"embedlink": {
|
||||
"copy": "Copy to Clipboard"
|
||||
'embedlink': {
|
||||
'copy': 'Copy to Clipboard'
|
||||
}
|
||||
},
|
||||
es: {
|
||||
"modqueue": {
|
||||
"pending": "pendiente",
|
||||
"rejected": "rechazado",
|
||||
"flagged": "marcado",
|
||||
"shortcuts": "Atajos de teclado",
|
||||
"close": "Cerrar"
|
||||
'modqueue': {
|
||||
'pending': 'pendiente',
|
||||
'rejected': 'rechazado',
|
||||
'flagged': 'marcado',
|
||||
'shortcuts': 'Atajos de teclado',
|
||||
'close': 'Cerrar'
|
||||
},
|
||||
"comment": {
|
||||
"flagged": "marcado",
|
||||
"anon": "Anónimo"
|
||||
'comment': {
|
||||
'flagged': 'marcado',
|
||||
'anon': 'Anónimo'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,63 +1,62 @@
|
||||
import React, {Component, PropTypes} from 'react'
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import {
|
||||
itemActions,
|
||||
Notification,
|
||||
notificationActions,
|
||||
authActions
|
||||
} from '../../coral-framework'
|
||||
import {connect} from 'react-redux'
|
||||
import CommentBox from '../../coral-plugin-commentbox/CommentBox'
|
||||
import Content from '../../coral-plugin-commentcontent/CommentContent'
|
||||
import PubDate from '../../coral-plugin-pubdate/PubDate'
|
||||
import Count from '../../coral-plugin-comment-count/CommentCount'
|
||||
import Flag from '../../coral-plugin-flags/FlagButton'
|
||||
import AuthorName from '../../coral-plugin-author-name/AuthorName'
|
||||
import {ReplyBox, ReplyButton} from '../../coral-plugin-replies'
|
||||
import Pym from 'pym.js'
|
||||
} from '../../coral-framework';
|
||||
import {connect} from 'react-redux';
|
||||
import CommentBox from '../../coral-plugin-commentbox/CommentBox';
|
||||
import Content from '../../coral-plugin-commentcontent/CommentContent';
|
||||
import PubDate from '../../coral-plugin-pubdate/PubDate';
|
||||
import Count from '../../coral-plugin-comment-count/CommentCount';
|
||||
import AuthorName from '../../coral-plugin-author-name/AuthorName';
|
||||
import {ReplyBox, ReplyButton} from '../../coral-plugin-replies';
|
||||
import Pym from 'pym.js';
|
||||
|
||||
const {addItem, updateItem, postItem, getStream, postAction, appendItemArray} = itemActions
|
||||
const {addNotification, clearNotification} = notificationActions
|
||||
const {setLoggedInUser} = authActions
|
||||
const {addItem, updateItem, postItem, getStream, postAction, appendItemArray} = itemActions;
|
||||
const {addNotification, clearNotification} = notificationActions;
|
||||
const {setLoggedInUser} = authActions;
|
||||
|
||||
@connect(
|
||||
(state) => {
|
||||
return {
|
||||
return {
|
||||
config: state.config.toJS(),
|
||||
items: state.items.toJS(),
|
||||
notification: state.notification.toJS(),
|
||||
auth: state.auth.toJS()
|
||||
}
|
||||
};
|
||||
},
|
||||
(dispatch) => {
|
||||
return {
|
||||
addItem: (item) => {
|
||||
return dispatch(addItem(item))
|
||||
return dispatch(addItem(item));
|
||||
},
|
||||
updateItem: (id, property, value) => {
|
||||
return dispatch(updateItem(id, property, value))
|
||||
return dispatch(updateItem(id, property, value));
|
||||
},
|
||||
postItem: (data, type, id) => {
|
||||
return dispatch(postItem(data, type, id))
|
||||
return dispatch(postItem(data, type, id));
|
||||
},
|
||||
getStream: (rootId) => {
|
||||
return dispatch(getStream(rootId))
|
||||
return dispatch(getStream(rootId));
|
||||
},
|
||||
addNotification: (type, text) => {
|
||||
return dispatch(addNotification(type, text))
|
||||
return dispatch(addNotification(type, text));
|
||||
},
|
||||
clearNotification: () => {
|
||||
return dispatch(clearNotification())
|
||||
return dispatch(clearNotification());
|
||||
},
|
||||
setLoggedInUser: (user_id) => {
|
||||
return dispatch(setLoggedInUser(user_id))
|
||||
return dispatch(setLoggedInUser(user_id));
|
||||
},
|
||||
postAction: (item, action, user) => {
|
||||
return dispatch(postAction(item, action, user))
|
||||
return dispatch(postAction(item, action, user));
|
||||
},
|
||||
appendItemArray: (item, property, value, addToFront) => {
|
||||
return dispatch(appendItemArray(item, property, value, addToFront))
|
||||
return dispatch(appendItemArray(item, property, value, addToFront));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
)
|
||||
|
||||
@@ -72,33 +71,30 @@ class CommentStream extends Component {
|
||||
componentDidMount () {
|
||||
// Set up messaging between embedded Iframe an parent component
|
||||
// Using recommended Pym init code which violates .eslint standards
|
||||
new Pym.Child({ polling: 500 })
|
||||
this.props.getStream('assetTest')
|
||||
new Pym.Child({polling: 500});
|
||||
this.props.getStream('assetTest');
|
||||
}
|
||||
|
||||
render () {
|
||||
if (Object.keys(this.props.items).length === 0) {
|
||||
if (Object.keys(this.props.items).length === 0) {
|
||||
// Loading mock asset
|
||||
this.props.postItem({
|
||||
comments: [],
|
||||
url: 'http://coralproject.net'
|
||||
}, 'asset', 'assetTest')
|
||||
this.props.postItem({
|
||||
comments: [],
|
||||
url: 'http://coralproject.net'
|
||||
}, 'asset', 'assetTest');
|
||||
|
||||
// Loading mock user
|
||||
this.props.postItem({name: 'Ban Ki-Moon'}, 'user', 'user_8989')
|
||||
this.props.postItem({name: 'Ban Ki-Moon'}, 'user', 'user_8989')
|
||||
.then((id) => {
|
||||
this.props.setLoggedInUser(id)
|
||||
})
|
||||
}
|
||||
|
||||
this.props.setLoggedInUser(id);
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Replace teststream id with id from params
|
||||
|
||||
|
||||
|
||||
const rootItemId = 'assetTest'
|
||||
const rootItem = this.props.items[rootItemId]
|
||||
return <div>
|
||||
const rootItemId = 'assetTest';
|
||||
const rootItem = this.props.items[rootItemId];
|
||||
return <div>
|
||||
{
|
||||
rootItem ?
|
||||
<div>
|
||||
@@ -116,7 +112,7 @@ class CommentStream extends Component {
|
||||
</div>
|
||||
{
|
||||
rootItem.comments.map((commentId) => {
|
||||
const comment = this.props.items[commentId]
|
||||
const comment = this.props.items[commentId];
|
||||
return <div className="comment" key={commentId}>
|
||||
<hr aria-hidden={true}/>
|
||||
<AuthorName name={comment.username}/>
|
||||
@@ -146,8 +142,8 @@ class CommentStream extends Component {
|
||||
{
|
||||
comment.children &&
|
||||
comment.children.map((replyId) => {
|
||||
let reply = this.props.items[replyId]
|
||||
return <div className="reply" key={replyId}>
|
||||
let reply = this.props.items[replyId];
|
||||
return <div className="reply" key={replyId}>
|
||||
<hr aria-hidden={true}/>
|
||||
<AuthorName name={reply.username}/>
|
||||
<PubDate created_at={reply.created_at}/>
|
||||
@@ -165,10 +161,10 @@ class CommentStream extends Component {
|
||||
updateItem={this.props.updateItem}
|
||||
parent_id={reply.parent_id}/>
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
<Notification
|
||||
@@ -176,12 +172,11 @@ class CommentStream extends Component {
|
||||
clearNotification={this.props.clearNotification}
|
||||
notification={this.props.notification}/>
|
||||
</div>
|
||||
:'Loading'
|
||||
: 'Loading'
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default CommentStream
|
||||
export default CommentStream;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react'
|
||||
import { render } from 'react-dom'
|
||||
import CommentStream from './CommentStream'
|
||||
import { Provider } from 'react-redux'
|
||||
import { fetchConfig, store } from '../../coral-framework'
|
||||
import React from 'react';
|
||||
import {render} from 'react-dom';
|
||||
import CommentStream from './CommentStream';
|
||||
import {Provider} from 'react-redux';
|
||||
import {fetchConfig, store} from '../../coral-framework';
|
||||
|
||||
store.dispatch(fetchConfig())
|
||||
store.dispatch(fetchConfig());
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<CommentStream />
|
||||
</Provider>
|
||||
, document.querySelector('#coralStream'))
|
||||
, document.querySelector('#coralStream'));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Map } from 'immutable'
|
||||
import {expect} from 'chai'
|
||||
import authReducer from '../../store/reducers/auth'
|
||||
import * as actions from '../../store/actions/auth'
|
||||
import {Map} from 'immutable';
|
||||
import {expect} from 'chai';
|
||||
import authReducer from '../../store/reducers/auth';
|
||||
import * as actions from '../../store/actions/auth';
|
||||
|
||||
describe ('authReducer', () => {
|
||||
describe('SET_LOGGED_IN_USER', () => {
|
||||
@@ -9,23 +9,23 @@ describe ('authReducer', () => {
|
||||
const action = {
|
||||
type: actions.SET_LOGGED_IN_USER,
|
||||
user_id: '123'
|
||||
}
|
||||
const store = new Map({})
|
||||
const result = authReducer(store, action)
|
||||
expect(result.get('user')).to.equal(action.user_id)
|
||||
})
|
||||
})
|
||||
};
|
||||
const store = new Map({});
|
||||
const result = authReducer(store, action);
|
||||
expect(result.get('user')).to.equal(action.user_id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('LOG_OUT_USER', () => {
|
||||
it('should clear the user store', () => {
|
||||
const action = {
|
||||
type: actions.LOG_OUT_USER
|
||||
}
|
||||
};
|
||||
const store = new Map({
|
||||
user: '123'
|
||||
})
|
||||
const result = authReducer(store, action)
|
||||
expect(result.get('user')).to.equal(undefined)
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
const result = authReducer(store, action);
|
||||
expect(result.get('user')).to.equal(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,70 +1,70 @@
|
||||
import 'react'
|
||||
import 'redux'
|
||||
import {expect} from 'chai'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import * as actions from '../../store/actions/items'
|
||||
import {Map} from 'immutable'
|
||||
import 'react';
|
||||
import 'redux';
|
||||
import {expect} from 'chai';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import * as actions from '../../store/actions/items';
|
||||
import {Map} from 'immutable';
|
||||
|
||||
import configureStore from 'redux-mock-store'
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
const mockStore = configureStore()
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('itemActions', () => {
|
||||
let store
|
||||
const host = 'http://test.host'
|
||||
let store;
|
||||
const host = 'http://test.host';
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore(new Map({}))
|
||||
fetchMock.restore()
|
||||
})
|
||||
store = mockStore(new Map({}));
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
describe('getItemsQuery', () => {
|
||||
const query = 'all'
|
||||
const rootId = '1234'
|
||||
const view = 'testView'
|
||||
const query = 'all';
|
||||
const rootId = '1234';
|
||||
const view = 'testView';
|
||||
const response = {results: [
|
||||
{Docs: [
|
||||
{type: 'comment', data: {content: 'stuff'}, item_id: '123'},
|
||||
{type: 'comment', data: {content: 'morestuff'}, item_id: '456'}
|
||||
]}
|
||||
]}
|
||||
]};
|
||||
|
||||
it('should get an item from a query and send the appropriate dispatches', () => {
|
||||
fetchMock.get('*', JSON.stringify(response))
|
||||
fetchMock.get('*', JSON.stringify(response));
|
||||
return actions.getItemsQuery(query, rootId, view, host)(store.dispatch)
|
||||
.then((res) => {
|
||||
expect(fetchMock.calls().matched[0][0]).to.equal('http://test.host/v1/exec/all/view/testView/1234')
|
||||
expect(res).to.deep.equal(response.results[0].Docs)
|
||||
expect(fetchMock.calls().matched[0][0]).to.equal('http://test.host/v1/exec/all/view/testView/1234');
|
||||
expect(res).to.deep.equal(response.results[0].Docs);
|
||||
expect(store.getActions()[0]).to.deep.equal({
|
||||
type: actions.ADD_ITEM,
|
||||
item: response.results[0].Docs[0],
|
||||
item_id: '123'
|
||||
})
|
||||
});
|
||||
expect(store.getActions()[1]).to.deep.equal({
|
||||
type: actions.ADD_ITEM,
|
||||
item: response.results[0].Docs[1],
|
||||
item_id: '456'
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should handle an error', () => {
|
||||
fetchMock.get('*', 404)
|
||||
fetchMock.get('*', 404);
|
||||
return actions.getItemsQuery(query, rootId, view, host)(store.dispatch)
|
||||
.catch((err) => {
|
||||
expect(err).to.be.truthy
|
||||
})
|
||||
})
|
||||
})
|
||||
expect(err).to.be.truthy;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getItemsArray', () => {
|
||||
const response = {items: [{type: 'comment', item_id: '123'}, {type: 'comment', item_id: '456'}]}
|
||||
const ids = [1, 2]
|
||||
const response = {items: [{type: 'comment', item_id: '123'}, {type: 'comment', item_id: '456'}]};
|
||||
const ids = [1, 2];
|
||||
|
||||
it('should get an item from an array of ids and send the appropriate dispatches', () => {
|
||||
fetchMock.get('*', JSON.stringify(response))
|
||||
fetchMock.get('*', JSON.stringify(response));
|
||||
return actions.getItemsArray(ids, host)(store.dispatch)
|
||||
.then((res) => {
|
||||
expect(res).to.deep.equal(response.items)
|
||||
expect(res).to.deep.equal(response.items);
|
||||
expect(store.getActions()[0]).to.deep.equal({
|
||||
type: actions.ADD_ITEM,
|
||||
item: {
|
||||
@@ -72,33 +72,33 @@ describe('itemActions', () => {
|
||||
item_id: '123'
|
||||
},
|
||||
item_id: '123'
|
||||
})
|
||||
});
|
||||
expect(store.getActions()[1]).to.deep.equal({
|
||||
type: actions.ADD_ITEM,
|
||||
item: {
|
||||
type: 'comment', item_id: '456'
|
||||
},
|
||||
item_id: '456'
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should handle an error', () => {
|
||||
fetchMock.get('*', 404)
|
||||
fetchMock.get('*', 404);
|
||||
return actions.getItemsArray(ids, host)(store.dispatch)
|
||||
.catch((err) => {
|
||||
expect(err).to.be.truthy
|
||||
})
|
||||
})
|
||||
})
|
||||
expect(err).to.be.truthy;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('postItem', () => {
|
||||
const item = {
|
||||
type: 'comment',
|
||||
data:{content: 'stuff'}
|
||||
}
|
||||
};
|
||||
|
||||
it ('should post an item, return an id, then dispatch that item to the store', () => {
|
||||
fetchMock.post('*', {item_id: '123', type: 'comment', data: {content: 'stuff'}})
|
||||
fetchMock.post('*', {item_id: '123', type: 'comment', data: {content: 'stuff'}});
|
||||
return actions.postItem(item.data, item.type, undefined, host)(store.dispatch)
|
||||
.then((id) => {
|
||||
expect(fetchMock.calls().matched[0][1]).to.deep.equal(
|
||||
@@ -106,8 +106,8 @@ describe('itemActions', () => {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({...item, version: 1})
|
||||
}
|
||||
)
|
||||
expect(id).to.equal('123')
|
||||
);
|
||||
expect(id).to.equal('123');
|
||||
expect(store.getActions()[0]).to.deep.equal({
|
||||
type: actions.ADD_ITEM,
|
||||
item: {
|
||||
@@ -118,35 +118,35 @@ describe('itemActions', () => {
|
||||
item_id: '123'
|
||||
},
|
||||
item_id: '123'
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
it('should handle an error', () => {
|
||||
fetchMock.post('*', 404)
|
||||
fetchMock.post('*', 404);
|
||||
return actions.postItem(item, host)(store.dispatch)
|
||||
.catch((err) => {
|
||||
expect(err).to.be.truthy
|
||||
})
|
||||
})
|
||||
})
|
||||
expect(err).to.be.truthy;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('postAction', () => {
|
||||
it ('should post an action', () => {
|
||||
fetchMock.post('*', 200)
|
||||
fetchMock.post('*', 200);
|
||||
return actions.postAction('abc', 'flag', '123', host)(store.dispatch)
|
||||
.then(response => {
|
||||
expect(fetchMock.calls().matched[0][0]).to.equal('http://test.host/v1/action/flag/user/123/on/item/abc')
|
||||
expect(response).to.equal('')
|
||||
})
|
||||
})
|
||||
expect(fetchMock.calls().matched[0][0]).to.equal('http://test.host/v1/action/flag/user/123/on/item/abc');
|
||||
expect(response).to.equal('');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle an error', () => {
|
||||
fetchMock.post('*', 404)
|
||||
fetchMock.post('*', 404);
|
||||
return actions.postItem('abc', 'flag', '123', host)(store.dispatch)
|
||||
.catch((err) => {
|
||||
expect(err).to.be.truthy
|
||||
})
|
||||
})
|
||||
expect(err).to.be.truthy;
|
||||
});
|
||||
});
|
||||
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Map, fromJS } from 'immutable'
|
||||
import {expect} from 'chai'
|
||||
import itemsReducer from '../../store/reducers/items'
|
||||
import * as actions from '../../store/actions/items'
|
||||
import {Map, fromJS} from 'immutable';
|
||||
import {expect} from 'chai';
|
||||
import itemsReducer from '../../store/reducers/items';
|
||||
|
||||
describe ('itemsReducer', () => {
|
||||
describe('ADD_ITEM', () => {
|
||||
@@ -16,18 +15,18 @@ describe ('itemsReducer', () => {
|
||||
item_id: '123'
|
||||
},
|
||||
item_id: '123'
|
||||
}
|
||||
const store = new Map({})
|
||||
const result = itemsReducer(store, action)
|
||||
};
|
||||
const store = new Map({});
|
||||
const result = itemsReducer(store, action);
|
||||
expect(result.get('123').toJS()).to.deep.equal({
|
||||
type: 'comment',
|
||||
data: {
|
||||
content: 'stuff'
|
||||
},
|
||||
item_id: '123'
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe ('UPDATE_ITEM', () => {
|
||||
it ('should update an item', () => {
|
||||
@@ -36,7 +35,7 @@ describe ('itemsReducer', () => {
|
||||
property: 'stuff',
|
||||
value: 'things',
|
||||
item_id: '123'
|
||||
}
|
||||
};
|
||||
const store = fromJS({
|
||||
'123': {
|
||||
item_id: '123',
|
||||
@@ -44,20 +43,20 @@ describe ('itemsReducer', () => {
|
||||
stuff: 'morestuff'
|
||||
}
|
||||
}
|
||||
})
|
||||
const result = itemsReducer(store, action)
|
||||
});
|
||||
const result = itemsReducer(store, action);
|
||||
expect(result.get('123').toJS()).to.deep.equal({
|
||||
item_id: '123',
|
||||
data: {
|
||||
stuff: 'things'
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('APPEND_ITEM_ARRAY', () => {
|
||||
let action
|
||||
let store
|
||||
let action;
|
||||
let store;
|
||||
|
||||
beforeEach (() => {
|
||||
action = {
|
||||
@@ -65,7 +64,7 @@ describe ('itemsReducer', () => {
|
||||
property: 'stuff',
|
||||
value: 'things',
|
||||
item_id: '123'
|
||||
}
|
||||
};
|
||||
store = fromJS({
|
||||
'123': {
|
||||
item_id: '123',
|
||||
@@ -73,37 +72,37 @@ describe ('itemsReducer', () => {
|
||||
stuff: ['morestuff']
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
it ('should append to an existing array', () => {
|
||||
const result = itemsReducer(store, action)
|
||||
const result = itemsReducer(store, action);
|
||||
expect(result.get('123').toJS()).to.deep.equal({
|
||||
item_id: '123',
|
||||
data: {
|
||||
stuff: ['morestuff', 'things']
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
it ('should create a new array', () => {
|
||||
store = fromJS({
|
||||
'123': {
|
||||
item_id: '123',
|
||||
data: {}
|
||||
}
|
||||
})
|
||||
const result = itemsReducer(store, action)
|
||||
});
|
||||
const result = itemsReducer(store, action);
|
||||
expect(result.get('123').toJS()).to.deep.equal({
|
||||
item_id: '123',
|
||||
data: {
|
||||
stuff: ['things']
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('APPEND_ITEM_RELATED', () => {
|
||||
let action
|
||||
let store
|
||||
let action;
|
||||
let store;
|
||||
|
||||
beforeEach (() => {
|
||||
action = {
|
||||
@@ -111,7 +110,7 @@ describe ('itemsReducer', () => {
|
||||
property: 'stuff',
|
||||
value: 'things',
|
||||
item_id: '123'
|
||||
}
|
||||
};
|
||||
store = fromJS({
|
||||
'123': {
|
||||
item_id: '123',
|
||||
@@ -119,31 +118,31 @@ describe ('itemsReducer', () => {
|
||||
stuff: ['morestuff']
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
it ('should append to an existing array', () => {
|
||||
const result = itemsReducer(store, action)
|
||||
const result = itemsReducer(store, action);
|
||||
expect(result.get('123').toJS()).to.deep.equal({
|
||||
item_id: '123',
|
||||
related: {
|
||||
stuff: ['morestuff', 'things']
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
it ('should create a new array', () => {
|
||||
store = fromJS({
|
||||
'123': {
|
||||
item_id: '123',
|
||||
related: {}
|
||||
}
|
||||
})
|
||||
const result = itemsReducer(store, action)
|
||||
});
|
||||
const result = itemsReducer(store, action);
|
||||
expect(result.get('123').toJS()).to.deep.equal({
|
||||
item_id: '123',
|
||||
related: {
|
||||
stuff: ['things']
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Map } from 'immutable'
|
||||
import {expect} from 'chai'
|
||||
import notificationReducer from '../../store/reducers/notification'
|
||||
import * as actions from '../../store/actions/notification'
|
||||
import {Map} from 'immutable';
|
||||
import {expect} from 'chai';
|
||||
import notificationReducer from '../../store/reducers/notification';
|
||||
import * as actions from '../../store/actions/notification';
|
||||
|
||||
describe ('notificationsReducer', () => {
|
||||
describe('ADD_NOTIFICATION', () => {
|
||||
@@ -10,26 +10,26 @@ describe ('notificationsReducer', () => {
|
||||
type: actions.ADD_NOTIFICATION,
|
||||
text: 'Test notification',
|
||||
notifType: 'test'
|
||||
}
|
||||
const store = new Map({})
|
||||
const result = notificationReducer(store, action)
|
||||
expect(result.get('text')).to.equal(action.text)
|
||||
expect(result.get('type')).to.equal(action.notifType)
|
||||
})
|
||||
})
|
||||
};
|
||||
const store = new Map({});
|
||||
const result = notificationReducer(store, action);
|
||||
expect(result.get('text')).to.equal(action.text);
|
||||
expect(result.get('type')).to.equal(action.notifType);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CLEAR_NOTIFICATION', () => {
|
||||
it('should clear a notification', () => {
|
||||
const action = {
|
||||
type: actions.CLEAR_NOTIFICATION
|
||||
}
|
||||
};
|
||||
const store = new Map({
|
||||
text: 'Test notification',
|
||||
type: 'test'
|
||||
})
|
||||
const result = notificationReducer(store, action)
|
||||
expect(result.get('text')).to.equal(undefined)
|
||||
expect(result.get('type')).to.equal(undefined)
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
const result = notificationReducer(store, action);
|
||||
expect(result.get('text')).to.equal(undefined);
|
||||
expect(result.get('type')).to.equal(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import timeago from 'timeago.js'
|
||||
import esTA from 'timeago.js/locales/es'
|
||||
import timeago from 'timeago.js';
|
||||
import esTA from 'timeago.js/locales/es';
|
||||
|
||||
/**
|
||||
* Default locales, this should be overriden by config file
|
||||
@@ -11,32 +11,34 @@ class i18n {
|
||||
* Register locales
|
||||
*/
|
||||
|
||||
this.locales = {'en': 'en', 'es': 'es'}
|
||||
timeago.register('es_ES', esTA)
|
||||
this.timeagoInstance = new timeago()
|
||||
this.locales = {'en': 'en', 'es': 'es'};
|
||||
timeago.register('es_ES', esTA);
|
||||
this.timeagoInstance = new timeago();
|
||||
/**
|
||||
* Load translations
|
||||
*/
|
||||
let trans = translations || { en: {} }
|
||||
let trans = translations || {en: {}};
|
||||
|
||||
try {
|
||||
const locale = localStorage.getItem('locale') || navigator.language
|
||||
localStorage.setItem('locale', locale)
|
||||
const lang = this.locales[locale.split('-')[0]] || 'en'
|
||||
this.translations = trans[lang]
|
||||
const locale = localStorage.getItem('locale') || navigator.language;
|
||||
localStorage.setItem('locale', locale);
|
||||
const lang = this.locales[locale.split('-')[0]] || 'en';
|
||||
this.translations = trans[lang];
|
||||
} catch (err) {
|
||||
this.translations = trans['en']
|
||||
this.translations = trans['en'];
|
||||
}
|
||||
|
||||
this.setLocale = (locale) => {
|
||||
try {
|
||||
localStorage.setItem('locale', locale)
|
||||
} catch (err) {}
|
||||
}
|
||||
localStorage.setItem('locale', locale);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
this.getLocale = () => (
|
||||
localStorage.getItem('locale') || navigator.locale || 'en-US'
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Expose the translation function
|
||||
@@ -47,28 +49,28 @@ class i18n {
|
||||
*/
|
||||
|
||||
this.t = (key) => {
|
||||
const arr = key.split('.')
|
||||
let translation = this.translations
|
||||
const arr = key.split('.');
|
||||
let translation = this.translations;
|
||||
try {
|
||||
for (var i = 0; i < arr.length; i++) translation = translation[arr[i]]
|
||||
for (let i = 0; i < arr.length; i++) {translation = translation[arr[i]];}
|
||||
} catch (error) {
|
||||
console.warn(`${key} language key not set`)
|
||||
return key
|
||||
console.warn(`${key} language key not set`);
|
||||
return key;
|
||||
}
|
||||
|
||||
const val = String(translation)
|
||||
const val = String(translation);
|
||||
if (val) {
|
||||
return val
|
||||
return val;
|
||||
} else {
|
||||
console.warn(`${key} language key not set`)
|
||||
return key
|
||||
console.warn(`${key} language key not set`);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.timeago = (time) => {
|
||||
return this.timeagoInstance.format(time)
|
||||
}
|
||||
return this.timeagoInstance.format(time);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default i18n
|
||||
export default i18n;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import Notification from './notification/Notification'
|
||||
import store from './store/store'
|
||||
import {fetchConfig} from './store/actions/config'
|
||||
import * as itemActions from './store/actions/items'
|
||||
import I18n from './i18n/i18n'
|
||||
import * as notificationActions from './store/actions/notification'
|
||||
import * as authActions from './store/actions/auth'
|
||||
import Notification from './notification/Notification';
|
||||
import store from './store/store';
|
||||
import {fetchConfig} from './store/actions/config';
|
||||
import * as itemActions from './store/actions/items';
|
||||
import I18n from './i18n/i18n';
|
||||
import * as notificationActions from './store/actions/notification';
|
||||
import * as authActions from './store/actions/auth';
|
||||
|
||||
export {
|
||||
Notification,
|
||||
@@ -14,4 +14,4 @@ export {
|
||||
I18n,
|
||||
notificationActions,
|
||||
authActions
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import React from 'react'
|
||||
import React from 'react';
|
||||
|
||||
const Notification = (props) => {
|
||||
if (props.notification.text) {
|
||||
setTimeout(() => {
|
||||
props.clearNotification()
|
||||
}, props.notifLength)
|
||||
props.clearNotification();
|
||||
}, props.notifLength);
|
||||
}
|
||||
return <div>
|
||||
{
|
||||
props.notification.text &&
|
||||
<dialog open id='coral-notif' className={'coral-notif-' + props.notification.type}>
|
||||
<dialog open id='coral-notif' className={`coral-notif-${ props.notification.type}`}>
|
||||
{props.notification.text}
|
||||
</dialog>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default Notification
|
||||
export default Notification;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
/* Auth Actions */
|
||||
|
||||
export const SET_LOGGED_IN_USER = 'SET_LOGGED_IN_USER'
|
||||
export const LOG_OUT_USER = 'LOG_OUT_USER'
|
||||
export const SET_LOGGED_IN_USER = 'SET_LOGGED_IN_USER';
|
||||
export const LOG_OUT_USER = 'LOG_OUT_USER';
|
||||
|
||||
export const setLoggedInUser = (user_id) => {
|
||||
return {
|
||||
type: SET_LOGGED_IN_USER,
|
||||
user_id
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const LogOutUser = () => {
|
||||
return {
|
||||
type: LOG_OUT_USER
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
/* @flow */
|
||||
|
||||
import { fromJS } from 'immutable'
|
||||
import {fromJS} from 'immutable';
|
||||
|
||||
/**
|
||||
* Action name constants
|
||||
*/
|
||||
|
||||
export const FETCH_CONFIG_REQUEST = 'FETCH_CONFIG_REQUEST'
|
||||
export const FETCH_CONFIG_FAILED = 'FETCH_CONFIG_FAILED'
|
||||
export const FETCH_CONFIG_SUCCESS = 'FETCH_CONFIG_SUCCESS'
|
||||
export const FETCH_CONFIG_REQUEST = 'FETCH_CONFIG_REQUEST';
|
||||
export const FETCH_CONFIG_FAILED = 'FETCH_CONFIG_FAILED';
|
||||
export const FETCH_CONFIG_SUCCESS = 'FETCH_CONFIG_SUCCESS';
|
||||
|
||||
/**
|
||||
* Action creators
|
||||
*/
|
||||
|
||||
export const fetchConfig = () => async (dispatch) => {
|
||||
dispatch({ type: FETCH_CONFIG_REQUEST })
|
||||
dispatch({type: FETCH_CONFIG_REQUEST});
|
||||
|
||||
try {
|
||||
//TODO: Replace with fetching config from backend
|
||||
// const response = await fetch(`./talk.config.json`)
|
||||
// const json = await response.json()
|
||||
dispatch({ type: FETCH_CONFIG_SUCCESS, config: fromJS({}) })
|
||||
dispatch({type: FETCH_CONFIG_SUCCESS, config: fromJS({})});
|
||||
} catch (error) {
|
||||
dispatch({ type: FETCH_CONFIG_FAILED })
|
||||
dispatch({type: FETCH_CONFIG_FAILED});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
/* Item Actions */
|
||||
|
||||
import { fromJS } from 'immutable'
|
||||
import mocks from '../../mocks.json'
|
||||
|
||||
/**
|
||||
* Action name constants
|
||||
*/
|
||||
|
||||
export const ADD_ITEM = 'ADD_ITEM'
|
||||
export const UPDATE_ITEM = 'UPDATE_ITEM'
|
||||
export const APPEND_ITEM_ARRAY = 'APPEND_ITEM_ARRAY'
|
||||
export const ADD_ITEM = 'ADD_ITEM';
|
||||
export const UPDATE_ITEM = 'UPDATE_ITEM';
|
||||
export const APPEND_ITEM_ARRAY = 'APPEND_ITEM_ARRAY';
|
||||
|
||||
/**
|
||||
* Action creators
|
||||
@@ -26,14 +23,14 @@ export const APPEND_ITEM_ARRAY = 'APPEND_ITEM_ARRAY'
|
||||
|
||||
export const addItem = (item) => {
|
||||
if (!item.id) {
|
||||
console.warn('addItem called without an item id.')
|
||||
console.warn('addItem called without an item id.');
|
||||
}
|
||||
return {
|
||||
type: ADD_ITEM,
|
||||
item: item,
|
||||
id: item.id
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
* Updates an item in the local store without posting it to the server
|
||||
@@ -46,15 +43,14 @@ export const addItem = (item) => {
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
export const updateItem = (id, property, value) => {
|
||||
return {
|
||||
type: UPDATE_ITEM,
|
||||
id,
|
||||
property,
|
||||
value
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const appendItemArray = (id, property, value, addToFront) => {
|
||||
return {
|
||||
@@ -63,8 +59,8 @@ export const appendItemArray = (id, property, value, addToFront) => {
|
||||
property,
|
||||
value,
|
||||
addToFront
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
* Get Items from Query
|
||||
@@ -81,47 +77,47 @@ export const appendItemArray = (id, property, value, addToFront) => {
|
||||
*/
|
||||
export function getStream (assetId) {
|
||||
return (dispatch) => {
|
||||
return fetch('/api/v1/stream?asset_id='+assetId)
|
||||
return fetch(`/api/v1/stream?asset_id=${assetId}`)
|
||||
.then(
|
||||
response => {
|
||||
return response.ok ? response.json() : Promise.reject(response.status + ' ' + response.statusText)
|
||||
return response.ok ? response.json() : Promise.reject(`${response.status } ${ response.statusText}`);
|
||||
}
|
||||
)
|
||||
.then((json) => {
|
||||
|
||||
/* Sort comments by date*/
|
||||
let rootComments = []
|
||||
let childComments = {}
|
||||
json.sort((a,b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
|
||||
json.reduce((prev, item) => {
|
||||
dispatch(addItem(item))
|
||||
let rootComments = [];
|
||||
let childComments = {};
|
||||
json.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
||||
json.forEach(item => {
|
||||
dispatch(addItem(item));
|
||||
|
||||
/* Check for root and child comments. */
|
||||
if (
|
||||
item.asset_id === assetId &&
|
||||
!item.parent_id) {
|
||||
rootComments.push(item.id)
|
||||
rootComments.push(item.id);
|
||||
} else if (
|
||||
item.asset_id === assetId
|
||||
) {
|
||||
let children = childComments[item.parent_id] || []
|
||||
childComments[item.parent_id] = children.concat(item.id)
|
||||
let children = childComments[item.parent_id] || [];
|
||||
childComments[item.parent_id] = children.concat(item.id);
|
||||
}
|
||||
}, {})
|
||||
}, {});
|
||||
|
||||
dispatch(addItem({
|
||||
id: assetId,
|
||||
comments: rootComments
|
||||
}))
|
||||
}));
|
||||
|
||||
const keys = Object.keys(childComments)
|
||||
for (var i=0; i < keys.length; i++ ) {
|
||||
dispatch(updateItem(keys[i], 'children', childComments[keys[i]].reverse()))
|
||||
const keys = Object.keys(childComments);
|
||||
for (let i = 0; i < keys.length; i++ ) {
|
||||
dispatch(updateItem(keys[i], 'children', childComments[keys[i]].reverse()));
|
||||
}
|
||||
|
||||
return (json)
|
||||
})
|
||||
}
|
||||
return (json);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -140,20 +136,20 @@ export function getStream (assetId) {
|
||||
|
||||
export function getItemsArray (ids) {
|
||||
return (dispatch) => {
|
||||
return fetch('/v1/item/' + ids)
|
||||
return fetch(`/v1/item/${ ids}`)
|
||||
.then(
|
||||
response => {
|
||||
return response.ok ? response.json()
|
||||
: Promise.reject(response.status + ' ' + response.statusText)
|
||||
: Promise.reject(`${response.status } ${ response.statusText}`);
|
||||
}
|
||||
)
|
||||
.then((json) => {
|
||||
for (var i = 0; i < json.items.length; i++) {
|
||||
dispatch(addItem(json.items[i]))
|
||||
for (let i = 0; i < json.items.length; i++) {
|
||||
dispatch(addItem(json.items[i]));
|
||||
}
|
||||
return json.items
|
||||
})
|
||||
}
|
||||
return json.items;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -173,7 +169,7 @@ export function getItemsArray (ids) {
|
||||
export function postItem (item, type, id) {
|
||||
return (dispatch) => {
|
||||
if (id) {
|
||||
item.id = id
|
||||
item.id = id;
|
||||
}
|
||||
let options = {
|
||||
method: 'POST',
|
||||
@@ -181,20 +177,20 @@ export function postItem (item, type, id) {
|
||||
headers: {
|
||||
'Content-Type':'application/json'
|
||||
}
|
||||
}
|
||||
};
|
||||
console.log('postItem', options);
|
||||
return fetch('/api/v1/' + type, options)
|
||||
return fetch(`/api/v1/${ type}`, options)
|
||||
.then(
|
||||
response => {
|
||||
return response.ok ? response.json()
|
||||
: Promise.reject(response.status + ' ' + response.statusText)
|
||||
: Promise.reject(`${response.status } ${ response.statusText}`);
|
||||
}
|
||||
)
|
||||
.then((json) => {
|
||||
dispatch(addItem({...item, id:json.id}))
|
||||
return json.id
|
||||
})
|
||||
}
|
||||
dispatch(addItem({...item, id:json.id}));
|
||||
return json.id;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
//http://localhost:16180/v1/action/flag/user/user_89654/on/item/87e418c5-aafb-4eb7-9ce4-78f28793782a
|
||||
@@ -219,19 +215,19 @@ export function postAction (id, type, user_id) {
|
||||
const action = {
|
||||
type,
|
||||
user_id
|
||||
}
|
||||
};
|
||||
const options = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(action)
|
||||
}
|
||||
};
|
||||
|
||||
dispatch(appendItemArray(id, type, user_id))
|
||||
return fetch('/api/v1/comments/' + id + '/actions', options)
|
||||
dispatch(appendItemArray(id, type, user_id));
|
||||
return fetch(`/api/v1/comments/${ id }/actions`, options)
|
||||
.then(
|
||||
response => {
|
||||
return response.ok ? response.text()
|
||||
: Promise.reject(response.status + ' ' + response.statusText)
|
||||
: Promise.reject(`${response.status } ${ response.statusText}`);
|
||||
}
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
/* Notification Actions */
|
||||
|
||||
export const ADD_NOTIFICATION = 'ADD_NOTIFICATION'
|
||||
export const CLEAR_NOTIFICATION = 'CLEAR_NOTIFICATION'
|
||||
export const ADD_NOTIFICATION = 'ADD_NOTIFICATION';
|
||||
export const CLEAR_NOTIFICATION = 'CLEAR_NOTIFICATION';
|
||||
|
||||
export const addNotification = (notifType, text) => {
|
||||
return {
|
||||
type: ADD_NOTIFICATION,
|
||||
notifType,
|
||||
text
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const clearNotification = () => {
|
||||
return {
|
||||
type: CLEAR_NOTIFICATION
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
/* Auth */
|
||||
|
||||
import * as actions from '../actions/auth'
|
||||
import { fromJS } from 'immutable'
|
||||
import * as actions from '../actions/auth';
|
||||
import {fromJS} from 'immutable';
|
||||
|
||||
const initialState = fromJS({})
|
||||
const initialState = fromJS({});
|
||||
|
||||
export default (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case actions.SET_LOGGED_IN_USER:
|
||||
return state.set('user', action.user_id)
|
||||
case actions.LOG_OUT_USER:
|
||||
return initialState
|
||||
default:
|
||||
return state
|
||||
case actions.SET_LOGGED_IN_USER:
|
||||
return state.set('user', action.user_id);
|
||||
case actions.LOG_OUT_USER:
|
||||
return initialState;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
/* @flow */
|
||||
|
||||
import { Map, fromJS } from 'immutable'
|
||||
import * as actions from '../actions/config'
|
||||
import {Map} from 'immutable';
|
||||
import * as actions from '../actions/config';
|
||||
|
||||
const initialState = Map({
|
||||
features: Map({})
|
||||
})
|
||||
});
|
||||
|
||||
export default (state = initialState, action) => {
|
||||
switch(action.type) {
|
||||
case actions.FETCH_CONFIG_REQUEST:
|
||||
return state.set('loading', true)
|
||||
case actions.FETCH_CONFIG_REQUEST:
|
||||
return state.set('loading', true);
|
||||
|
||||
case actions.FETCH_CONFIG_FAILED:
|
||||
return state.set('loading', false)
|
||||
case actions.FETCH_CONFIG_FAILED:
|
||||
return state.set('loading', false);
|
||||
|
||||
// Override config if worked
|
||||
case actions.FETCH_CONFIG_SUCCESS:
|
||||
return action.config.set('loading', false)
|
||||
case actions.FETCH_CONFIG_SUCCESS:
|
||||
return action.config.set('loading', false);
|
||||
|
||||
default:
|
||||
return state
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/* @flow */
|
||||
|
||||
import { combineReducers } from 'redux'
|
||||
import config from './config'
|
||||
import items from './items'
|
||||
import notification from './notification'
|
||||
import auth from './auth'
|
||||
import {combineReducers} from 'redux';
|
||||
import config from './config';
|
||||
import items from './items';
|
||||
import notification from './notification';
|
||||
import auth from './auth';
|
||||
|
||||
/**
|
||||
* Expose the combined main reducer
|
||||
@@ -15,4 +15,4 @@ export default combineReducers({
|
||||
items,
|
||||
notification,
|
||||
auth
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
/* Items Reducer */
|
||||
|
||||
import { Map, fromJS } from 'immutable'
|
||||
import * as actions from '../actions/items'
|
||||
import {fromJS} from 'immutable';
|
||||
import * as actions from '../actions/items';
|
||||
|
||||
const initialState = fromJS({})
|
||||
const initialState = fromJS({});
|
||||
|
||||
export default (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case actions.ADD_ITEM:
|
||||
return state.set(action.id, fromJS(action.item))
|
||||
case actions.UPDATE_ITEM:
|
||||
return state.updateIn([action.id, action.property], () =>
|
||||
case actions.ADD_ITEM:
|
||||
return state.set(action.id, fromJS(action.item));
|
||||
case actions.UPDATE_ITEM:
|
||||
return state.updateIn([action.id, action.property], () =>
|
||||
fromJS(action.value)
|
||||
)
|
||||
case actions.APPEND_ITEM_ARRAY:
|
||||
return state.updateIn([action.id, action.property], (prop) => {
|
||||
if (action.addToFront) {
|
||||
return prop ? prop.unshift(action.value) : fromJS([action.value])
|
||||
} else {
|
||||
return prop ? prop.push(action.value) : fromJS([action.value])
|
||||
}
|
||||
|
||||
);
|
||||
case actions.APPEND_ITEM_ARRAY:
|
||||
return state.updateIn([action.id, action.property], (prop) => {
|
||||
if (action.addToFront) {
|
||||
return prop ? prop.unshift(action.value) : fromJS([action.value]);
|
||||
} else {
|
||||
return prop ? prop.push(action.value) : fromJS([action.value]);
|
||||
}
|
||||
)
|
||||
default:
|
||||
return state
|
||||
|
||||
}
|
||||
);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
/* Items Notifications */
|
||||
|
||||
import * as actions from '../actions/notification'
|
||||
import { fromJS } from 'immutable'
|
||||
import * as actions from '../actions/notification';
|
||||
import {fromJS} from 'immutable';
|
||||
|
||||
const initialState = fromJS({})
|
||||
const initialState = fromJS({});
|
||||
|
||||
export default (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case actions.ADD_NOTIFICATION:
|
||||
return state.set('text', action.text).set('type', action.notifType)
|
||||
case actions.CLEAR_NOTIFICATION:
|
||||
return initialState
|
||||
default:
|
||||
return state
|
||||
case actions.ADD_NOTIFICATION:
|
||||
return state.set('text', action.text).set('type', action.notifType);
|
||||
case actions.CLEAR_NOTIFICATION:
|
||||
return initialState;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
|
||||
import { createStore, applyMiddleware } from 'redux'
|
||||
import thunk from 'redux-thunk'
|
||||
import mainReducer from './reducers'
|
||||
import {createStore, applyMiddleware} from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import mainReducer from './reducers';
|
||||
|
||||
export default createStore(
|
||||
mainReducer,
|
||||
window.devToolsExtension && window.devToolsExtension(),
|
||||
applyMiddleware(thunk)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react'
|
||||
const packagename = 'coral-plugin-author-name'
|
||||
import React from 'react';
|
||||
const packagename = 'coral-plugin-author-name';
|
||||
|
||||
const AuthorName = ({name}) =>
|
||||
<div className={packagename + '-text'}>
|
||||
<div className={`${packagename }-text`}>
|
||||
{name}
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
export default AuthorName
|
||||
export default AuthorName;
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import React from 'react'
|
||||
import React from 'react';
|
||||
|
||||
const name = 'coral-plugin-comment-count'
|
||||
const name = 'coral-plugin-comment-count';
|
||||
|
||||
const CommentCount = ({items, id}) => {
|
||||
let count = 0
|
||||
let count = 0;
|
||||
if (items[id]) {
|
||||
count += items[id].comments.length
|
||||
count += items[id].comments.length;
|
||||
}
|
||||
const itemKeys = Object.keys(items)
|
||||
for (var i=0; i < itemKeys.length; i++) {
|
||||
const item = items[itemKeys[i]]
|
||||
const itemKeys = Object.keys(items);
|
||||
for (let i = 0; i < itemKeys.length; i++) {
|
||||
const item = items[itemKeys[i]];
|
||||
if (item.children) {
|
||||
count += item.children.length
|
||||
count += item.children.length;
|
||||
}
|
||||
}
|
||||
|
||||
return <div className={name + '-text'}>
|
||||
{count + ' ' + (count === 1 ? 'Comment':'Comments')}
|
||||
</div>
|
||||
}
|
||||
return <div className={`${name }-text`}>
|
||||
{`${count } ${ count === 1 ? 'Comment' : 'Comments'}`}
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default CommentCount
|
||||
export default CommentCount;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, {Component, PropTypes} from 'react'
|
||||
import {I18n} from '../coral-framework'
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import {I18n} from '../coral-framework';
|
||||
|
||||
const name='coral-plugin-commentbox'
|
||||
const name = 'coral-plugin-commentbox';
|
||||
|
||||
class CommentBox extends Component {
|
||||
|
||||
@@ -19,35 +19,35 @@ class CommentBox extends Component {
|
||||
}
|
||||
|
||||
postComment = () => {
|
||||
const {postItem, updateItem, id, parent_id, addNotification, appendItemArray} = this.props
|
||||
const {postItem, updateItem, id, parent_id, addNotification, appendItemArray} = this.props;
|
||||
let comment = {
|
||||
body: this.state.body,
|
||||
asset_id: id,
|
||||
username: this.state.username
|
||||
}
|
||||
let related
|
||||
};
|
||||
let related;
|
||||
if (parent_id) {
|
||||
comment.parent_id = parent_id
|
||||
related = 'children'
|
||||
comment.parent_id = parent_id;
|
||||
related = 'children';
|
||||
} else {
|
||||
related = 'comments'
|
||||
related = 'comments';
|
||||
}
|
||||
updateItem(parent_id, 'showReply', false)
|
||||
updateItem(parent_id, 'showReply', false);
|
||||
postItem(comment, 'comments')
|
||||
.then((comment_id) => {
|
||||
appendItemArray(parent_id || id, related, comment_id, parent_id ? false : true)
|
||||
addNotification('success', 'Your comment has been posted.')
|
||||
}).catch((err) => console.error(err))
|
||||
this.setState({body: ''})
|
||||
appendItemArray((parent_id || id, related, comment_id, parent_id));
|
||||
addNotification('success', 'Your comment has been posted.');
|
||||
}).catch((err) => console.error(err));
|
||||
this.setState({body: ''});
|
||||
}
|
||||
|
||||
render () {
|
||||
const {styles, reply} = this.props
|
||||
const {styles, reply} = this.props;
|
||||
// How to handle language in plugins? Should we have a dependency on our central translation file?
|
||||
return <div>
|
||||
<div className={name + '-container'}>
|
||||
<div className={`${name }-container`}>
|
||||
<input type='text'
|
||||
className={name + '-username'}
|
||||
className={`${name }-username`}
|
||||
style={styles && styles.textarea}
|
||||
value={this.state.username}
|
||||
id={reply ? 'replyUser' : 'commentUser'}
|
||||
@@ -55,15 +55,15 @@ class CommentBox extends Component {
|
||||
onChange={(e) => this.setState({username: e.target.value})}/>
|
||||
</div>
|
||||
<div
|
||||
className={name + '-container'}>
|
||||
className={`${name }-container`}>
|
||||
<label
|
||||
htmlFor={ reply ? 'replyText' : 'commentText'}
|
||||
className="screen-reader-text"
|
||||
aria-hidden={true}>
|
||||
{reply ? lang.t('reply'): lang.t('comment')}
|
||||
{reply ? lang.t('reply') : lang.t('comment')}
|
||||
</label>
|
||||
<textarea
|
||||
className={name + '-textarea'}
|
||||
className={`${name }-textarea`}
|
||||
style={styles && styles.textarea}
|
||||
value={this.state.body}
|
||||
placeholder='Comment'
|
||||
@@ -71,19 +71,19 @@ class CommentBox extends Component {
|
||||
onChange={(e) => this.setState({body: e.target.value})}
|
||||
rows={3}/>
|
||||
</div>
|
||||
<div className={name + '-button-container'}>
|
||||
<div className={`${name }-button-container`}>
|
||||
<button
|
||||
className={name + '-button'}
|
||||
className={`${name }-button`}
|
||||
style={styles && styles.button}
|
||||
onClick={this.postComment}>
|
||||
{lang.t('post')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export default CommentBox
|
||||
export default CommentBox;
|
||||
|
||||
const lang = new I18n({
|
||||
en: {
|
||||
@@ -96,4 +96,4 @@ const lang = new I18n({
|
||||
reply: 'Respuesta',
|
||||
comment: 'Comentario'
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import React from 'react'
|
||||
import {shallow, mount} from 'enzyme'
|
||||
import {expect} from 'chai'
|
||||
import CommentBox from '../CommentBox'
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import {expect} from 'chai';
|
||||
import CommentBox from '../CommentBox';
|
||||
|
||||
describe('CommentBox', () => {
|
||||
let comment
|
||||
let render
|
||||
let comment;
|
||||
let render;
|
||||
beforeEach(() => {
|
||||
comment = {}
|
||||
comment = {};
|
||||
const postItem = (item) => {
|
||||
comment.posted=item
|
||||
return Promise.resolve(4)
|
||||
}
|
||||
comment.posted = item;
|
||||
return Promise.resolve(4);
|
||||
};
|
||||
render = shallow(<CommentBox
|
||||
postItem={postItem}
|
||||
updateItem={(e) => comment.text=e.target.value}
|
||||
updateItem={(e) => comment.text = e.target.value}
|
||||
item_id={'1'}
|
||||
comments={['1', '2', '3']}/>)
|
||||
})
|
||||
comments={['1', '2', '3']}/>);
|
||||
});
|
||||
|
||||
it('should render the CommentBox appropriately', () => {
|
||||
expect(render.contains('<div class="CommentBox"')).to.be.truthy
|
||||
expect(render.contains('<button class="postCommentButton"')).to.be.truthy
|
||||
})
|
||||
})
|
||||
expect(render.contains('<div class="CommentBox"')).to.be.truthy;
|
||||
expect(render.contains('<button class="postCommentButton"')).to.be.truthy;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react'
|
||||
const name = 'coral-plugin-replies'
|
||||
import React from 'react';
|
||||
const name = 'coral-plugin-replies';
|
||||
|
||||
const Content = ({body, styles}) => {
|
||||
const textbreaks = body.split('\n')
|
||||
const textbreaks = body.split('\n');
|
||||
return <div
|
||||
className={name + '-text'}
|
||||
className={`${name }-text`}
|
||||
style={styles && styles.text}>
|
||||
{
|
||||
textbreaks.map((line, i) => <span key={i} className={name+'-line'}>
|
||||
{line} <br className={name+'-linebreak'}/>
|
||||
textbreaks.map((line, i) => <span key={i} className={`${name}-line`}>
|
||||
{line} <br className={`${name}-linebreak`}/>
|
||||
</span>)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default Content
|
||||
export default Content;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react'
|
||||
import {shallow, mount} from 'enzyme'
|
||||
import {expect} from 'chai'
|
||||
import CommentContent from '../CommentContent'
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import {expect} from 'chai';
|
||||
import CommentContent from '../CommentContent';
|
||||
|
||||
describe('CommentContent', () => {
|
||||
it('should render content', () => {
|
||||
const render = shallow(<CommentContent content="test"/>)
|
||||
expect(render.contains('test')).to.be.truthy
|
||||
})
|
||||
})
|
||||
const render = shallow(<CommentContent content="test"/>);
|
||||
expect(render.contains('test')).to.be.truthy;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import React from 'react'
|
||||
import React from 'react';
|
||||
|
||||
const name='coral-plugin-flags'
|
||||
const name = 'coral-plugin-flags';
|
||||
|
||||
const FlagButton = ({flag, item_id, postAction, currentUser, addNotification}) => {
|
||||
const flagged = flag && flag.includes(currentUser)
|
||||
const flagged = flag && flag.includes(currentUser);
|
||||
const onFlagClick = () => {
|
||||
postAction(item_id, 'flag', currentUser)
|
||||
addNotification('success', 'Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.')
|
||||
postAction(item_id, 'flag', currentUser);
|
||||
addNotification('success', 'Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.');
|
||||
|
||||
}
|
||||
return <div className={name + '-container'}>
|
||||
<button onClick={onFlagClick} className={name + '-button'}>
|
||||
<i className={name + '-icon material-icons'}
|
||||
};
|
||||
return <div className={`${name }-container`}>
|
||||
<button onClick={onFlagClick} className={`${name }-button`}>
|
||||
<i className={`${name }-icon material-icons`}
|
||||
style={flagged ? styles.flaggedIcon : styles.unflaggedIcon}
|
||||
aria-hidden={true}>flag</i>
|
||||
{
|
||||
flagged
|
||||
? <span className={name + '-button-text'}>Flagged</span>
|
||||
: <span className={name + '-button-text'}>Flag</span>
|
||||
? <span className={`${name }-button-text`}>Flagged</span>
|
||||
: <span className={`${name }-button-text`}>Flag</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default FlagButton
|
||||
export default FlagButton;
|
||||
|
||||
const styles = {
|
||||
flaggedIcon: {
|
||||
@@ -32,4 +32,4 @@ const styles = {
|
||||
unflaggedIcon: {
|
||||
color: 'inherit'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react'
|
||||
import {I18n} from '../coral-framework'
|
||||
import React from 'react';
|
||||
import {I18n} from '../coral-framework';
|
||||
|
||||
const lang = new I18n()
|
||||
const name = 'coral-plugin-pubdate'
|
||||
const lang = new I18n();
|
||||
const name = 'coral-plugin-pubdate';
|
||||
|
||||
const PubDate = ({created_at}) => <div className={name + '-text'}>
|
||||
const PubDate = ({created_at}) => <div className={`${name }-text`}>
|
||||
{lang.timeago(created_at)}
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
export default PubDate
|
||||
export default PubDate;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react'
|
||||
import CommentBox from '../coral-plugin-commentbox/CommentBox'
|
||||
import React from 'react';
|
||||
import CommentBox from '../coral-plugin-commentbox/CommentBox';
|
||||
|
||||
const name = 'coral-plugin-replies'
|
||||
const name = 'coral-plugin-replies';
|
||||
|
||||
const ReplyBox = (props) => <div
|
||||
className={name + '-textarea'}
|
||||
className={`${name }-textarea`}
|
||||
style={props.styles && props.styles.container}>
|
||||
{
|
||||
props.showReply && <CommentBox
|
||||
@@ -17,6 +17,6 @@ const ReplyBox = (props) => <div
|
||||
comments = {props.child}
|
||||
reply = {true}/>
|
||||
}
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
export default ReplyBox
|
||||
export default ReplyBox;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react'
|
||||
import {I18n} from '../coral-framework'
|
||||
import React from 'react';
|
||||
import {I18n} from '../coral-framework';
|
||||
|
||||
const name = 'coral-plugin-replies'
|
||||
const name = 'coral-plugin-replies';
|
||||
|
||||
const ReplyButton = (props) => <button
|
||||
className={name + '-reply-button'}
|
||||
onClick={(e) => props.updateItem(props.id || props.parent_id, 'showReply', true)}>
|
||||
<i className={name + '-icon material-icons'}
|
||||
className={`${name }-reply-button`}
|
||||
onClick={() => props.updateItem(props.id || props.parent_id, 'showReply', true)}>
|
||||
<i className={`${name }-icon material-icons`}
|
||||
aria-hidden={true}>reply</i>
|
||||
{lang.t('reply')}
|
||||
</button>
|
||||
</button>;
|
||||
|
||||
export default ReplyButton
|
||||
export default ReplyButton;
|
||||
|
||||
const lang = new I18n({
|
||||
en: {
|
||||
@@ -20,4 +20,4 @@ const lang = new I18n({
|
||||
es: {
|
||||
'reply': '¡traduceme!'
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import ReplyBox from './ReplyBox'
|
||||
import ReplyButton from './ReplyButton'
|
||||
import ReplyBox from './ReplyBox';
|
||||
import ReplyButton from './ReplyButton';
|
||||
|
||||
export {
|
||||
ReplyBox,
|
||||
ReplyButton
|
||||
}
|
||||
};
|
||||
|
||||
@@ -55,6 +55,8 @@
|
||||
"uuid": "^2.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^7.1.0",
|
||||
"babel-jest": "^15.0.0",
|
||||
"autoprefixer": "^6.5.0",
|
||||
"babel-core": "^6.18.2",
|
||||
"babel-loader": "^6.2.7",
|
||||
@@ -71,6 +73,7 @@
|
||||
"copy-webpack-plugin": "^4.0.0",
|
||||
"css-loader": "^0.25.0",
|
||||
"eslint": "^3.9.1",
|
||||
"eslint-plugin-react": "^6.6.0",
|
||||
"exports-loader": "^0.6.3",
|
||||
"hammerjs": "^2.0.8",
|
||||
"immutable": "^3.8.1",
|
||||
|
||||
Reference in New Issue
Block a user