mirror of
https://github.com/wassname/talk.git
synced 2026-07-23 13:10:20 +08:00
Reducers refactor, working Moderate Queue
This commit is contained in:
@@ -29,7 +29,6 @@ class ModerationContainer extends Component {
|
||||
componentWillMount() {
|
||||
const {toggleModal, singleView} = this.props;
|
||||
|
||||
this.props.fetchModerationQueueComments();
|
||||
this.props.fetchSettings();
|
||||
|
||||
key('s', () => singleView());
|
||||
@@ -54,7 +53,7 @@ class ModerationContainer extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {data, moderation} = this.props;
|
||||
const {data, moderation, settings} = this.props;
|
||||
|
||||
if (data.loading) {
|
||||
return <div><Spinner/></div>;
|
||||
@@ -66,27 +65,16 @@ class ModerationContainer extends Component {
|
||||
<ModerationQueue
|
||||
activeTab={moderation.activeTab}
|
||||
data={data}
|
||||
|
||||
onTabClick={this.onTabClick}
|
||||
onClose={this.onClose}
|
||||
{...this.props}
|
||||
{...moderation}
|
||||
suspectWords={settings.wordlist.suspect}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ModerationContainer.contextTypes = {
|
||||
// router: React.PropTypes.func.isRequired
|
||||
// };
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
moderation: state.moderation.toJS(),
|
||||
comments: state.comments.toJS(),
|
||||
settings: state.settings.toJS(),
|
||||
users: state.users.toJS(),
|
||||
actions: state.actions.toJS()
|
||||
settings: state.settings.toJS()
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
import React from 'react';
|
||||
import React, {PropTypes} from 'react';
|
||||
|
||||
// import Comment from '../../components/Comment';
|
||||
import Comment from './components/Comment';
|
||||
|
||||
export default (props) => {
|
||||
const ModerationQueue = props => {
|
||||
return (
|
||||
<div>
|
||||
{/* <Comment*/}
|
||||
{/* comment={comment}*/}
|
||||
{/* key={i}*/}
|
||||
{/* author={comment.user}*/}
|
||||
{/* />*/}
|
||||
<ul>
|
||||
{
|
||||
props.data[props.activeTab].map((comment, i) =>
|
||||
<div key={i}>
|
||||
{comment.body}
|
||||
</div>
|
||||
)
|
||||
props.data[props.activeTab].map((comment, i) => {
|
||||
console.log(comment);
|
||||
return <Comment
|
||||
key={i}
|
||||
index={i}
|
||||
suspectWords={props.suspectWords}
|
||||
{...comment}
|
||||
/>;
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ModerationQueue.propTypes = {
|
||||
data: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default ModerationQueue;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import timeago from 'timeago.js';
|
||||
import Linkify from 'react-linkify';
|
||||
import Highlighter from 'react-highlight-words';
|
||||
|
||||
import styles from './styles.css';
|
||||
|
||||
import {Icon} from 'coral-ui';
|
||||
|
||||
// import ActionButton from '../components/ActionButton';
|
||||
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../../translations.json';
|
||||
|
||||
const Comment = props => {
|
||||
const links = linkify.getMatches(props.body);
|
||||
|
||||
return (
|
||||
<li tabIndex={props.index}
|
||||
className={`mdl-card mdl-shadow--2dp ${styles.listItem} ${props.isActive && !props.hideActive ? styles.activeItem : ''}`}>
|
||||
<div className={styles.itemHeader}>
|
||||
<div className={styles.author}>
|
||||
<span>{props.user.name}</span>
|
||||
<span className={styles.created}>
|
||||
{timeago().format(props.created_at || (Date.now() - props.index * 60 * 1000), lang.getLocale().replace('-', '_'))}
|
||||
</span>
|
||||
{props.flagged ? <p className={styles.flagged}>{lang.t('comment.flagged')}</p> : null}
|
||||
</div>
|
||||
<div className={styles.sideActions}>
|
||||
{links ? <span className={styles.hasLinks}><Icon name='error_outline'/> Contains Link</span> : null}
|
||||
<div className={`actions ${styles.actions}`}>
|
||||
{/* {props.modActions.map(*/}
|
||||
{/* (action, i) =>*/}
|
||||
{/* <ActionButton*/}
|
||||
{/* option={action}*/}
|
||||
{/* key={i}*/}
|
||||
{/* type='COMMENTS'*/}
|
||||
{/* comment={comment}*/}
|
||||
{/* user={author}*/}
|
||||
{/* menuOptionsMap={props.menuOptionsMap}*/}
|
||||
{/* onClickAction={props.onClickAction}*/}
|
||||
{/* onClickShowBanDialog={props.onClickShowBanDialog}/>*/}
|
||||
{/* )}*/}
|
||||
</div>
|
||||
{props.user.banned === 'banned' ?
|
||||
<span className={styles.banned}>
|
||||
<Icon name='error_outline'/>
|
||||
{lang.t('comment.banned_user')}
|
||||
</span>
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.itemBody}>
|
||||
<span className={styles.body}>
|
||||
<Linkify component='span' properties={{style: linkStyles}}>
|
||||
<Highlighter searchWords={props.suspectWords} textToHighlight={props.body}/>
|
||||
</Linkify>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const linkStyles = {
|
||||
backgroundColor: 'rgb(255, 219, 135)',
|
||||
padding: '1px 2px'
|
||||
};
|
||||
|
||||
const linkify = new Linkify();
|
||||
const lang = new I18n(translations);
|
||||
|
||||
Comment.propTypes = {
|
||||
user: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default Comment;
|
||||
@@ -0,0 +1,184 @@
|
||||
|
||||
@custom-media --big-viewport (min-width: 780px);
|
||||
|
||||
.list {
|
||||
padding: 8px 0;
|
||||
list-style: none;
|
||||
display: block;
|
||||
|
||||
&.singleView .listItem {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.singleView .listItem.activeItem {
|
||||
display: block;
|
||||
height: 100%;
|
||||
font-size: 1.5em;
|
||||
line-height: 1.5em;
|
||||
border: none;
|
||||
|
||||
.actions {
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
left: 25%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 50%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.listItem {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
max-width: 660px;
|
||||
min-width: 400px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 14px;
|
||||
position: relative;
|
||||
transition: box-shadow 200ms;
|
||||
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.sideActions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
padding: 40px 18px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.itemHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.author {
|
||||
min-width: 230px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.itemBody {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-right: 16px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: #757575;
|
||||
font-size: 40px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.created {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
transform: scale(.8);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-top: 20px;
|
||||
flex: 1;
|
||||
font-size: 0.88em;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.flagged {
|
||||
color: rgba(255, 0, 0, .5);
|
||||
padding-top: 15px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.flagCount{
|
||||
font-size: 12px;
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: #444;
|
||||
margin-top: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@media (--big-viewport) {
|
||||
.listItem {
|
||||
border: 1px solid #e0e0e0;
|
||||
margin-bottom: 30px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
&.activeItem {
|
||||
border: 2px solid #333;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.hasLinks {
|
||||
color: #f00;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.banned {
|
||||
color: #f00;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.ban {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.banButton {
|
||||
width: 114px;
|
||||
letter-spacing: 1px;
|
||||
|
||||
i {
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import {Map, Set, fromJS} from 'immutable';
|
||||
import * as types from '../constants/actions';
|
||||
|
||||
const initialState = Map({
|
||||
ids: Set(),
|
||||
byId: Map()
|
||||
});
|
||||
|
||||
export default (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case types.ACTIONS_MODERATION_QUEUE_FETCH_SUCCESS: return addActions(state, action);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const addActions = (state, action) => {
|
||||
|
||||
// Make ids that are unique by item_id and by action type
|
||||
const typeId = (action) => `${action.action_type}_${action.item_id}`;
|
||||
const ids = action.actions.map(action => typeId(action));
|
||||
const map = action.actions.reduce((memo, action) => {
|
||||
memo[typeId(action)] = action;
|
||||
return memo;
|
||||
}, {});
|
||||
return state.set('byId', fromJS(map)).set('ids', new Set(ids));
|
||||
};
|
||||
@@ -1,77 +0,0 @@
|
||||
import * as actions from '../constants/comments';
|
||||
import * as userActions from '../constants/users';
|
||||
import {Map, List, fromJS} from 'immutable';
|
||||
|
||||
/**
|
||||
* Comments state is stored using 2 structures:
|
||||
* - byId is a Map holding the comments using the item_id property as keys
|
||||
* - ids is a List of item_id, this allows us to order and iterate easily
|
||||
* since maps are unordered and some times we just need a list of things
|
||||
*/
|
||||
|
||||
const initialState = Map({
|
||||
byId: Map(),
|
||||
ids: List(),
|
||||
loading: false,
|
||||
showBanUserDialog: false,
|
||||
banUser: {
|
||||
'userName': '',
|
||||
'userId': '',
|
||||
'commentId': ''
|
||||
}
|
||||
});
|
||||
|
||||
// Handle the comment actions
|
||||
export default (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case actions.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST: return state.set('loading', true);
|
||||
case actions.COMMENTS_MODERATION_QUEUE_FETCH_SUCCESS: return replaceComments(action, state);
|
||||
case actions.COMMENTS_MODERATION_QUEUE_FAILED: return state.set('loading', false);
|
||||
case actions.COMMENT_STATUS_UPDATE_REQUEST: return updateStatus(state, action);
|
||||
case actions.COMMENT_FLAG: return flag(state, action);
|
||||
case actions.COMMENT_CREATE_SUCCESS: return addComment(state, action);
|
||||
case actions.COMMENT_STREAM_FETCH_SUCCESS: return replaceComments(action, state);
|
||||
case actions.SHOW_BANUSER_DIALOG: return setBanUser(state, true, action);
|
||||
case actions.HIDE_BANUSER_DIALOG: return setBanUser(state, false, action);
|
||||
case actions.USER_BAN_SUCCESS: return setBanUser(state, false, action);
|
||||
case userActions.UPDATE_STATUS_SUCCESS: return setBanUser(state, false, action);
|
||||
default: return state;
|
||||
}
|
||||
};
|
||||
|
||||
// hide or show the UI for the dialog confirming the ban
|
||||
// set the user that is going to set and the comment that is the reason
|
||||
const setBanUser = (state, showBanUser, action) => {
|
||||
const banUser = {'userName': action.userName, 'userId': action.userId, 'commentId': action.commentId};
|
||||
return state.set('showBanUserDialog', showBanUser)
|
||||
.set('banUser', banUser);
|
||||
};
|
||||
|
||||
// Update a comment status
|
||||
const updateStatus = (state, action) => {
|
||||
const byId = state.get('byId');
|
||||
const data = byId.get(action.id).set('status', action.status.toLowerCase());
|
||||
return state.set('byId', byId.set(action.id, data));
|
||||
};
|
||||
|
||||
// Flag a comment
|
||||
const flag = (state, action) => {
|
||||
const byId = state.get('byId');
|
||||
const data = byId.get(action.id).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; }, {}));
|
||||
return state.set('byId', comments).set('loading', false)
|
||||
.set('ids', List(comments.keys()));
|
||||
};
|
||||
|
||||
// Add a new comment
|
||||
const addComment = (state, action) => {
|
||||
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')));
|
||||
};
|
||||
@@ -1,19 +1,13 @@
|
||||
import auth from './auth';
|
||||
import users from './users';
|
||||
import assets from './assets';
|
||||
import actions from './actions';
|
||||
import comments from './comments';
|
||||
import settings from './settings';
|
||||
import community from './community';
|
||||
import moderation from './moderation';
|
||||
|
||||
export default {
|
||||
auth,
|
||||
users,
|
||||
assets,
|
||||
actions,
|
||||
settings,
|
||||
comments,
|
||||
community,
|
||||
moderation
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Map, List} from 'immutable';
|
||||
import * as types from '../actions/settings';
|
||||
import * as actions from '../actions/settings';
|
||||
|
||||
const initialState = Map({
|
||||
settings: Map({
|
||||
@@ -13,43 +13,46 @@ const initialState = Map({
|
||||
fetchingSettings: false
|
||||
});
|
||||
|
||||
// Handle the comment actions
|
||||
export default (state = initialState, action) => {
|
||||
export default function settings (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case types.SETTINGS_LOADING: return state.set('fetchingSettings', true).set('fetchSettingsError', null);
|
||||
case types.SETTINGS_RECEIVED: return updateSettings(state, action);
|
||||
case types.SETTINGS_FETCH_ERROR: return settingsFetchFailed(state, action);
|
||||
case types.SETTINGS_UPDATED: return updateSettings(state, action);
|
||||
case types.SAVE_SETTINGS_LOADING: return state.set('fetchingSettings', true).set('saveSettingsError', null);
|
||||
case types.SAVE_SETTINGS_SUCCESS: return saveComplete(state, action);
|
||||
case types.SAVE_SETTINGS_FAILED: return settingsSaveFailed(state, action);
|
||||
case types.WORDLIST_UPDATED: return updateWordlist(state, action);
|
||||
default: return state;
|
||||
case actions.SETTINGS_LOADING:
|
||||
return state
|
||||
.set('fetchingSettings', true)
|
||||
.set('fetchSettingsError', null);
|
||||
case actions.SETTINGS_RECEIVED:
|
||||
return state.merge({
|
||||
fetchingSettings: null,
|
||||
fetchSettingsError: null,
|
||||
...action.settings
|
||||
});
|
||||
case actions.SETTINGS_FETCH_ERROR:
|
||||
return state
|
||||
.set('fetchingSettings', false)
|
||||
.set('fetchSettingsError', action.error);
|
||||
case actions.SETTINGS_UPDATED:
|
||||
return state.merge({
|
||||
fetchingSettings: null,
|
||||
fetchSettingsError: null,
|
||||
...action.settings
|
||||
});
|
||||
case actions.SAVE_SETTINGS_LOADING:
|
||||
return state
|
||||
.set('fetchingSettings', true)
|
||||
.set('saveSettingsError', null);
|
||||
case actions.SAVE_SETTINGS_SUCCESS:
|
||||
return state.merge({
|
||||
fetchingSettings: false,
|
||||
fetchSettingsError: null,
|
||||
...action.settings
|
||||
});
|
||||
case actions.SAVE_SETTINGS_FAILED:
|
||||
return state
|
||||
.set('fetchingSettings', false)
|
||||
.set('fetchSettingsError', action.error);
|
||||
case actions.WORDLIST_UPDATED:
|
||||
return state
|
||||
.setIn(['settings', 'wordlist', action.listName], action.list);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
// only for updating top-level settings
|
||||
const updateSettings = (state, action) => {
|
||||
const s = state.set('fetchingSettings', false).set('fetchSettingsError', null);
|
||||
const settings = s.get('settings').merge(action.settings);
|
||||
return s.set('settings', settings);
|
||||
};
|
||||
|
||||
// any nested settings must have a specialized setter
|
||||
const updateWordlist = (state, action) => {
|
||||
return state.setIn(['settings', 'wordlist', action.listName], action.list);
|
||||
};
|
||||
|
||||
const saveComplete = (state, action) => {
|
||||
const s = state.set('fetchingSettings', false).set('saveSettingsError', null);
|
||||
const settings = s.get('settings').merge(action.settings);
|
||||
return s.set('settings', settings);
|
||||
};
|
||||
|
||||
const settingsFetchFailed = (state, action) => {
|
||||
return state.set('fetchingSettings', false).set('fetchSettingsError', action.error);
|
||||
};
|
||||
|
||||
const settingsSaveFailed = (state, action) => {
|
||||
return state.set('fetchingSettings', false).set('fetchSettingsError', action.error);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import {Map, List, fromJS} from 'immutable';
|
||||
|
||||
const initialState = Map({
|
||||
byId: Map(),
|
||||
ids: List()
|
||||
});
|
||||
|
||||
export default (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case 'USERS_MODERATION_QUEUE_FETCH_SUCCESS': return replaceUsers(action, state);
|
||||
case 'USER_STATUS_UPDATE': return updateUserStatus(state, action);
|
||||
default: return state;
|
||||
}
|
||||
};
|
||||
|
||||
// Replace the comment list with a new one
|
||||
const replaceUsers = (action, state) => {
|
||||
const users = fromJS(action.users.reduce((prev, curr) => { prev[curr.id] = curr; return prev; }, {}));
|
||||
return state.set('byId', users)
|
||||
.set('ids', List(users.keys()));
|
||||
};
|
||||
|
||||
// Update a user status
|
||||
const updateUserStatus = (state, action) => {
|
||||
const byId = state.get('byId');
|
||||
const data = byId.get(action.author_id).set('status', action.status.toLowerCase());
|
||||
return state.set('byId', byId.set(action.author_id, data));
|
||||
};
|
||||
@@ -2,7 +2,6 @@ import ApolloClient, {addTypename} from 'apollo-client';
|
||||
import getNetworkInterface from './transport';
|
||||
|
||||
export const client = new ApolloClient({
|
||||
connectToDevTools: true,
|
||||
queryTransformer: addTypename,
|
||||
dataIdFromObject: (result) => {
|
||||
if (result.id && result.__typename) { // eslint-disable-line no-underscore-dangle
|
||||
|
||||
Reference in New Issue
Block a user