Merge branch 'master' of github.com:coralproject/talk into passport

This commit is contained in:
Belen Curcio
2016-11-17 10:54:16 -03:00
46 changed files with 1078 additions and 458 deletions
+8 -1
View File
@@ -5,7 +5,8 @@ import {
FETCH_COMMENTERS_SUCCESS,
FETCH_COMMENTERS_FAILURE,
SORT_UPDATE,
COMMENTERS_NEW_PAGE
COMMENTERS_NEW_PAGE,
SET_ROLE
} from '../constants/community';
import {base, getInit, handleResp} from '../helpers/response';
@@ -40,3 +41,9 @@ export const newPage = () => ({
type: COMMENTERS_NEW_PAGE
});
export const setRole = (id, role) => dispatch => {
return fetch(`${base}/user/${id}/role`, getInit('POST', {role}))
.then(() => {
return dispatch({type: SET_ROLE, id, role});
});
};
+2 -2
View File
@@ -3,8 +3,8 @@ 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/modules/i18n/i18n';
import translations from '../translations';
import I18n from 'coral-framework/i18n/i18n';
import translations from '../translations.json';
// Render a single comment for the list
export default props => (
+8 -4
View File
@@ -2,22 +2,26 @@ import React from 'react';
import {Layout, Navigation, Drawer, Header} from 'react-mdl';
import {Link} from 'react-router';
import styles from './Header.css';
import I18n from 'coral-framework/i18n/i18n';
import translations from '../translations.json';
// 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>
<Link className={styles.navLink} to={'/admin/'}>{lang.t('configure.moderate')}</Link>
<Link className={styles.navLink} to={'/admin/configure'}>{lang.t('Configure')}</Link>
</Navigation>
</Header>
<Drawer>
<Navigation>
<Link className={styles.navLink} to={'/admin/'}>Moderate</Link>
<Link className={styles.navLink} to={'/admin/configure'}>Configure</Link>
<Link className={styles.navLink} to={'/admin/'}>{lang.t('configure.moderate')}</Link>
<Link className={styles.navLink} to={'/admin/configure'}>{lang.t('configure.Configure')}</Link>
</Navigation>
</Drawer>
{props.children}
</Layout>
);
const lang = new I18n(translations);
@@ -1,5 +1,5 @@
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations';
import I18n from 'coral-framework/i18n/i18n';
import translations from '../translations.json';
import React from 'react';
import Modal from 'components/Modal';
import styles from './ModerationKeysModal.css';
@@ -2,13 +2,17 @@ import React from 'react';
import {Navigation, Drawer} from 'react-mdl';
import {Link} from 'react-router';
import styles from './Header.css';
import I18n from 'coral-framework/i18n/i18n';
import translations from '../../translations.json';
export default () => (
<Drawer>
<Navigation>
<Link className={styles.navLink} to="/admin">Moderate</Link>
<Link className={styles.navLink} to="/admin/community">Community</Link>
<Link className={styles.navLink} to="/admin/configure">Configure</Link>
<Link className={styles.navLink} to="/admin">{lang.t('configure.moderate')}</Link>
<Link className={styles.navLink} to="/admin/community">{lang.t('configure.community')}</Link>
<Link className={styles.navLink} to="/admin/configure">{lang.t('configure.configure')}</Link>
</Navigation>
</Drawer>
);
const lang = new I18n(translations);
@@ -2,16 +2,20 @@ import React from 'react';
import {Navigation, Header} from 'react-mdl';
import {Link} from 'react-router';
import styles from './Header.css';
import I18n from 'coral-framework/i18n/i18n';
import translations from '../../translations.json';
export default () => (
<Header title='Talk'>
<Navigation>
<Link className={styles.navLink} to="/admin">Moderate</Link>
<Link className={styles.navLink} to="/admin/community">Community</Link>
<Link className={styles.navLink} to="/admin/configure">Configure</Link>
<Link className={styles.navLink} to="/admin">{lang.t('configure.moderate')}</Link>
<Link className={styles.navLink} to="/admin/community">{lang.t('configure.community')}</Link>
<Link className={styles.navLink} to="/admin/configure">{lang.t('configure.configure')}</Link>
<span>
{`v${process.env.VERSION}`}
</span>
</Navigation>
</Header>
);
const lang = new I18n(translations);
@@ -3,3 +3,4 @@ export const FETCH_COMMENTERS_SUCCESS = 'FETCH_COMMENTERS_SUCCESS';
export const FETCH_COMMENTERS_FAILURE = 'FETCH_COMMENTERS_FAILURE';
export const SORT_UPDATE = 'SORT_UPDATE';
export const COMMENTERS_NEW_PAGE = 'COMMENTERS_NEW_PAGE';
export const SET_ROLE = 'SET_ROLE';
@@ -1,6 +1,6 @@
import React from 'react';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../../translations';
import I18n from 'coral-framework/i18n/i18n';
import translations from '../../translations.json';
import {Grid, Cell} from 'react-mdl';
import styles from './Community.css';
@@ -19,6 +19,10 @@ const tableHeaders = [
{
title: lang.t('community.account_creation_date'),
field: 'created_at'
},
{
title: lang.t('community.newsroom_role'),
field: 'role'
}
];
@@ -1,34 +1,66 @@
import React from 'react';
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {SelectField, Option} from 'react-mdl-selectfield';
import styles from './Community.css';
import I18n from 'coral-framework/i18n/i18n';
import translations from '../../translations';
import {setRole} from '../../actions/community';
const Table = ({headers, data, onHeaderClickHandler}) => (
<table className={`mdl-data-table ${styles.dataTable}`}>
<thead>
<tr>
{headers.map((header, i) =>(
<th
key={i}
className="mdl-data-table__cell--non-numeric"
onClick={() => onHeaderClickHandler({field: header.field})}>
{header.title}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, i)=> (
<tr key={i}>
<td className="mdl-data-table__cell--non-numeric">
{row.displayName}
<span className={styles.email}>{row.profiles.map(({id}) => id)}</span>
</td>
<td className="mdl-data-table__cell--non-numeric">
{row.created_at}
</td>
</tr>
))}
</tbody>
</table>
);
const lang = new I18n(translations);
export default Table;
class Table extends Component {
constructor (props) {
super(props);
this.onRoleChange = this.onRoleChange.bind(this);
}
onRoleChange (id, role) {
this.props.dispatch(setRole(id, role));
}
render () {
const {headers, commenters, onHeaderClickHandler} = this.props;
return (
<table className={`mdl-data-table ${styles.dataTable}`}>
<thead>
<tr>
{headers.map((header, i) =>(
<th
key={i}
className="mdl-data-table__cell--non-numeric"
onClick={() => onHeaderClickHandler({field: header.field})}>
{header.title}
</th>
))}
</tr>
</thead>
<tbody>
{commenters.map((row, i)=> (
<tr key={i}>
<td className="mdl-data-table__cell--non-numeric">
{row.displayName}
<span className={styles.email}>{row.profiles.map(({id}) => id)}</span>
</td>
<td className="mdl-data-table__cell--non-numeric">
{row.created_at}
</td>
<td className="mdl-data-table__cell--non-numeric">
<SelectField label={'Select me'} value={row.roles[0] || ''}
label={lang.t('community.role')}
onChange={role => this.onRoleChange(row.id, role)}>
<Option value={''}>.</Option>
<Option value={'moderator'}>{lang.t('community.moderator')}</Option>
<Option value={'admin'}>{lang.t('community.admin')}</Option>
</SelectField>
</td>
</tr>
))}
</tbody>
</table>
);
}
}
export default connect(state => ({commenters: state.community.get('commenters')}))(Table);
@@ -23,6 +23,21 @@
cursor: pointer;
}
.configSettingInfoBox {
border: 1px solid #ccc;
border-radius: 4px;
margin-bottom: 10px;
cursor: pointer;
width: auto;
height: auto;
text-align: left;
}
.configSettingInfoBox p {
font-size: 12px;
bottom: 0;
}
.configSettingEmbed {
border: 1px solid #ccc;
border-radius: 4px;
@@ -53,3 +68,7 @@
font-size: 14px;
letter-spacing: 0.03em;
}
.hidden {
display: none;
}
+42 -22
View File
@@ -7,14 +7,14 @@ import {
ListItem,
ListItemContent,
ListItemAction,
//Textfield,
Textfield,
Checkbox,
Button,
Icon
} from 'react-mdl';
import styles from './Configure.css';
import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations';
import I18n from 'coral-framework/i18n/i18n';
import translations from '../translations.json';
class Configure extends React.Component {
constructor (props) {
@@ -24,6 +24,8 @@ class Configure extends React.Component {
this.copyToClipBoard = this.copyToClipBoard.bind(this);
this.updateModeration = this.updateModeration.bind(this);
this.updateInfoBoxEnable = this.updateInfoBoxEnable.bind(this);
this.updateInfoBoxContent = this.updateInfoBoxContent.bind(this);
this.saveSettings = this.saveSettings.bind(this);
}
@@ -36,6 +38,16 @@ class Configure extends React.Component {
this.props.dispatch(updateSettings({moderation}));
}
updateInfoBoxEnable () {
const infoBoxEnable = !this.props.settings.infoBoxEnable;
this.props.dispatch(updateSettings({infoBoxEnable}));
}
updateInfoBoxContent (event) {
const infoBoxContent = event.target.value;
this.props.dispatch(updateSettings({infoBoxContent}));
}
saveSettings () {
this.props.dispatch(saveSettingsToServer());
}
@@ -48,22 +60,30 @@ class Configure extends React.Component {
onClick={this.updateModeration}
checked={this.props.settings.moderation === 'pre'} />
</ListItemAction>
Enable pre-moderation
{lang.t('configure.enable-pre-moderation')}
</ListItem>
{/*
<ListItem className={styles.configSetting}>
<ListItemAction><Checkbox /></ListItemAction>
Include Comment Stream Description for Readers
<ListItem threeLine className={styles.configSettingInfoBox}>
<ListItemAction>
<Checkbox
onClick={this.updateInfoBoxEnable}
checked={this.props.settings.infoBoxEnable} />
</ListItemAction>
<ListItemContent>
{lang.t('configure.include-comment-stream')}
<p>
{lang.t('configure.include-comment-stream-desc')}
</p>
</ListItemContent>
</ListItem>
<ListItem className={styles.configSetting}>
<ListItemAction><Checkbox /></ListItemAction>
Limit Comment Length
<Textfield
pattern='-?[0-9]*(\.[0-9]+)?'
error='Input is not a number!'
label='Maximum Characters' />
<ListItem className={`${styles.configSettingInfoBox} ${this.props.settings.infoBoxEnable ? null : styles.hidden}`} >
<ListItemContent>
<Textfield
onChange={this.updateInfoBoxContent}
value={this.props.settings.infoBoxContent}
label={lang.t('configure.include-text')}
rows={3}/>
</ListItemContent>
</ListItem>
*/}
</List>;
}
@@ -84,7 +104,7 @@ class Configure extends React.Component {
return <List>
<ListItem className={styles.configSettingEmbed}>
<p>Copy and paste code below into your CMS to embed your comment box in your articles</p>
<p>{lang.t('configure.copy-and-paste')}</p>
<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')}
@@ -100,8 +120,8 @@ class Configure extends React.Component {
render () {
let pageTitle = this.state.activeSection === 'comments'
? 'Comment Settings'
: 'Embed Comment Stream';
? lang.t('configure.comment-settings')
: lang.t('configure.embed-comment-stream');
if (this.props.fetchingSettings) {
pageTitle += ' - Loading...';
@@ -114,16 +134,16 @@ class Configure extends React.Component {
<ListItem className={styles.settingOption}>
<ListItemContent
onClick={this.changeSection.bind(this, 'comments')}
icon='settings'>Comment Settings</ListItemContent>
icon='settings'>{lang.t('configure.comment-settings')}</ListItemContent>
</ListItem>
<ListItem className={styles.settingOption}>
<ListItemContent
onClick={this.changeSection.bind(this, 'embed')}
icon='code'>Embed Comment Stream</ListItemContent>
icon='code'>{lang.t('configure.embed-comment-stream')}</ListItemContent>
</ListItem>
</List>
<Button raised colored onClick={this.saveSettings}>
<Icon name='save' /> Save Changes
<Icon name='save' /> {lang.t('configure.save-changes')}
</Button>
</div>
<div className={styles.mainContent}>
@@ -5,8 +5,8 @@ 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';
import I18n from 'coral-framework/i18n/i18n';
import translations from '../translations.json';
/*
* Renders the moderation queue as a tabbed layout with 3 moderation
@@ -91,7 +91,7 @@ class ModerationQueue extends React.Component {
commentIds={
comments
.get('ids')
.filter(id =>
.filter(id =>
comments
.get('byId')
.get(id)
+9 -1
View File
@@ -4,7 +4,8 @@ import {
FETCH_COMMENTERS_REQUEST,
FETCH_COMMENTERS_FAILURE,
FETCH_COMMENTERS_SUCCESS,
SORT_UPDATE
SORT_UPDATE,
SET_ROLE
} from '../constants/community';
const initialState = Map({
@@ -37,6 +38,13 @@ export default function community (state = initialState, action) {
})
.set('commenters', commenters); // Sets to normal array
}
case SET_ROLE : {
const commenters = state.get('commenters');
const idx = commenters.findIndex(el => el.id === action.id);
commenters[idx].roles[0] = action.role;
return state.set('commenters', commenters.map(id => id));
}
case SORT_UPDATE :
return state
.set('field', action.sort.field)
-47
View File
@@ -1,47 +0,0 @@
export default {
en: {
'community': {
username_and_email: 'Username and Email',
account_creation_date: 'Account Creation Date'
},
'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'
},
'embedlink': {
'copy': 'Copy to Clipboard'
}
},
es: {
'community': {
username_and_email: 'Usuario y E-mail',
account_creation_date: 'Fecha de creación de la cuenta'
},
'modqueue': {
'pending': 'pendiente',
'rejected': 'rechazado',
'flagged': 'marcado',
'shortcuts': 'Atajos de teclado',
'close': 'Cerrar'
},
'comment': {
'flagged': 'marcado',
'anon': 'Anónimo'
}
}
};
+81
View File
@@ -0,0 +1,81 @@
{
"en": {
"community": {
"username_and_email": "Username and Email",
"account_creation_date": "Account Creation Date",
"newsroom_role": "Newsroom Role",
"admin": "Administrator",
"moderator": "Moderator",
"role": "Select role..."
},
"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"
},
"embedlink": {
"copy": "Copy to Clipboard"
},
"configure": {
"enable-pre-moderation": "Enable pre-moderation",
"include-comment-stream": "Include Comment Stream Description for Readers.",
"include-comment-stream-desc": "Write a message to be added to the top of your comment stream. Pose a topic, include community guidelines, etc.",
"include-text": "Include your text here.",
"comment-settings": "Comment Settings",
"embed-comment-stream": "Embed Comment Stream",
"save-changes": "Save Changes",
"copy-and-paste": "Copy and paste code below into your CMS to embed your comment box in your articles",
"moderate": "Moderate",
"configure": "Configure",
"community": "Community"
}
},
"es": {
"community": {
"username_and_email": "Usuario y E-mail",
"account_creation_date": "Fecha de creación de la cuenta",
"newsroom_role": "Rol en la redacción",
"admin": "Administrador",
"moderator": "Moderador",
"role": "Select role..."
},
"modqueue": {
"pending": "pendiente",
"rejected": "rechazado",
"flagged": "marcado",
"shortcuts": "Atajos de teclado",
"close": "Cerrar"
},
"comment": {
"flagged": "marcado",
"anon": "Anónimo"
},
"configure": {
"enable-pre-moderation": "Habilitar pre-moderación",
"include-comment-stream": "Incluir la Descripción a un Hilo de Comentario para los y las Lectoras.",
"include-comment-stream-desc": "Escribir un mensaje que será agregado a la parte de arriba del tu hilo de comentarios. Por ejemplo, un tema, guias de comunidad, etc.",
"include-text": "Incluir tu texto aqui.",
"comment-settings": "Configuración de Comentarios",
"embed-comment-stream": "Colocar Hilo de Comentarios",
"save-changes": "Guardar Cambios",
"copy-and-paste": "Copiar y pegar el código de más abajo en tu CMS para colocar la caja de comentarios en tus articulos",
"moderate": "Moderar",
"configure": "Configurar",
"community": "Comunidad"
}
}
}
+62 -29
View File
@@ -7,6 +7,7 @@ import {
} from '../../coral-framework';
import {connect} from 'react-redux';
import CommentBox from '../../coral-plugin-commentbox/CommentBox';
import InfoBox from '../../coral-plugin-infobox/InfoBox';
import Content from '../../coral-plugin-commentcontent/CommentContent';
import PubDate from '../../coral-plugin-pubdate/PubDate';
import Count from '../../coral-plugin-comment-count/CommentCount';
@@ -14,11 +15,12 @@ import AuthorName from '../../coral-plugin-author-name/AuthorName';
import {ReplyBox, ReplyButton} from '../../coral-plugin-replies';
import Pym from 'pym.js';
import FlagButton from '../../coral-plugin-flags/FlagButton';
import LikeButton from '../../coral-plugin-likes/LikeButton';
import PermalinkButton from '../../coral-plugin-permalinks/PermalinkButton';
import SignInContainer from '../../coral-sign-in/containers/SignInContainer';
import UserBox from '../../coral-sign-in/components/UserBox';
const {addItem, updateItem, postItem, getStream, postAction, appendItemArray} = itemActions;
const {addItem, updateItem, postItem, getStream, postAction, deleteAction, appendItemArray} = itemActions;
const {addNotification, clearNotification} = notificationActions;
const {logout} = authActions;
@@ -39,6 +41,9 @@ const mapDispatchToProps = (dispatch) => ({
addNotification: (type, text) => dispatch(addNotification(type, text)),
clearNotification: () => dispatch(clearNotification()),
postAction: (item, action, user, itemType) => dispatch(postAction(item, action, user, itemType)),
deleteAction: (item, action, user, itemType) => {
return dispatch(deleteAction(item, action, user, itemType));
},
appendItemArray: (item, property, value, addToFront, itemType) =>
dispatch(appendItemArray(item, property, value, addToFront, itemType)),
logout: () => dispatch(logout())
@@ -55,8 +60,9 @@ 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');
const pym = new Pym.Child({polling: 100});
const path = /https?\:\/\/([^?]+)/.exec(pym.parentUrl)[1];
this.props.getStream(path);
}
render () {
@@ -76,7 +82,7 @@ class CommentStream extends Component {
// TODO: Replace teststream id with id from params
const rootItemId = 'assetTest';
const rootItemId = this.props.items.assets && Object.keys(this.props.items.assets)[0];
const rootItem = this.props.items.assets && this.props.items.assets[rootItemId];
const {loggedIn, user} = this.props.auth;
return <div>
@@ -84,6 +90,9 @@ class CommentStream extends Component {
rootItem
? <div>
<div id="commentBox">
<InfoBox
content={this.props.config.infoBoxContent}
enable={this.props.config.infoBoxEnable}/>
<Count
id={rootItemId}
items={this.props.items}/>
@@ -101,28 +110,40 @@ class CommentStream extends Component {
{!loggedIn && <SignInContainer />}
</div>
{
rootItem.comments.map((commentId) => {
rootItem.comments && rootItem.comments.map((commentId) => {
const comment = this.props.items.comments[commentId];
return <div className="comment" key={commentId}>
<hr aria-hidden={true}/>
<AuthorName name={comment.username}/>
<PubDate created_at={comment.created_at}/>
<Content body={comment.body}/>
<div className="commentActions">
<div className="commentActionsLeft">
<ReplyButton
updateItem={this.props.updateItem}
id={commentId}/>
<LikeButton
addNotification={this.props.addNotification}
id={commentId}
like={this.props.items.actions[comment.like]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
</div>
<div className="commentActionsRight">
<FlagButton
addNotification={this.props.addNotification}
id={commentId}
flag={this.props.items.actions[comment.flag]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
<ReplyButton
updateItem={this.props.updateItem}
id={commentId}/>
<PermalinkButton
comment_id={commentId}
asset_id={comment.asset_id}/>
<PermalinkButton
comment_id={commentId}
asset_id={comment.asset_id}/>
</div>
<ReplyBox
addNotification={this.props.addNotification}
@@ -142,23 +163,35 @@ class CommentStream extends Component {
<AuthorName name={reply.username}/>
<PubDate created_at={reply.created_at}/>
<Content body={reply.body}/>
<div className="replyActions">
<FlagButton
addNotification={this.props.addNotification}
id={replyId}
flag={this.props.items.actions[reply.flag]}
postAction={this.props.postAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
<ReplyButton
updateItem={this.props.updateItem}
parent_id={reply.parent_id}/>
<PermalinkButton
comment_id={reply.comment_id}
asset_id={reply.comment_id}
/>
</div>
<div className="replyActionsLeft">
<ReplyButton
updateItem={this.props.updateItem}
id={replyId}/>
<LikeButton
addNotification={this.props.addNotification}
id={replyId}
like={this.props.items.actions[reply.like]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
</div>
<div className="replyActionsRight">
<FlagButton
addNotification={this.props.addNotification}
id={replyId}
flag={this.props.items.actions[reply.flag]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
<PermalinkButton
comment_id={reply.comment_id}
asset_id={reply.comment_id}
/>
</div>
</div>;
})
}
+36 -2
View File
@@ -49,6 +49,23 @@ hr {
font-weight: bold;
}
/* Info Box Styles */
.coral-plugin-infobox-info {
position: fixed;
top: 0;
border: 0;
background: rgb(105,105,105);
color: white;
border-radius: 2px;
font-weight: bold;
display: block;
}
.hidden {
visibility: hidden;
display: none;
}
/* Comment Box Styles */
.coral-plugin-commentbox-container {
display: flex;
@@ -105,16 +122,33 @@ hr {
/* Comment Action Styles */
.commentActions, .replyActions {
.commentActionsRight, .replyActionsRight {
display: flex;
justify-content: flex-end;
width: 50%;
}
.commentActionsLeft, .replyActionsLeft {
display: flex;
justify-content: flex-start;
float: left;
width: 50%;
}
.commentActions .material-icons, .replyActions .material-icons {
.commentActionsLeft .material-icons,.commentActionsRight .material-icons,
.replyActionsLeft .material-icons, .replyActionsRight .material-icons {
font-size: 12px;
margin-left: 3px;
vertical-align: middle;
}
.likedButton {
color: rgb(0,134,227);
}
.flaggedIcon {
color: #F00;
}
/* Comment count styles */
.coral-plugin-comment-count-text {
margin-bottom: 15px;
+56 -49
View File
@@ -8,6 +8,23 @@ export const ADD_ITEM = 'ADD_ITEM';
export const UPDATE_ITEM = 'UPDATE_ITEM';
export const APPEND_ITEM_ARRAY = 'APPEND_ITEM_ARRAY';
const getInit = (method, body) => {
const headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
};
const init = {method, headers};
if (method.toLowerCase() !== 'get') {
init.body = JSON.stringify(body);
}
return init;
};
const responseHandler = response => {
return response.ok ? response.json() : Promise.reject(`${response.status} ${response.statusText}`);
};
/**
* Action creators
*/
@@ -77,14 +94,10 @@ export const appendItemArray = (id, property, value, add_to_front, item_type) =>
* @dispatches
* A set of items to the item store
*/
export function getStream (assetId) {
export function getStream (assetUrl) {
return (dispatch) => {
return fetch(`/api/v1/stream?asset_id=${assetId}`)
.then(
response => {
return response.ok ? response.json() : Promise.reject(`${response.status} ${response.statusText}`);
}
)
return fetch(`/api/v1/stream?asset_url=${encodeURIComponent(assetUrl)}`)
.then(responseHandler)
.then((json) => {
/* Add items to the store */
@@ -95,6 +108,8 @@ export function getStream (assetId) {
}
}
const assetId = json.assets[0].id;
/* Sort comments by date*/
json.comments.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
const rels = json.comments.reduce((h, item) => {
@@ -112,10 +127,7 @@ export function getStream (assetId) {
return h;
}, {rootComments: [], childComments: {}});
dispatch(addItem({
id: assetId,
comments: rels.rootComments,
}, 'assets'));
dispatch(updateItem(assetId, 'comments', rels.rootComments, 'assets'));
const childKeys = Object.keys(rels.childComments);
for (let i = 0; i < childKeys.length; i++ ) {
@@ -148,13 +160,8 @@ export function getStream (assetId) {
export function getItemsArray (ids) {
return (dispatch) => {
return fetch(`/v1/item/${ids}`)
.then(
response => {
return response.ok ? response.json()
: Promise.reject(`${response.status } ${ response.statusText}`);
}
)
return fetch(`/v1/item/${ids}`, getInit('GET'))
.then(responseHandler)
.then((json) => {
for (let i = 0; i < json.items.length; i++) {
dispatch(addItem(json.items[i]));
@@ -183,20 +190,8 @@ export function postItem (item, type, id) {
if (id) {
item.id = id;
}
let options = {
method: 'POST',
body: JSON.stringify(item),
headers: {
'Content-Type':'application/json'
}
};
return fetch(`/api/v1/${type}`, options)
.then(
response => {
return response.ok ? response.json()
: Promise.reject(`${response.status} ${response.statusText}`);
}
)
return fetch(`/api/v1/${type}`, getInit('POST', item))
.then(responseHandler)
.then((json) => {
dispatch(addItem({...item, id:json.id}, type));
return json.id;
@@ -227,23 +222,35 @@ export function postAction (item_id, action_type, user_id, item_type) {
action_type,
user_id
};
const options = {
method: 'POST',
headers: {
'Content-Type':'application/json'
},
body: JSON.stringify(action)
};
return fetch(`/api/v1/${item_type}/${item_id}/actions`, options)
.then(
response => {
return response.ok ? response.json()
: Promise.reject(`${response.status} ${response.statusText}`);
}
)
.then((json)=>{
return json;
});
return fetch(`/api/v1/${item_type}/${item_id}/actions`, getInit('POST', action))
.then(responseHandler);
};
}
/*
* DeleteAction
* Deletes an action to an item
*
* @params
* id - the id of the item on which the action is taking place
* action - the name of the action
* user - the user performing the action
* host - the coral host
*
* @returns
* A promise resolving to null or an error
*
*/
export function deleteAction (item_id, action_type, user_id, item_type) {
return () => {
const action = {
action_type,
user_id
};
return fetch(`/api/v1/${item_type}/${item_id}/actions`, getInit('DELETE', action))
.then(responseHandler);
};
}
@@ -5,12 +5,12 @@ const name = 'coral-plugin-comment-count';
const CommentCount = ({items, id}) => {
let count = 0;
if (items[id]) {
count += items[id].comments.length;
if (items.assets[id] && items.assets[id].comments) {
count += items.assets[id].comments.length;
}
const itemKeys = Object.keys(items);
const itemKeys = Object.keys(items.comments);
for (let i = 0; i < itemKeys.length; i++) {
const item = items[itemKeys[i]];
const item = items.comments[itemKeys[i]];
if (item.children) {
count += item.children.length;
}
@@ -5,7 +5,7 @@
"comment": "Comment",
"name": "Name",
"comment-post-notif": "Your comment has been posted.",
"comment-post-notif-premod": "Thank you for reporting this comment. Our moderation team has been notified and will review it shortly."
"comment-post-notif-premod": "Thank you for posting. Our moderation team will review your comment shortly."
},
"es": {
"post": "Publicar",
+22 -14
View File
@@ -4,27 +4,35 @@ import translations from './translations.json';
const name = 'coral-plugin-flags';
const FlagButton = ({flag, id, postAction, addItem, updateItem, addNotification}) => {
const FlagButton = ({flag, id, postAction, deleteAction, addItem, updateItem, addNotification}) => {
const flagged = flag && flag.current_user;
const onFlagClick = () => {
postAction(id, 'flag', '123', 'comments')
.then((action) => {
addItem({...action, current_user:true}, 'actions');
updateItem(action.item_id, action.action_type, action.id, 'comments');
});
addNotification('success', lang.t('flag-notif'));
if (!flagged) {
postAction(id, 'flag', '123', 'comments')
.then((action) => {
addItem({...action, current_user:true}, 'actions');
updateItem(action.item_id, action.action_type, action.id, 'comments');
});
addNotification('success', lang.t('flag-notif'));
} else {
deleteAction(id, 'flag', '123', 'comments')
.then(() => {
updateItem(id, 'flag', '', 'comments');
});
addNotification('success', lang.t('flag-notif-remove'));
}
};
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>
return <div className={`${name}-container`}>
<button onClick={onFlagClick} className={`${name}-button`}>
{
flagged
? <span className={`${name}-button-text`}>{lang.t('flag')}</span>
: <span className={`${name}-button-text`}>{lang.t('flagged')}</span>
? <span className={`${name}-button-text`}>{lang.t('flagged')}</span>
: <span className={`${name}-button-text`}>{lang.t('flag')}</span>
}
<i className={`${name}-icon material-icons ${flagged && 'flaggedIcon'}`}
style={flagged && styles.flaggedIcon}
aria-hidden={true}>flag</i>
</button>
</div>;
};
+4 -2
View File
@@ -2,11 +2,13 @@
"en": {
"flag": "Flag",
"flagged": "Flagged",
"flag-notif": "Thank you for reporting this comment. Our moderation team has been notified and will review it shortly."
"flag-notif": "Thank you for reporting this comment. Our moderation team has been notified and will review it shortly.",
"flag-notif-remove": "Your flag has been removed."
},
"es": {
"flag": "Marcar",
"flagged": "Marcado",
"flag-notif": "Gracias por marcar este comentario. Nuestro equipo de moderación ha sido notificado y muy pronto lo va a revisar."
"flag-notif": "Gracias por marcar este comentario. Nuestro equipo de moderación ha sido notificado y muy pronto lo va a revisar.",
"flag-notif-remove": "¡traduceme!"
}
}
+10
View File
@@ -0,0 +1,10 @@
import React from 'react';
const packagename = 'coral-plugin-infobox';
const InfoBox = ({enable, content}) =>
<div
className={`${packagename}-info ${enable ? null : ', hidden'}` }>
{content}
</div>;
export default InfoBox;
@@ -0,0 +1,26 @@
import React from 'react';
import {shallow} from 'enzyme';
import {expect} from 'chai';
import InfoBox from '../InfoBox';
describe('InfoBox', () => {
let comment;
let render;
beforeEach(() => {
comment = {};
const postItem = (item) => {
comment.posted = item;
return Promise.resolve(4);
};
render = shallow(<InfoBox
postItem={postItem}
updateItem={(e) => comment.text = e.target.value}
item_id={'1'}
comments={['1', '2', '3']}/>);
});
it('should render the InfoBox appropriately', () => {
expect(render.contains('<div class="InfoBox"')).to.be.truthy;
expect(render.contains('<button class="postCommentButton"')).to.be.truthy;
});
});
+41
View File
@@ -0,0 +1,41 @@
import React from 'react';
import {I18n} from '../coral-framework';
import translations from './translations.json';
const name = 'coral-plugin-flags';
const LikeButton = ({like, id, postAction, deleteAction, addItem, updateItem}) => {
const liked = like && like.current_user;
const onLikeClick = () => {
if (!liked) {
postAction(id, 'like', '123', 'comments')
.then((action) => {
addItem({id: action.id, current_user:true, count: like ? like.count + 1 : 1}, 'actions');
updateItem(action.item_id, action.action_type, action.id, 'comments');
});
} else {
deleteAction(id, 'like', '123', 'comments')
.then(() => {
updateItem(like.id, 'count', like.count - 1, 'actions');
updateItem(like.id, 'current_user', false, 'actions');
});
}
};
return <div className={`${name}-container`}>
<button onClick={onLikeClick} className={`${name}-button ${liked && 'likedButton'}`}>
{
liked
? <span className={`${name}-button-text`}>{lang.t('liked')}</span>
: <span className={`${name}-button-text`}>{lang.t('like')}</span>
}
<i className={`${name}-icon material-icons`}
aria-hidden={true}>thumb_up</i>
<span className={`${name}-like-count`}>{like && like.count > 0 && like.count}</span>
</button>
</div>;
};
export default LikeButton;
const lang = new I18n(translations);
@@ -0,0 +1,10 @@
{
"en": {
"like": "Like",
"liked": "Liked"
},
"es": {
"like": "Me Gusta",
"liked": "Me Gustó"
}
}
+1 -1
View File
@@ -7,9 +7,9 @@ const name = 'coral-plugin-replies';
const ReplyButton = (props) => <button
className={`${name}-reply-button`}
onClick={() => props.updateItem(props.id || props.parent_id, 'showReply', true, 'comments')}>
{lang.t('reply')}
<i className={`${name}-icon material-icons`}
aria-hidden={true}>reply</i>
{lang.t('reply')}
</button>;
export default ReplyButton;