mirror of
https://github.com/wassname/talk.git
synced 2026-08-20 12:50:41 +08:00
Merge branch 'master' into csrf
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import {SelectField, Option} from 'react-mdl-selectfield';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import styles from './Configure.css';
|
||||
@@ -12,6 +13,12 @@ import {
|
||||
Icon
|
||||
} from 'react-mdl';
|
||||
|
||||
const TIMESTAMPS = {
|
||||
weeks: 60 * 60 * 24 * 7,
|
||||
days: 60 * 60 * 24,
|
||||
hours: 60 * 60
|
||||
};
|
||||
|
||||
const updateCharCountEnable = (updateSettings, charCountChecked) => () => {
|
||||
const charCountEnable = !charCountChecked;
|
||||
updateSettings({charCountEnable});
|
||||
@@ -47,6 +54,21 @@ const updateClosedMessage = (updateSettings) => (event) => {
|
||||
updateSettings({closedMessage});
|
||||
};
|
||||
|
||||
// If we are changing the measure we need to recalculate using the old amount
|
||||
// Same thing if we are just changing the amount
|
||||
const updateClosedTimeout = (updateSettings, ts, isMeasure) => (event) => {
|
||||
if (isMeasure) {
|
||||
const amount = getTimeoutAmount(ts);
|
||||
const closedTimeout = amount * TIMESTAMPS[event];
|
||||
updateSettings({closedTimeout});
|
||||
} else {
|
||||
const val = event.target.value;
|
||||
const measure = getTimeoutMeasure(ts);
|
||||
const closedTimeout = val * TIMESTAMPS[measure];
|
||||
updateSettings({closedTimeout});
|
||||
}
|
||||
};
|
||||
|
||||
const CommentSettings = ({updateSettings, settingsError, settings, errors}) => <List>
|
||||
<ListItem className={`${styles.configSetting} ${settings.moderation === 'pre' ? styles.enabledSetting : styles.disabledSetting}`}>
|
||||
<ListItemAction>
|
||||
@@ -110,6 +132,27 @@ const CommentSettings = ({updateSettings, settingsError, settings, errors}) => <
|
||||
rows={3}/>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={styles.configSettingInfoBox}>
|
||||
<ListItemContent>
|
||||
{lang.t('configure.close-after')}
|
||||
<br />
|
||||
<Textfield
|
||||
type='number'
|
||||
pattern='[0-9]+'
|
||||
style={{width: 50}}
|
||||
onChange={updateClosedTimeout(updateSettings, settings.closedTimeout)}
|
||||
value={getTimeoutAmount(settings.closedTimeout)}
|
||||
label={lang.t('configure.closed-comments-label')} />
|
||||
<div className={styles.configTimeoutSelect}>
|
||||
<SelectField value={getTimeoutMeasure(settings.closedTimeout)}
|
||||
onChange={updateClosedTimeout(updateSettings, settings.closedTimeout, true)}>
|
||||
<Option value={'hours'}>{lang.t('configure.hours')}</Option>
|
||||
<Option value={'days'}>{lang.t('configure.days')}</Option>
|
||||
<Option value={'weeks'}>{lang.t('configure.weeks')}</Option>
|
||||
</SelectField>
|
||||
</div>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={styles.configSettingInfoBox}>
|
||||
<ListItemContent>
|
||||
{lang.t('configure.closed-comments-desc')}
|
||||
@@ -124,4 +167,20 @@ const CommentSettings = ({updateSettings, settingsError, settings, errors}) => <
|
||||
|
||||
export default CommentSettings;
|
||||
|
||||
// To see if we are talking about weeks, days or hours
|
||||
// We talk the remainder of the division and see if it's 0
|
||||
const getTimeoutMeasure = ts => {
|
||||
if (ts % TIMESTAMPS['weeks'] === 0) {
|
||||
return 'weeks';
|
||||
} else if (ts % TIMESTAMPS['days'] === 0) {
|
||||
return 'days';
|
||||
} else if (ts % TIMESTAMPS['hours'] === 0) {
|
||||
return 'hours';
|
||||
}
|
||||
};
|
||||
|
||||
// Dividing the amount by it's measure (hours, days, weeks) we
|
||||
// obtain the amount of time
|
||||
const getTimeoutAmount = ts => ts / TIMESTAMPS[getTimeoutMeasure(ts)];
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
@@ -63,6 +63,11 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.configTimeoutSelect {
|
||||
display: inline-block;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.charCountTexfield {
|
||||
width: 4em;
|
||||
padding: 0px;
|
||||
|
||||
@@ -57,6 +57,10 @@
|
||||
"community": "Community",
|
||||
"closed-comments-desc": "Write a message for closed threads",
|
||||
"closed-comments-label": "Write a message...",
|
||||
"hours": "Hours",
|
||||
"days": "Days",
|
||||
"weeks": "Weeks",
|
||||
"close-after": "Close comments after",
|
||||
"comment-count-header": "Limit Comment Length",
|
||||
"comment-count-text-pre": "Comments will be limited to ",
|
||||
"comment-count-text-post": " characters.",
|
||||
@@ -117,6 +121,11 @@
|
||||
"community": "Comunidad",
|
||||
"closed-comments-desc": "Escribe un mensaje para cuando los comentarios se encuentran cerrados",
|
||||
"closed-comments-label": "Escribe un mensaje...",
|
||||
"never": "Nunca",
|
||||
"hours": "Horas",
|
||||
"days": "Días",
|
||||
"weeks": "Semanas",
|
||||
"close-after": "Cerrar comentarios luego de",
|
||||
"comment-count-header": "Limitar el largo del comentario",
|
||||
"comment-count-text-pre": "El largo de comentarios será ",
|
||||
"comment-count-text-post": " caracteres",
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {I18n} from '../../coral-framework';
|
||||
import {updateOpenStatus, updateConfiguration} from '../../coral-framework/actions/config';
|
||||
|
||||
import CloseCommentsInfo from '../components/CloseCommentsInfo';
|
||||
import ConfigureCommentStream from '../components/ConfigureCommentStream';
|
||||
|
||||
const lang = new I18n();
|
||||
|
||||
class ConfigureStreamContainer extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
@@ -47,8 +50,15 @@ class ConfigureStreamContainer extends Component {
|
||||
this.props.updateStatus(this.props.config.status === 'open' ? 'closed' : 'open');
|
||||
}
|
||||
|
||||
getClosedIn () {
|
||||
const {config} = this.props;
|
||||
const {created_at, closedTimeout} = config;
|
||||
return lang.timeago(new Date(created_at).getTime() + (1000 * closedTimeout));
|
||||
}
|
||||
|
||||
render () {
|
||||
const {status} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ConfigureCommentStream
|
||||
@@ -59,6 +69,7 @@ class ConfigureStreamContainer extends Component {
|
||||
/>
|
||||
<hr />
|
||||
<h3>{status === 'open' ? 'Close' : 'Open'} Comment Stream</h3>
|
||||
{status === 'open' ? <p>The comment stream will close in {this.getClosedIn()}.</p> : ''}
|
||||
<CloseCommentsInfo
|
||||
onClick={this.toggleStatus}
|
||||
status={status}
|
||||
|
||||
@@ -2,6 +2,10 @@ import coralApi from '../helpers/response';
|
||||
import {fromJS} from 'immutable';
|
||||
import {UPDATE_CONFIG} from '../constants/config';
|
||||
|
||||
/**
|
||||
* Action name constants
|
||||
*/
|
||||
|
||||
export const ADD_ITEM = 'ADD_ITEM';
|
||||
export const UPDATE_ITEM = 'UPDATE_ITEM';
|
||||
export const APPEND_ITEM_ARRAY = 'APPEND_ITEM_ARRAY';
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as actions from '../constants/user';
|
||||
import * as assetActions from '../constants/assets';
|
||||
import {addNotification} from '../actions/notification';
|
||||
import {addItem} from '../actions/items';
|
||||
import coralApi from '../helpers/response';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
@@ -19,3 +21,30 @@ export const saveBio = (user_id, formData) => dispatch => {
|
||||
})
|
||||
.catch(error => dispatch(saveBioFailure(error)));
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* Get a list of comments by a single user
|
||||
*
|
||||
* @param {string} user_id
|
||||
* @returns Promise
|
||||
*/
|
||||
export const fetchCommentsByUserId = userId => {
|
||||
return (dispatch) => {
|
||||
dispatch({type: actions.COMMENTS_BY_USER_REQUEST});
|
||||
return coralApi(`/comments?user_id${userId}`)
|
||||
.then(({comments, assets}) => {
|
||||
comments.forEach(comment => dispatch(addItem(comment, 'comments')));
|
||||
|
||||
assets.forEach(asset => dispatch(addItem(asset, 'assets')));
|
||||
|
||||
dispatch({type: actions.COMMENTS_BY_USER_SUCCESS, comments: comments.map(comment => comment.id)});
|
||||
dispatch({type: assetActions.MULTIPLE_ASSETS_SUCCESS, assets: assets.map(asset => asset.id)});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error.stack);
|
||||
console.error('FAILURE_COMMENTS_BY_USER', error);
|
||||
dispatch({type: actions.COMMENTS_BY_USER_FAILURE, error});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const MULTIPLE_ASSETS_REQUEST = 'MULTIPLE_ASSETS_REQUEST';
|
||||
export const MULTIPLE_ASSETS_SUCCESS = 'MULTIPLE_ASSETS_SUCCESS';
|
||||
export const MULTIPLE_ASSSETS_FAILURE = 'MULTIPLE_ASSSETS_FAILURE';
|
||||
@@ -1,3 +1,6 @@
|
||||
export const SAVE_BIO_REQUEST = 'SAVE_BIO_REQUEST';
|
||||
export const SAVE_BIO_SUCCESS = 'SAVE_BIO_SUCCESS';
|
||||
export const SAVE_BIO_FAILURE = 'SAVE_BIO_FAILURE';
|
||||
export const COMMENTS_BY_USER_REQUEST = 'COMMENTS_BY_USER_REQUEST';
|
||||
export const COMMENTS_BY_USER_SUCCESS = 'COMMENTS_BY_USER_SUCCESS';
|
||||
export const COMMENTS_BY_USER_FAILURE = 'COMMENTS_BY_USER_FAILURE';
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as actions from '../actions/items';
|
||||
const initialState = fromJS({
|
||||
comments: {},
|
||||
users: {},
|
||||
assets: {},
|
||||
actions: {}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import {Map} from 'immutable';
|
||||
import {Map, fromJS} from 'immutable';
|
||||
import * as authActions from '../constants/auth';
|
||||
import * as actions from '../constants/user';
|
||||
import * as assetActions from '../constants/assets';
|
||||
|
||||
const initialState = Map({
|
||||
displayName: '',
|
||||
profiles: [],
|
||||
settings: {}
|
||||
settings: {},
|
||||
myComments: [],
|
||||
myAssets: [] // the assets from which myComments (above) originated
|
||||
});
|
||||
|
||||
const purge = user => {
|
||||
@@ -30,6 +33,10 @@ export default function user (state = initialState, action) {
|
||||
case actions.SAVE_BIO_SUCCESS:
|
||||
return state
|
||||
.set('settings', action.settings);
|
||||
case actions.COMMENTS_BY_USER_SUCCESS:
|
||||
return state.set('myComments', fromJS(action.comments));
|
||||
case assetActions.MULTIPLE_ASSETS_SUCCESS:
|
||||
return state.set('myAssets', fromJS(action.assets));
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import React from 'react';
|
||||
import {I18n} from '../coral-framework';
|
||||
import translations from './translations.json';
|
||||
import has from 'lodash/has';
|
||||
import reduce from 'lodash/reduce';
|
||||
const name = 'coral-plugin-comment-count';
|
||||
|
||||
const CommentCount = ({items, id}) => {
|
||||
let count = 0;
|
||||
if (items.assets[id] && items.assets[id].comments) {
|
||||
if (has(items, `assets.${id}.comments`)) {
|
||||
count += items.assets[id].comments.length;
|
||||
}
|
||||
const itemKeys = Object.keys(items.comments);
|
||||
for (let i = 0; i < itemKeys.length; i++) {
|
||||
const item = items.comments[itemKeys[i]];
|
||||
if (item.children) {
|
||||
count += item.children.length;
|
||||
|
||||
// lodash reduce works on {}
|
||||
count += reduce(items.comments, (accum, comment) => {
|
||||
if (comment.children) {
|
||||
accum += comment.children.length;
|
||||
}
|
||||
}
|
||||
return accum;
|
||||
}, 0);
|
||||
|
||||
return <div className={`${name}-text`}>
|
||||
{`${count} ${count === 1 ? lang.t('comment') : lang.t('comment-plural')}`}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
.assetURL {
|
||||
font-size: 16px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.commentBody {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
|
||||
import styles from './Comment.css';
|
||||
|
||||
const Comment = props => {
|
||||
return (
|
||||
<div>
|
||||
<p className="myCommentAsset">
|
||||
<a className={`${styles.assetURL} myCommentAnchor`} href={props.asset.url}>{props.asset.url}</a>
|
||||
</p>
|
||||
<p className={`${styles.commentBody} myCommentBody`}>{props.comment.body}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Comment.propTypes = {
|
||||
comment: PropTypes.shape({
|
||||
body: PropTypes.string
|
||||
}).isRequired,
|
||||
asset: PropTypes.shape({
|
||||
url: PropTypes.string
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default Comment;
|
||||
@@ -0,0 +1,27 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import Comment from './Comment';
|
||||
import styles from './CommentHistory.css';
|
||||
|
||||
const CommentHistory = props => {
|
||||
return (
|
||||
<div className={`${styles.header} commentHistory`}>
|
||||
<h2>All Comments</h2>
|
||||
<div className="commentHistory__list">
|
||||
{props.comments.map((comment, i) => {
|
||||
const asset = props.assets.find(asset => asset.id === comment.asset_id);
|
||||
return <Comment
|
||||
key={i}
|
||||
comment={comment}
|
||||
asset={asset} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CommentHistory.propTypes = {
|
||||
comments: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
assets: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
export default CommentHistory;
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
import styles from './CommentHistory.css';
|
||||
|
||||
export default ({comments = []}) => (
|
||||
<div className={styles.header}>
|
||||
<h1>Comments</h1>
|
||||
<ul>
|
||||
{comments.map(() => (
|
||||
<li>
|
||||
{/* Comment Data*/}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {saveBio} from 'coral-framework/actions/user';
|
||||
import {saveBio, fetchCommentsByUserId} from 'coral-framework/actions/user';
|
||||
|
||||
import BioContainer from './BioContainer';
|
||||
import NotLoggedIn from '../components/NotLoggedIn';
|
||||
import {TabBar, Tab, TabContent} from '../../coral-ui';
|
||||
import CommentHistory from '../components/CommentHistory';
|
||||
import CommentHistory from 'coral-plugin-history/CommentHistory';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import RestrictedContent from 'coral-framework/components/RestrictedContent';
|
||||
|
||||
@@ -22,6 +22,7 @@ class SignInContainer extends Component {
|
||||
|
||||
componentWillMount () {
|
||||
// Fetch commentHistory
|
||||
this.props.fetchCommentsByUserId(this.props.userData.id);
|
||||
}
|
||||
|
||||
handleTabChange(tab) {
|
||||
@@ -31,7 +32,7 @@ class SignInContainer extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {loggedIn, userData, showSignInDialog} = this.props;
|
||||
const {loggedIn, userData, showSignInDialog, items, user} = this.props;
|
||||
const {activeTab} = this.state;
|
||||
return (
|
||||
<RestrictedContent restricted={!loggedIn} restrictedComp={<NotLoggedIn showSignInDialog={showSignInDialog} />}>
|
||||
@@ -41,7 +42,13 @@ class SignInContainer extends Component {
|
||||
<Tab>Profile Settings</Tab>
|
||||
</TabBar>
|
||||
<TabContent show={activeTab === 0}>
|
||||
<CommentHistory {...this.props}/>
|
||||
{
|
||||
user.myComments.length && user.myAssets.length
|
||||
? <CommentHistory
|
||||
comments={user.myComments.map(id => items.comments[id])}
|
||||
assets={user.myAssets.map(id => items.assets[id])} />
|
||||
: <p>Loading comment history...</p>
|
||||
}
|
||||
</TabContent>
|
||||
<TabContent show={activeTab === 1}>
|
||||
<BioContainer bio={userData.settings.bio} handleSave={this.handleSave} {...this.props} />
|
||||
@@ -51,12 +58,14 @@ class SignInContainer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = () => ({
|
||||
const mapStateToProps = state => ({
|
||||
items: state.items.toJS(),
|
||||
user: state.user.toJS()
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
saveBio: (user_id, formData) => dispatch(saveBio(user_id, formData)),
|
||||
getHistory: () => dispatch(),
|
||||
fetchCommentsByUserId: userId => dispatch(fetchCommentsByUserId(userId))
|
||||
});
|
||||
|
||||
export default connect(
|
||||
|
||||
Reference in New Issue
Block a user