Merge branch 'master' of https://github.com/coralproject/talk into settings-in-stream

This commit is contained in:
David Jay
2016-11-28 17:25:07 -05:00
72 changed files with 1261 additions and 708 deletions
+3 -3
View File
@@ -1,9 +1,9 @@
import React from 'react';
import {Router, Route, IndexRoute, browserHistory} from 'react-router';
import ModerationQueue from 'containers/ModerationQueue';
import CommentStream from 'containers/CommentStream';
import Configure from 'containers/Configure';
import ModerationQueue from 'containers/ModerationQueue/ModerationQueue';
import CommentStream from 'containers/CommentStream/CommentStream';
import Configure from 'containers/Configure/Configure';
import CommunityContainer from 'containers/Community/CommunityContainer';
import LayoutContainer from 'containers/LayoutContainer';
+14
View File
@@ -17,3 +17,17 @@ export const checkLogin = () => dispatch => {
})
.catch(error => dispatch(checkLoginFailure(error)));
};
// LogOut Actions
const logOutRequest = () => ({type: actions.LOGOUT_REQUEST});
const logOutSuccess = () => ({type: actions.LOGOUT_SUCCESS});
const logOutFailure = () => ({type: actions.LOGOUT_FAILURE});
export const logout = () => dispatch => {
dispatch(logOutRequest());
fetch(`${base}/auth`, getInit('DELETE'))
.then(handleResp)
.then(() => dispatch(logOutSuccess()))
.catch(error => dispatch(logOutFailure(error)));
};
+2 -28
View File
@@ -1,3 +1,5 @@
import {base, handleResp, getInit} from '../helpers/response';
export const SETTINGS_LOADING = 'SETTINGS_LOADING';
export const SETTINGS_RECEIVED = 'SETTINGS_RECEIVED';
export const SETTINGS_FETCH_ERROR = 'SETTINGS_FETCH_ERROR';
@@ -8,34 +10,6 @@ export const SAVE_SETTINGS_LOADING = 'SAVE_SETTINGS_LOADING';
export const SAVE_SETTINGS_SUCCESS = 'SAVE_SETTINGS_SUCCESS';
export const SAVE_SETTINGS_FAILED = 'SAVE_SETTINGS_FAILED';
const base = '/api/v1';
const getInit = (method, body) => {
const headers = new Headers({
'Content-Type': 'application/json',
'Accept': 'application/json'
});
const init = {method, headers};
if (method.toLowerCase() !== 'get') {
init.body = JSON.stringify(body);
}
return init;
};
const handleResp = res => {
if (res.status === 401) {
throw new Error('Not Authorized to make this request');
} else if (res.status > 399) {
throw new Error('Error! Status ', res.status);
} else if (res.status === 204) {
return res.text();
} else {
return res.json();
}
};
export const fetchSettings = () => dispatch => {
dispatch({type: SETTINGS_LOADING});
fetch(`${base}/settings`, getInit('GET'))
@@ -0,0 +1,12 @@
.layout {
max-width: 800px;
margin: 0 auto;
}
.layout h1 {
font-size: 40px;
}
.layout img {
width: 100%;
}
@@ -0,0 +1,13 @@
import React from 'react';
import {Layout} from 'react-mdl';
import styles from './FullLoading.css';
import {CoralLogo} from 'coral-ui';
export const FullLoading = () => (
<Layout fixedDrawer>
<div className={styles.layout} >
<h1>Loading</h1>
<CoralLogo />
</div>
</Layout>
);
@@ -1,6 +1,5 @@
.header {
background: #505050;
overflow: hidden;
}
.header > div {
@@ -14,8 +13,35 @@
background: #232323;
}
.version {
.rightPanel {
position: absolute;
right: 0;
width: 50px;
width: 170px;
}
.rightPanel ul {
list-style: none;
line-height: 38px;
}
.rightPanel li {
display: inline-block;
float: right;
margin-left: 15px;
}
.rightPanel .settings {
vertical-align: middle;
border-radius: 3px;
border: solid 1px #9e9e9e;
line-height: 10px;
}
.rightPanel .settings > div {
position: relative;
}
.rightPanel .settings:hover {
background: rgba(158, 158, 158, 0.69);
cursor: pointer;
}
+22 -7
View File
@@ -1,21 +1,36 @@
import React from 'react';
import {Navigation, Header} from 'react-mdl';
import {Navigation, Header, IconButton, MenuItem, Menu} from 'react-mdl';
import {Link, IndexLink} from 'react-router';
import styles from './Header.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations.json';
import {Logo} from './Logo';
export default () => (
export default ({handleLogout}) => (
<Header className={styles.header}>
<Logo />
<Navigation>
<IndexLink className={styles.navLink} to="/admin" activeClassName={styles.active}>{lang.t('configure.moderate')}</IndexLink>
<Link className={styles.navLink} to="/admin/community" activeClassName={styles.active}>{lang.t('configure.community')}</Link>
<Link className={styles.navLink} to="/admin/configure" activeClassName={styles.active}>{lang.t('configure.configure')}</Link>
<IndexLink className={styles.navLink} to="/admin"
activeClassName={styles.active}>{lang.t('configure.moderate')}</IndexLink>
<Link className={styles.navLink} to="/admin/community"
activeClassName={styles.active}>{lang.t('configure.community')}</Link>
<Link className={styles.navLink} to="/admin/configure"
activeClassName={styles.active}>{lang.t('configure.configure')}</Link>
</Navigation>
<div className={styles.version}>
{`v${process.env.VERSION}`}
<div className={styles.rightPanel}>
<ul>
<li className={styles.settings}>
<div>
<IconButton name="settings" id="menu-settings"/>
<Menu target="menu-settings" align="right">
<MenuItem onClick={handleLogout}>Sign Out</MenuItem>
</Menu>
</div>
</li>
<li>
{`v${process.env.VERSION}`}
</li>
</ul>
</div>
</Header>
);
@@ -4,9 +4,9 @@ import Header from './Header';
import Drawer from './Drawer';
import styles from './Layout.css';
export const Layout = ({children}) => (
export const Layout = ({children, ...props}) => (
<LayoutMDL fixedDrawer>
<Header />
<Header {...props}/>
<Drawer />
<div className={styles.layout} >
{children}
@@ -1,7 +1,9 @@
.logo h1 {
color: #272727;
font-size: 20px;
padding: 0 30px;
margin: 0;
line-height: 60px;
padding: 0 20px;
}
.logo span {
@@ -13,6 +15,7 @@
.logo {
background: #E5E5E5;
height: 100%;
}
@@ -31,7 +31,7 @@ class CommentStream extends React.Component {
// The only action for now is flagging
onClickAction (action, id) {
if (action === 'flagged') {
if (action === 'flag') {
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.'});
@@ -1,7 +1,6 @@
import React from 'react';
import {connect} from 'react-redux';
import {fetchSettings, updateSettings, saveSettingsToServer} from '../actions/settings';
import {fetchSettings, updateSettings, saveSettingsToServer} from '../../actions/settings';
import {
List,
ListItem,
@@ -14,7 +13,7 @@ import {
} from 'react-mdl';
import styles from './Configure.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations.json';
import translations from '../../translations.json';
class Configure extends React.Component {
constructor (props) {
@@ -23,9 +22,13 @@ class Configure extends React.Component {
this.state = {activeSection: 'comments', copied: false};
this.copyToClipBoard = this.copyToClipBoard.bind(this);
// Update settings
this.updateModeration = this.updateModeration.bind(this);
// InfoBox has two settings. Enable or not and the content of it if it is enable.
this.updateInfoBoxEnable = this.updateInfoBoxEnable.bind(this);
this.updateInfoBoxContent = this.updateInfoBoxContent.bind(this);
this.saveSettings = this.saveSettings.bind(this);
}
@@ -1,37 +1,31 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {Layout} from '../components/ui/Layout';
import {checkLogin} from '../actions/auth';
import {NotFound} from '../components/NotFound';
import {checkLogin, logout} from '../actions/auth';
import {FullLoading} from '../components/FullLoading';
import {PermissionRequired} from '../components/PermissionRequired';
class LayoutContainer extends Component {
componentWillMount () {
this.props.checkLogin();
const {checkLogin} = this.props;
checkLogin();
}
render () {
const {isAdmin, loggedIn} = this.props.auth;
if (!loggedIn) {
return <NotFound />;
}
if (!isAdmin && loggedIn) {
return <PermissionRequired />;
}
return <Layout {...this.props} />;
const {isAdmin, loggedIn, loadingUser} = this.props.auth;
if (loadingUser) { return <FullLoading />; }
if (!isAdmin) { return <PermissionRequired />; }
if (isAdmin && loggedIn) { return <Layout {...this.props} />; }
return <FullLoading />;
}
}
LayoutContainer.propTypes = {};
const mapStateToProps = state => ({
auth: state.auth.toJS()
});
const mapDispatchToProps = dispatch => ({
checkLogin: () => dispatch(checkLogin()),
handleLogout: () => dispatch(logout())
});
export default connect(
@@ -1,16 +1,22 @@
import React from 'react';
import {connect} from 'react-redux';
import key from 'keymaster';
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/modules/i18n/i18n';
import translations from '../translations.json';
import translations from '../../translations.json';
/*
* Renders the moderation queue as a tabbed layout with 3 moderation
* queues filtered by status (Untouched, Rejected and Approved)
* queues :
* * pending: filtered by status Untouched
* * rejected: filtered by status Rejected
* * flagged: with a flagged action on them
*/
class ModerationQueue extends React.Component {
+7 -1
View File
@@ -9,19 +9,25 @@ const initialState = Map({
export default function auth (state = initialState, action) {
switch (action.type) {
case actions.CHECK_LOGIN_REQUEST:
return state
.set('loadingUser', true);
case actions.CHECK_LOGIN_FAILURE:
return state
.set('loggedIn', false)
.set('loadingUser', false)
.set('user', null);
case actions.CHECK_LOGIN_SUCCESS:
return state
.set('loggedIn', true)
.set('loadingUser', false)
.set('isAdmin', action.isAdmin)
.set('user', action.user);
case actions.LOGOUT_SUCCESS:
return state
.set('loggedIn', false)
.set('user', null);
.set('user', null)
.set('isAdmin', false);
default :
return state;
}
+15 -21
View File
@@ -1,3 +1,4 @@
import {base, handleResp, getInit} from '../helpers/response';
/**
* The adapter is a redux middleware that interecepts the actions that need
@@ -7,9 +8,6 @@
* for the coral but also for wordpress comments, disqus and many more.
*/
// Default headers for json payloads.
const jsonHeader = new Headers({'Content-Type': 'application/json'});
// Intercept redux actions and act over the ones we are interested
export default store => next => action => {
@@ -35,11 +33,11 @@ export default store => next => action => {
const fetchModerationQueueComments = store =>
Promise.all([
fetch('/api/v1/queue/comments/pending'),
fetch('/api/v1/comments?status=rejected'),
fetch('/api/v1/comments?action=flag')
fetch(`${base}/queue/comments/pending`, getInit('GET')),
fetch(`${base}/comments?status=rejected`, getInit('GET')),
fetch(`${base}/comments?action_type=flag`, getInit('GET'))
])
.then(res => Promise.all(res.map(r => r.json())))
.then(res => Promise.all(res.map(handleResp)))
.then(res => {
res[2] = res[2].map(comment => { comment.flagged = true; return comment; });
return res.reduce((prev, curr) => prev.concat(curr), []);
@@ -51,26 +49,22 @@ Promise.all([
// 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.get('id')}/status`, {
method: 'PUT',
headers: jsonHeader,
body: JSON.stringify({status: comment.get('status')})
})
.then(res => res.json())
fetch(`${base}/comments/${comment.get('id')}/status`, getInit('PUT', {status: comment.get('status')}))
.then(handleResp)
.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', {
method: 'POST',
body: JSON.stringify({
const createComment = (store, name, comment) => {
const body = {
status: 'Untouched',
body: comment,
name: name,
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}));
};
return fetch(`${base}/comments`, getInit('POST', body))
.then(handleResp)
.then(res => store.dispatch({type: 'COMMENT_CREATE_SUCCESS', comment: res}))
.catch(error => store.dispatch({type: 'COMMENT_CREATE_FAILED', error}));
};
+22 -5
View File
@@ -61,8 +61,12 @@ class CommentStream extends Component {
// Set up messaging between embedded Iframe an parent component
// Using recommended Pym init code which violates .eslint standards
const pym = new Pym.Child({polling: 100});
const path = /https?\:\/\/([^?]+)/.exec(pym.parentUrl);
this.props.getStream(path && path[1] || window.location);
if (/https?\:\/\/([^?]+)/.test(pym.parentUrl)) {
this.props.getStream(pym.parentUrl);
} else {
this.props.getStream(window.location);
}
}
render () {
@@ -121,7 +125,8 @@ class CommentStream extends Component {
<div className="commentActionsLeft">
<ReplyButton
updateItem={this.props.updateItem}
id={commentId}/>
id={commentId}
showReply={comment.showReply}/>
<LikeButton
addNotification={this.props.addNotification}
id={commentId}
@@ -162,13 +167,14 @@ class CommentStream extends Component {
let reply = this.props.items.comments[replyId];
return <div className="reply" key={replyId}>
<hr aria-hidden={true}/>
<AuthorName author={users[comment.author_id]}/>
<AuthorName author={users[reply.author_id]}/>
<PubDate created_at={reply.created_at}/>
<Content body={reply.body}/>
<div className="replyActionsLeft">
<ReplyButton
updateItem={this.props.updateItem}
parent_id={reply.parent_id}/>
id={replyId}
showReply={reply.showReply}/>
<LikeButton
addNotification={this.props.addNotification}
id={replyId}
@@ -194,6 +200,17 @@ class CommentStream extends Component {
asset_id={rootItemId}
/>
</div>
<ReplyBox
addNotification={this.props.addNotification}
postItem={this.props.postItem}
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}
author={user}
parent_id={commentId}
child_id={replyId}
premod={this.props.config.moderation}
showReply={reply.showReply}/>
</div>;
})
}
+7 -1
View File
@@ -127,7 +127,13 @@ const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error});
export const checkLogin = () => dispatch => {
dispatch(checkLoginRequest());
fetch(`${base}/auth`, getInit('GET'))
.then(handleResp)
.then((res) => {
if (res.status !== 200) {
throw new Error('not logged in');
}
return res.json();
})
.then(user => dispatch(checkLoginSuccess(user)))
.catch(error => dispatch(checkLoginFailure(error)));
};
+16 -19
View File
@@ -119,22 +119,20 @@ export function getStream (assetUrl) {
.then((json) => {
/* Add items to the store */
const itemTypes = Object.keys(json);
for (let i = 0; i < itemTypes.length; i++ ) {
if (itemTypes[i] === 'actions') {
for (let j = 0; j < json[itemTypes[i]].length; j++ ) {
let action = json[itemTypes[i]][j];
Object.keys(json).forEach(type => {
if (type === 'actions') {
json[type].forEach(action => {
action.id = `${action.action_type}_${action.item_id}`;
dispatch(addItem(action, 'actions'));
}
} else if (itemTypes[i] === 'settings') {
return dispatch({type: UPDATE_SETTINGS, config: fromJS(json[itemTypes[i]])});
});
} else if (type === 'settings') {
dispatch({type: UPDATE_SETTINGS, config: fromJS(json[type])});
} else {
for (let j = 0; j < json[itemTypes[i]].length; j++ ) {
dispatch(addItem(json[itemTypes[i]][j], itemTypes[i]));
}
json[type].forEach(item => {
dispatch(addItem(item, type));
});
}
}
});
const assetId = json.assets[0].id;
@@ -157,15 +155,14 @@ export function getStream (assetUrl) {
dispatch(updateItem(assetId, 'comments', rels.rootComments, 'assets'));
const childKeys = Object.keys(rels.childComments);
for (let i = 0; i < childKeys.length; i++ ) {
dispatch(updateItem(childKeys[i], 'children', rels.childComments[childKeys[i]].reverse(), 'comments'));
}
Object.keys(rels.childComments).forEach(key => {
dispatch(updateItem(key, 'children', rels.childComments[key].reverse(), 'comments'));
});
/* Hydrate actions on comments */
for (let i = 0; i < json.actions.length; i++ ) {
dispatch(updateItem(json.actions[i].item_id, json.actions[i].action_type, json.actions[i].id, 'comments'));
}
json.actions.forEach(action => {
dispatch(updateItem(action.item_id, action.action_type, action.id, 'comments'));
});
return (json);
});
+2 -2
View File
@@ -22,7 +22,7 @@ class CommentBox extends Component {
}
postComment = () => {
const {postItem, updateItem, id, parent_id, addNotification, appendItemArray, premod, author} = this.props;
const {postItem, updateItem, id, parent_id, child_id, addNotification, appendItemArray, premod, author} = this.props;
let comment = {
body: this.state.body,
asset_id: id,
@@ -38,7 +38,7 @@ class CommentBox extends Component {
related = 'comments';
parent_type = 'assets';
}
updateItem(parent_id, 'showReply', false, 'comments');
updateItem(child_id || parent_id, 'showReply', false, 'comments');
postItem(comment, 'comments')
.then((comment_id) => {
if (premod === 'pre') {
+1 -1
View File
@@ -6,7 +6,7 @@ const name = 'coral-plugin-replies';
const ReplyButton = (props) => <button
className={`${name}-reply-button`}
onClick={() => props.updateItem(props.id || props.parent_id, 'showReply', true, 'comments')}>
onClick={() => props.updateItem(props.id, 'showReply', !props.showReply, 'comments')}>
{lang.t('reply')}
<i className={`${name}-icon material-icons`}
aria-hidden={true}>reply</i>