Merge branch 'next' into reply

This commit is contained in:
Kiwi
2018-01-10 18:08:12 +01:00
committed by GitHub
221 changed files with 7193 additions and 4100 deletions
@@ -4,4 +4,3 @@ export const showBanUserDialog = ({userId, username, commentId, commentStatus})
({type: SHOW_BAN_USER_DIALOG, userId, username, commentId, commentStatus});
export const hideBanUserDialog = () => ({type: HIDE_BAN_USER_DIALOG});
@@ -7,8 +7,6 @@ import {
SORT_UPDATE,
SET_PAGE,
SET_SEARCH_VALUE,
SET_ROLE,
SET_COMMENTER_STATUS,
SHOW_BANUSER_DIALOG,
HIDE_BANUSER_DIALOG,
SHOW_REJECT_USERNAME_DIALOG,
@@ -56,20 +54,6 @@ export const setSearchValue = (value) => ({
value,
});
export const setRole = (id, role) => (dispatch, _, {rest}) => {
return rest(`/users/${id}/role`, {method: 'POST', body: {role}})
.then(() => {
return dispatch({type: SET_ROLE, id, role});
});
};
export const setCommenterStatus = (id, status) => (dispatch, _, {rest}) => {
return rest(`/users/${id}/status`, {method: 'POST', body: {status}})
.then(() => {
return dispatch({type: SET_COMMENTER_STATUS, id, status});
});
};
// Ban User Dialog
export const showBanUserDialog = (user) => ({type: SHOW_BANUSER_DIALOG, user});
export const hideBanUserDialog = () => ({type: HIDE_BANUSER_DIALOG});
+2 -2
View File
@@ -3,12 +3,12 @@ import * as actions from 'constants/userDetail';
export const viewUserDetail = (userId) => ({type: actions.VIEW_USER_DETAIL, userId});
export const hideUserDetail = () => ({type: actions.HIDE_USER_DETAIL});
export const changeUserDetailStatuses = (tab) => {
export const changeTab = (tab) => {
let statuses = null;
if (tab === 'rejected') {
statuses = ['REJECTED'];
}
return {type: actions.CHANGE_USER_DETAIL_STATUSES, tab, statuses};
return {type: actions.CHANGE_TAB_USER_DETAIL, tab, statuses};
};
export const clearUserDetailSelections = () => ({type: actions.CLEAR_USER_DETAIL_SELECTIONS});
@@ -0,0 +1,30 @@
.table {
flex-direction: column;
}
.table, .headerRow, .row {
display: flex;
}
.headerRowItem, .item {
flex: 1;
padding: 20px;
&:nth-child(2) {
flex: 2;
}
}
.headerRowItem {
color: #595959;
font-weight: bold;
}
.headerRow, .row {
border-bottom: 1px solid #e0e0e0;
}
.action {
color: black;
font-weight: bold;
}
@@ -0,0 +1,64 @@
import React from 'react';
import PropTypes from 'prop-types';
import {murmur3} from 'murmurhash-js';
import styles from './AccountHistory.css';
import cn from 'classnames';
import flatten from 'lodash/flatten';
import orderBy from 'lodash/orderBy';
import moment from 'moment';
const buildUserHistory = (userState = {}) => {
return orderBy(flatten(Object.keys(userState.status)
.filter((k) => k !== '__typename')
.map((k) => userState.status[k].history)), 'created_at', 'desc');
};
const buildActionResponse = (typename, status) => {
const actionResponses = {
'UsernameStatusHistory' : `Username Status: ${status}`,
'BannedStatusHistory': status ? 'User banned' : 'Ban removed',
'SuspensionStatusHistory': status ? 'Account Suspended' : 'Suspension removed'
};
return actionResponses[typename];
};
class AccountHistory extends React.Component {
render() {
const {userState} = this.props;
const userHistory = buildUserHistory(userState);
return (
<div>
<div className={cn(styles.table, 'talk-admin-account-history')}>
<div className={cn(styles.headerRow, 'talk-admin-account-history-header-row')}>
<div className={styles.headerRowItem}>Date</div>
<div className={styles.headerRowItem}>Action</div>
<div className={styles.headerRowItem}>Moderation</div>
</div>
{
userHistory.map((h) => (
<div className={cn(styles.row, 'talk-admin-account-history-row')} key={`${h.__typename}_${murmur3(h.created_at)}`}>
<div className={cn(styles.item, 'talk-admin-account-history-row-date')}>
{moment(new Date(h.created_at)).format('MMM DD, YYYY')}
</div>
<div className={cn(styles.item, styles.action, 'talk-admin-account-history-row-status')}>
{buildActionResponse(h.__typename, h.status)}
</div>
<div className={cn(styles.item, 'talk-admin-account-history-row-assigned-by')}>
{h.assigned_by ? h.assigned_by.username : 'SYSTEM'}
</div>
</div>
))
}
</div>
</div>
);
}
}
AccountHistory.propTypes = {
history: PropTypes.array,
userState: PropTypes.object,
};
export default AccountHistory;
@@ -8,9 +8,6 @@
color: black;
> :global(.mdl-menu__container) {
margin-left: 10px;
> :global(.mdl-menu__outline) {
box-shadow: none;
}
}
}
@@ -18,12 +15,13 @@
box-shadow: none;
color: white;
background-color: #616161;
border-color: #616161;
}
.arrowIcon {
margin-left: 6px;
margin-right: 0;
vertical-align: middle;
vertical-align: middle;
margin-right: 0;
font-size: 14px;
}
@@ -33,8 +31,10 @@
}
.menuItem {
background-color: #2a2a2a;
color: white;
color: #2a2a2a;
background-color: white;
font-size: 0.95em;
&:first-child {
margin-bottom: 1px;
border-radius: 2px 2px 0px 0px;
@@ -43,7 +43,8 @@
border-radius: 0px 0px 2px 2px;
}
&:hover, &:active, &:focus {
background-color: #767676;
background-color: #e2e2e2;
border-color: #616161;
}
&[disabled], &[disabled]:hover, &[disabled]:focus, &[disabled]:active {
background-color: #262626;
@@ -32,18 +32,18 @@ class ActionsMenu extends React.Component {
};
render() {
const {className = ''} = this.props;
const {className = '', buttonClassNames = '', label = ''} = this.props;
return (
<div className={cn(styles.root, className)} onBlur={this.syncOpenState} >
<Button
cStyle='actions'
className={cn(styles.button, {[styles.buttonOpen]: this.state.open})}
className={cn(styles.button, {[styles.buttonOpen]: this.state.open}, buttonClassNames)}
disabled={false}
id={this.id}
onClick={this.syncOpenState}
icon={this.props.icon}
raised>
{t('modqueue.actions')}
{label ? label : t('modqueue.actions')}
<Icon
name={this.state.open ? 'keyboard_arrow_up' : 'keyboard_arrow_down'}
className={styles.arrowIcon}
@@ -61,6 +61,8 @@ ActionsMenu.propTypes = {
icon: PropTypes.string,
children: PropTypes.node,
className: PropTypes.string,
label: PropTypes.string,
buttonClassNames: PropTypes.string,
};
export default ActionsMenu;
@@ -1,153 +1,153 @@
.dialog {
border: none;
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
width: 500px;
width: 400px;
top: 50%;
transform: translateY(-50%);
height: 184px;
padding: 20px;
border-radius: 4px;
}
h2 {
color: black;
font-size: 1.76em;
font-weight: 500;
margin: 0;
}
.header {
color: black;
font-size: 1.5em;
font-weight: 500;
margin: 0 0 8px 0;
}
h3 {
color: black;
font-size: 1.4em;
font-weight: 500;
margin: 0;
}
.subheader {
color: black;
font-size: 1.3em;
font-weight: 500;
margin: 0 0 8px 0;
}
.textField {
margin-top: 15px;
margin-top: 15px;
}
.textField label {
font-size: 1.08em;
font-weight: bold;
margin-bottom: 5px;
font-size: 1.08em;
font-weight: bold;
margin-bottom: 5px;
}
.textField input {
width: 100%;
display: block;
border: none;
outline: none;
border: 1px solid rgba(0,0,0,.12);
padding: 10px 6px;
box-sizing: border-box;
border-radius: 2px;
margin: 5px auto;
width: 100%;
display: block;
border: none;
outline: none;
border: 1px solid rgba(0,0,0,.12);
padding: 10px 6px;
box-sizing: border-box;
border-radius: 2px;
margin: 5px auto;
}
.footer {
margin: 20px auto 10px;
text-align: center;
margin: 20px auto 10px;
text-align: center;
}
.footer span {
display: block;
margin-bottom: 5px;
display: block;
margin-bottom: 5px;
}
.footer a {
color: #2c69b6;
cursor: pointer;
margin: 0 5px;
color: #2c69b6;
cursor: pointer;
margin: 0 5px;
}
.socialConnections {
margin-bottom: 20px;
margin-bottom: 20px;
}
.signInButton {
margin-top: 10px;
margin-top: 10px;
}
.close {
font-size: 20px;
line-height: 14px;
top: 10px;
right: 10px;
position: absolute;
display: block;
font-weight: bold;
color: #363636;
cursor: pointer;
font-size: 20px;
line-height: 14px;
top: 10px;
right: 10px;
position: absolute;
display: block;
font-weight: bold;
color: #363636;
cursor: pointer;
}
.close:hover {
color: #6b6b6b;
color: #6b6b6b;
}
input.error{
border: solid 2px #f44336;
border: solid 2px #f44336;
}
.errorMsg, .hint {
color: grey;
font-weight: 600;
padding: 3px 0 16px;
color: grey;
font-weight: 600;
padding: 3px 0 16px;
}
.alert {
padding: 10px;
margin-bottom: 20px;
border-radius: 2px;
padding: 10px;
margin-bottom: 20px;
border-radius: 2px;
}
.alert--success {
border: solid 1px #1ec00e;
background: #cbf1b8;
color: #006900;
border: solid 1px #1ec00e;
background: #cbf1b8;
color: #006900;
}
.alert--error {
background: #FFEBEE;
color: #B71C1C;
background: #FFEBEE;
color: #B71C1C;
}
.userBox a {
color: #2c69b6;
cursor: pointer;
margin: 0px;
color: #2c69b6;
cursor: pointer;
margin: 0px;
}
.attention {
display: inline-block;
width: 15px;
height: 15px;
background: #B71C1C;
color: #FFEBEE;
font-weight: bolder;
padding: 4px;
vertical-align: middle;
border-radius: 20px;
box-sizing: border-box;
font-size: 9px;
line-height: 7px;
text-align: center;
margin-right: 5px;
display: inline-block;
width: 15px;
height: 15px;
background: #B71C1C;
color: #FFEBEE;
font-weight: bolder;
padding: 4px;
vertical-align: middle;
border-radius: 20px;
box-sizing: border-box;
font-size: 9px;
line-height: 7px;
text-align: center;
margin-right: 5px;
}
.action {
margin-top: 15px;
margin-top: 15px;
}
.passwordRequestSuccess {
border: 1px solid green;
background-color: lightgreen;
padding: 10px;
border: 1px solid green;
background-color: lightgreen;
padding: 10px;
}
.passwordRequestFailure {
border: 1px solid orange;
background-color: 1px solid coral;
padding: 10px;
border: 1px solid orange;
background-color: 1px solid coral;
padding: 10px;
}
.cancel {
@@ -160,6 +160,20 @@ input.error{
}
.buttons {
margin: 20px;
text-align: center;
margin-top: 8px;
margin-bottom: 6px;
text-align: right;
}
.legend {
padding: 0;
font-weight: bold;
}
.messageInput {
border-radius: 3px;
width: 100%;
padding: 10px;
font-size: 14px;
box-sizing: border-box;
}
@@ -7,39 +7,132 @@ import styles from './BanUserDialog.css';
import Button from 'coral-ui/components/Button';
import t from 'coral-framework/services/i18n';
const BanUserDialog = ({open, onCancel, onPerform, username, info}) => (
<Dialog
className={cn(styles.dialog, 'talk-ban-user-dialog')}
id="banUserDialog"
open={open}
onCancel={onCancel}
title={t('bandialog.ban_user')}>
<span className={styles.close} onClick={onCancel}>×</span>
<div className={styles.header}>
<h2>{t('bandialog.ban_user')}</h2>
</div>
<div className={styles.separator}>
<h3>{t('bandialog.are_you_sure', username)}</h3>
<i>{info}</i>
</div>
<div className={styles.buttons}>
<Button
className={cn(styles.cancel, 'talk-ban-user-dialog-button-cancel')}
cStyle="cancel"
onClick={onCancel}
raised >
{t('bandialog.cancel')}
</Button>
<Button
className={cn(styles.ban, 'talk-ban-user-dialog-button-confirm')}
cStyle="black"
onClick={onPerform}
raised >
{t('bandialog.yes_ban_user')}
</Button>
</div>
</Dialog>
);
const initialState = {step: 0, message: ''};
class BanUserDialog extends React.Component {
state = initialState;
componentWillReceiveProps(next) {
if (this.props.open && !next.open) {
this.setState(initialState);
}
}
handleMessageChange = (e) => {
const {value: message} = e;
this.setState({message});
}
goToStep1 = () => {
this.setState({
step: 1,
message: t(
'bandialog.email_message_ban',
this.props.username,
),
});
}
renderStep0() {
const {
onCancel,
username,
info,
} = this.props;
return (
<section>
<h2 className={styles.header}>
{t('bandialog.ban_user')}
</h2>
<h3 className={styles.subheader}>
{t('bandialog.are_you_sure', username)}
</h3>
<p className={styles.description}>
{info}
</p>
<div className={styles.buttons}>
<Button
className={cn('talk-ban-user-dialog-button-cancel')}
cStyle="white"
onClick={onCancel}
raised >
{t('bandialog.cancel')}
</Button>
<Button
className={cn('talk-ban-user-dialog-button-confirm')}
cStyle="black"
onClick={this.goToStep1}
raised >
{t('bandialog.yes_ban_user')}
</Button>
</div>
</section>
);
}
renderStep1() {
const {
onCancel,
onPerform,
} = this.props;
const {message} = this.state;
return (
<section>
<h2 className={styles.header}>
{t('bandialog.notify_ban_headline')}
</h2>
<p className={styles.description}>
{t('bandialog.notify_ban_description')}
</p>
<fieldset>
<legend className={styles.legend}>{t('bandialog.write_a_message')}</legend>
<textarea
rows={5}
className={styles.messageInput}
value={message}
onChange={this.handleMessageChange}
/>
</fieldset>
<div className={styles.buttons}>
<Button
className={cn('talk-ban-user-dialog-button-cancel')}
cStyle="white"
onClick={onCancel}
raised >
{t('bandialog.cancel')}
</Button>
<Button
className={cn('talk-ban-user-dialog-button-confirm')}
cStyle="black"
onClick={onPerform}
raised >
{t('bandialog.send')}
</Button>
</div>
</section>
);
}
render() {
const {step} = this.state;
const {open, onCancel} = this.props;
return (
<Dialog
className={cn(styles.dialog, 'talk-ban-user-dialog')}
id="banUserDialog"
open={open}
onCancel={onCancel}
title={t('bandialog.ban_user')} >
<span className={styles.close} onClick={onCancel}>×</span>
{step === 0 && this.renderStep0()}
{step === 1 && this.renderStep1()}
</Dialog>
);
}
}
BanUserDialog.propTypes = {
open: PropTypes.bool,
@@ -17,7 +17,7 @@ function getUserFlaggedType(actions) {
.some((action) =>
action.__typename === 'FlagAction' &&
action.user &&
action.user.roles.some((role) => staffRoles.includes(role))
staffRoles.includes(action.user.role)
) ? 'Staff' : 'User';
}
@@ -4,7 +4,7 @@ import {Button} from 'coral-ui';
import styles from './LoadMore.css';
import cn from 'classnames';
const LoadMore = ({loadMore, showLoadMore, className, ...rest}) =>
const LoadMore = ({loadMore, showLoadMore, className = '', ...rest}) =>
<div {...rest} className={cn(className, styles.loadMoreContainer)}>
{
showLoadMore && <Button
@@ -16,6 +16,7 @@ const LoadMore = ({loadMore, showLoadMore, className, ...rest}) =>
</div>;
LoadMore.propTypes = {
className: PropTypes.string,
loadMore: PropTypes.func.isRequired,
showLoadMore: PropTypes.bool.isRequired
};
@@ -28,7 +28,7 @@
padding: 15px;
box-sizing: border-box;
box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.15);
z-index: 10;
z-index: 4;
.ctaHeader {
font-size: 16px;
@@ -62,7 +62,7 @@ class SuspendUserDialog extends React.Component {
const {onCancel, username} = this.props;
const {duration} = this.state;
return (
<section>
<section className="talk-admin-suspend-user-dialog-step-0">
<h1 className={styles.header}>
{t('suspenduser.title_suspend')}
</h1>
@@ -97,10 +97,10 @@ class SuspendUserDialog extends React.Component {
}
renderStep1() {
const {onCancel, username} = this.props;
const {message} = this.state;
const {onCancel, username} = this.props;
return (
<section>
<section className="talk-admin-suspend-user-dialog-step-1">
<h1 className={styles.header}>
{t('suspenduser.title_notify')}
</h1>
@@ -94,73 +94,52 @@
width: calc(100% - 90px);
}
.commentStatuses {
.tabBar {
padding: 0 0 0 10px;
margin: 0;
align-self: center;
list-style: none;
box-sizing: border-box;
li {
display: inline-block;
margin-right: 10px;
cursor: pointer;
padding: 0 10px;
}
border: none;
}
.active {
.tab {
display: inline-block;
margin-right: 10px;
cursor: pointer;
padding: 0 10px;
}
.tabButton {
border: none;
background: white;
border-radius: 0;
font-size: 1em;
padding: 5px;
cursor: pointer;
}
.tabButtonActive {
font-weight: bold;
border-bottom: 3px solid #F36451;
}
.bulkActionGroup {
height: 52px;
background-color: #efefef;
padding: 0 0 0 10px;
display: flex;
i {
margin-right: 0;
}
.bulkAction {
display: inline-block;
width: 48px;
height: 48px;
transform: scale(.7);
min-width: 0;
}
.bulkAction:last-child {
margin-left: -10px;
}
.username {
display: inline-block;
vertical-align: middle;
}
.selectedCommentsInfo {
align-self: center;
font-weight: 500;
margin-left: 15px;
.actionsMenu {
display: inline-block;
}
.loadMore>button {
background-color: #696969;
&:hover {
background-color: #404040;
color: white;
}
.actionsMenuSuspended {
background-color: #F29336;
border-color: #F29336;
color: white;
}
.toggleAll {
padding: 0 10px 0 0;
align-self: center;
.actionsMenuBanned {
background-color: #E45241;
border-color: #E45241;
color: white;
}
.commentList {
clear: both;
}
.bulkActionHeader {
display: flex;
justify-content: space-between;
height: 52px;
&.selected {
background-color: #efefef;
}
}
+176 -102
View File
@@ -1,42 +1,21 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import Comment from '../containers/UserDetailComment';
import capitalize from 'lodash/capitalize';
import {getErrorMessages} from 'coral-framework/utils';
import styles from './UserDetail.css';
import {Icon, Drawer, Spinner} from 'coral-ui';
import AccountHistory from './AccountHistory';
import {Slot} from 'coral-framework/components';
import UserDetailCommentList from '../components/UserDetailCommentList';
import {getReliability, isSuspended, isBanned} from 'coral-framework/utils/user';
import ButtonCopyToClipboard from './ButtonCopyToClipboard';
import ClickOutside from 'coral-framework/components/ClickOutside';
import LoadMore from '../components/LoadMore';
import cn from 'classnames';
import capitalize from 'lodash/capitalize';
import {getReliability} from 'coral-framework/utils/user';
import ApproveButton from './ApproveButton';
import RejectButton from './RejectButton';
import {getErrorMessages} from 'coral-framework/utils';
import {Icon, Drawer, Spinner, TabBar, Tab, TabContent, TabPane} from 'coral-ui';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import UserInfoTooltip from './UserInfoTooltip';
export default class UserDetail extends React.Component {
static propTypes = {
userId: PropTypes.string.isRequired,
hideUserDetail: PropTypes.func.isRequired,
root: PropTypes.object.isRequired,
acceptComment: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
changeStatus: PropTypes.func.isRequired,
toggleSelect: PropTypes.func.isRequired,
bulkAccept: PropTypes.func.isRequired,
bulkReject: PropTypes.func.isRequired,
toggleSelectAll: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
data: PropTypes.shape({
refetch: PropTypes.func.isRequired,
}),
activeTab: PropTypes.string.isRequired,
selectedCommentIds: PropTypes.array.isRequired,
viewUserDetail: PropTypes.any.isRequired,
loadMore: PropTypes.any.isRequired,
notify: PropTypes.func.isRequired
}
class UserDetail extends React.Component {
rejectThenReload = async (info) => {
try {
@@ -82,13 +61,19 @@ export default class UserDetail extends React.Component {
}
}
showAll = () => {
this.props.changeStatus('all');
changeTab = (tab) => {
this.props.changeTab(tab);
}
showRejected = () => {
this.props.changeStatus('rejected');
}
showSuspenUserDialog = () => this.props.showSuspendUserDialog({
userId: this.props.root.user.id,
username: this.props.root.user.username,
});
showBanUserDialog = () => this.props.showBanUserDialog({
userId: this.props.root.user.id,
username: this.props.root.user.username,
});
renderLoading() {
return (
@@ -100,15 +85,27 @@ export default class UserDetail extends React.Component {
);
}
getActionMenuLabel() {
const {root: {user}} = this.props;
if (isBanned(user)) {
return 'Banned';
} else if (isSuspended(user)) {
return 'Suspended';
}
return '';
}
renderLoaded() {
const {
data,
root,
root: {
me,
user,
totalComments,
rejectedComments,
comments: {nodes, hasNextPage}
},
activeTab,
selectedCommentIds,
@@ -116,20 +113,61 @@ export default class UserDetail extends React.Component {
hideUserDetail,
viewUserDetail,
loadMore,
toggleSelectAll
toggleSelectAll,
unbanUser,
unsuspendUser,
modal,
} = this.props;
// if totalComments is 0, you're dividing by zero
let rejectedPercent = (rejectedComments / totalComments) * 100;
if (rejectedPercent === Infinity || isNaN(rejectedPercent)) {
// if totalComments is 0, you're dividing by zero, which is naughty
if (rejectedPercent === Infinity || isNaN(rejectedPercent)) {
rejectedPercent = 0;
}
const banned = isBanned(user);
const suspended = isSuspended(user);
return (
<ClickOutside onClickOutside={hideUserDetail}>
<Drawer onClose={hideUserDetail}>
<h3>{user.username}</h3>
<ClickOutside onClickOutside={modal ? null : hideUserDetail}>
<Drawer className="talk-admin-user-detail-drawer" onClose={hideUserDetail}>
<h3 className={cn(styles.username, 'talk-admin-user-detail-username')}>
{user.username}
</h3>
{user.id &&
<ActionsMenu
icon="person"
className={cn(styles.actionsMenu, 'talk-admin-user-detail-actions-menu')}
buttonClassNames={cn({
[styles.actionsMenuSuspended]: suspended,
[styles.actionsMenuBanned]: banned,
}, 'talk-admin-user-detail-actions-button')}
label={this.getActionMenuLabel()}>
{suspended ? <ActionsMenuItem
onClick={() => unsuspendUser({id: user.id})}>
Remove Suspension
</ActionsMenuItem> : <ActionsMenuItem
disabled={me.id === user.id}
onClick={this.showSuspenUserDialog}>
Suspend User
</ActionsMenuItem>}
{banned ? <ActionsMenuItem
onClick={() => unbanUser({id: user.id})}>
Remove Ban
</ActionsMenuItem> : <ActionsMenuItem
disabled={me.id === user.id}
onClick={this.showBanUserDialog}>
Ban User
</ActionsMenuItem>}
</ActionsMenu>
}
{(banned || suspended) && <UserInfoTooltip user={user} banned={banned} suspended={suspended} />}
<div>
<ul className={styles.userDetailList}>
@@ -173,65 +211,72 @@ export default class UserDetail extends React.Component {
data={this.props.data}
queryData={{root, user}}
/>
<hr />
<div className={(selectedCommentIds.length > 0) ? cn(styles.bulkActionHeader, styles.selected) : styles.bulkActionHeader}>
{
selectedCommentIds.length === 0
? (
<ul className={styles.commentStatuses}>
<li className={activeTab === 'all' ? styles.active : ''} onClick={this.showAll}>All</li>
<li className={activeTab === 'rejected' ? styles.active : ''} onClick={this.showRejected}>Rejected</li>
</ul>
)
: (
<div className={styles.bulkActionGroup}>
<ApproveButton
onClick={this.bulkAcceptThenReload}
minimal
/>
<RejectButton
onClick={this.bulkRejectThenReload}
minimal
/>
<span className={styles.selectedCommentsInfo}> {selectedCommentIds.length} comments selected</span>
</div>
)
}
<div className={styles.toggleAll}>
<input
type='checkbox'
id='toogleAll'
checked={selectedCommentIds.length > 0 && selectedCommentIds.length === nodes.length}
onChange={(e) => {
toggleSelectAll(nodes.map((comment) => comment.id), e.target.checked);
}} />
<label htmlFor='toogleAll'>Select all</label>
</div>
</div>
<div className={styles.commentList}>
{
nodes.map((comment) => {
const selected = selectedCommentIds.indexOf(comment.id) !== -1;
return <Comment
key={comment.id}
user={user}
root={root}
data={data}
comment={comment}
acceptComment={this.acceptThenReload}
rejectComment={this.rejectThenReload}
selected={selected}
toggleSelect={toggleSelect}
viewUserDetail={viewUserDetail}
/>;
})
}
</div>
<LoadMore
className={styles.loadMore}
loadMore={loadMore}
showLoadMore={hasNextPage}
/>
<TabBar
onTabClick={this.changeTab}
activeTab={activeTab}
className={cn(styles.tabBar, 'talk-admin-user-detail-tab-bar')}
aria-controls='talk-admin-user-detail-content'
tabClassNames={{
button: styles.tabButton,
buttonActive: styles.tabButtonActive,
}} >
<Tab
tabId={'all'}
className={cn(styles.tab, styles.button, 'talk-admin-user-detail-all-tab')} >
All
</Tab>
<Tab
tabId={'rejected'}
className={cn(styles.tab, 'talk-admin-user-detail-rejected-tab')} >
Rejected
</Tab>
<Tab tabId={'history'} className={cn(styles.tab, styles.button, 'talk-admin-user-detail-history-tab')}>
Account History
</Tab>
</TabBar>
<TabContent activeTab={activeTab} className='talk-admin-user-detail-content'>
<TabPane tabId={'all'} className={'talk-admin-user-detail-all-tab-pane'}>
<UserDetailCommentList
user={user}
root={root}
data={data}
loadMore={loadMore}
toggleSelect={toggleSelect}
viewUserDetail={viewUserDetail}
acceptComment={this.acceptThenReload}
rejectComment={this.rejectThenReload}
selectedCommentIds={selectedCommentIds}
toggleSelectAll={toggleSelectAll}
bulkAcceptThenReload={this.bulkAcceptThenReload}
bulkRejectThenReload={this.bulkRejectThenReload}
/>
</TabPane>
<TabPane tabId={'rejected'} className={'talk-admin-user-detail-rejected-tab-pane'}>
<UserDetailCommentList
user={user}
root={root}
data={data}
loadMore={loadMore}
toggleSelect={toggleSelect}
viewUserDetail={viewUserDetail}
acceptComment={this.acceptThenReload}
rejectComment={this.rejectThenReload}
selectedCommentIds={selectedCommentIds}
toggleSelectAll={toggleSelectAll}
bulkAcceptThenReload={this.bulkAcceptThenReload}
bulkRejectThenReload={this.bulkRejectThenReload}
/>
</TabPane>
<TabPane tabId={'history'} className={'talk-admin-user-detail-history-tab-pane'}>
<AccountHistory
userState={user.state}
/>
</TabPane>
</TabContent>
</Drawer>
</ClickOutside>
);
@@ -244,3 +289,32 @@ export default class UserDetail extends React.Component {
return this.renderLoaded();
}
}
UserDetail.propTypes = {
userId: PropTypes.string.isRequired,
hideUserDetail: PropTypes.func.isRequired,
root: PropTypes.object.isRequired,
acceptComment: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
changeTab: PropTypes.func.isRequired,
toggleSelect: PropTypes.func.isRequired,
bulkAccept: PropTypes.func.isRequired,
bulkReject: PropTypes.func.isRequired,
toggleSelectAll: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
data: PropTypes.shape({
refetch: PropTypes.func.isRequired,
}),
activeTab: PropTypes.string.isRequired,
selectedCommentIds: PropTypes.array.isRequired,
viewUserDetail: PropTypes.any.isRequired,
loadMore: PropTypes.any.isRequired,
notify: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func,
showBanUserDialog: PropTypes.func,
unbanUser: PropTypes.func.isRequired,
unsuspendUser: PropTypes.func.isRequired,
modal: PropTypes.bool,
};
export default UserDetail;
@@ -117,6 +117,8 @@ class UserDetailComment extends React.Component {
}
UserDetailComment.propTypes = {
selected: PropTypes.bool,
data: PropTypes.object,
user: PropTypes.object.isRequired,
viewUserDetail: PropTypes.func.isRequired,
acceptComment: PropTypes.func.isRequired,
@@ -0,0 +1,56 @@
.commentList {
clear: both;
}
.loadMore > button {
background-color: #696969;
&:hover {
background-color: #404040;
color: white;
}
}
.bulkActionHeader {
display: flex;
justify-content: space-between;
height: 52px;
justify-content: flex-end;
&.selected {
background-color: #efefef;
}
}
.bulkActionGroup {
height: 52px;
background-color: #efefef;
padding: 0 0 0 10px;
display: flex;
flex: 1;
i {
margin-right: 0;
}
.bulkAction {
display: inline-block;
width: 48px;
height: 48px;
transform: scale(.7);
min-width: 0;
}
.bulkAction:last-child {
margin-left: -10px;
}
}
.selectedCommentsInfo {
align-self: center;
font-weight: 500;
margin-left: 15px;
}
.toggleAll {
padding-right: 14px;
align-self: center;
}
@@ -0,0 +1,102 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import styles from './UserDetailCommentList.css';
import LoadMore from '../components/LoadMore';
import Comment from '../containers/UserDetailComment';
import RejectButton from './RejectButton';
import ApproveButton from './ApproveButton';
const UserDetailCommentList = (props) => {
const {
data,
root,
root: {
user,
comments: {
nodes,
hasNextPage
}
},
acceptComment,
rejectComment,
selectedCommentIds,
toggleSelect,
viewUserDetail,
loadMore,
toggleSelectAll,
bulkAcceptThenReload,
bulkRejectThenReload,
} = props;
return (
<div className={cn(styles.commentList, 'talk-admin-user-detail-comment-list')}>
<div className={(selectedCommentIds.length > 0) ? cn(styles.bulkActionHeader, styles.selected) : styles.bulkActionHeader}>
{selectedCommentIds.length > 0 && (
<div className={styles.bulkActionGroup}>
<ApproveButton
onClick={bulkAcceptThenReload}
minimal
/>
<RejectButton
onClick={bulkRejectThenReload}
minimal
/>
<span className={styles.selectedCommentsInfo}> {selectedCommentIds.length} comments selected</span>
</div>
)}
<div className={styles.toggleAll}>
<input
type='checkbox'
id='toogleAll'
checked={selectedCommentIds.length > 0 && selectedCommentIds.length === nodes.length}
onChange={(e) => {
toggleSelectAll(nodes.map((comment) => comment.id), e.target.checked);
}} />
<label htmlFor='toogleAll'>Select all</label>
</div>
</div>
{
nodes.map((comment) => {
const selected = selectedCommentIds.indexOf(comment.id) !== -1;
return <Comment
key={comment.id}
user={user}
root={root}
data={data}
comment={comment}
acceptComment={acceptComment}
rejectComment={rejectComment}
selected={selected}
toggleSelect={toggleSelect}
viewUserDetail={viewUserDetail}
/>;
})
}
<LoadMore
className={styles.loadMore}
loadMore={loadMore}
showLoadMore={hasNextPage}
/>
</div>
);
};
UserDetailCommentList.propTypes = {
root: PropTypes.object.isRequired,
acceptComment: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
data: PropTypes.object.isRequired,
selectedCommentIds: PropTypes.array.isRequired,
viewUserDetail: PropTypes.any.isRequired,
loadMore: PropTypes.any.isRequired,
toggleSelect: PropTypes.func.isRequired,
toggleSelectAll: PropTypes.func.isRequired,
bulkAcceptThenReload: PropTypes.func.isRequired,
bulkRejectThenReload: PropTypes.func.isRequired,
};
export default UserDetailCommentList;
@@ -0,0 +1,69 @@
.userInfo {
position: relative;
display: inline-block;
}
.icon {
font-size: 16px;
color: #616161;
-ms-user-select:none;
-moz-user-select: none;
-webkit-user-select: none;
-webkit-touch-callout:none;
user-select: none;
-webkit-tap-highlight-color:rgba(0,0,0,0);
}
.icon:hover {
cursor: pointer;
}
.menu {
background-color: white;
border: solid 1px #999;
border-radius: 3px;
padding: 10px;
position: absolute;
-webkit-box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 1px 5px 0 rgba(0,0,0,0.12), 0 3px 1px -2px rgba(0,0,0,0.2);
z-index: 10;
top: 32px;
right: 0px;
width: 140px;
text-align: left;
color: #616161;
}
.menu::before{
content: '';
border: 10px solid transparent;
border-top-color: #999;
position: absolute;
right: 0px;
top: -20px;
transform: rotate(180deg);
}
.menu::after{
content: '';
border: 10px solid transparent;
border-top-color: white;
position: absolute;
right: 0px;
top: -19px;
transform: rotate(180deg);
}
.descriptionList {
padding: 0;
margin: 0;
list-style: none;
}
.strongItem {
margin-right: 3px;
}
.descriptionItem {
font-size: 0.9em;
}
@@ -0,0 +1,95 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import {Icon} from 'coral-ui';
import styles from './UserInfoTooltip.css';
import ClickOutside from 'coral-framework/components/ClickOutside';
import moment from 'moment';
const initialState = {menuVisible: false};
class UserInfoTooltip extends React.Component {
state = initialState;
toogleMenu = () => {
this.setState({menuVisible: !this.state.menuVisible});
}
hideMenu = () => {
this.setState({menuVisible: false});
}
getLastHistoryItem = (user, status = 'banned') => {
const userHistory = user.state.status[status].history;
return userHistory[userHistory.length - 1];
}
render() {
const {menuVisible} = this.state;
const {user, banned, suspended} = this.props;
return (
<ClickOutside onClickOutside={this.hideMenu}>
<div className={cn(styles.userInfo, 'talk-admin-user-info-tooltip')}>
<span onClick={this.toogleMenu} className={cn(styles.icon, 'talk-admin-user-info-tooltip-icon')}>
<Icon name="info_outline" />
</span>
{menuVisible && (
<div className={cn(styles.menu, 'talk-admin-user-info-tooltip-menu')}>
{
banned && (
<div className={cn(styles.description, 'talk-admin-user-info-tooltip-description-banned')}>
<ul className={cn(styles.descriptionList, 'talk-admin-user-info-tooltip-description-list')}>
<li className={cn(styles.descriptionItem, 'talk-admin-user-info-tooltip-description-item')}>
<strong className={styles.strongItem}>Banned On</strong>
<span>{moment(new Date(this.getLastHistoryItem(user, 'banned').created_at)).format('MMMM Do YYYY, h:mm:ss a')}</span>
</li>
<li className={cn(styles.descriptionItem, 'talk-admin-user-info-tooltip-description-item')}>
<strong className={styles.strongItem}>By</strong>
<span>{this.getLastHistoryItem(user, 'banned').assigned_by.username}</span>
</li>
</ul>
</div>
)
}
{
suspended && (
<div className={cn(styles.description, 'talk-admin-user-info-tooltip-description-suspended')}>
<ul className={cn(styles.descriptionList, 'talk-admin-user-info-tooltip-description-list')}>
<li className={cn(styles.descriptionItem, 'talk-admin-user-info-tooltip-description-item')}>
<strong className={styles.strongItem}>Suspension</strong>
<span></span>
</li>
<li className={cn(styles.descriptionItem, 'talk-admin-user-info-tooltip-description-item')}>
<strong className={styles.strongItem}>By</strong>
<span>{this.getLastHistoryItem(user, 'suspension').assigned_by.username}</span>
</li>
<li className={cn(styles.descriptionItem, 'talk-admin-user-info-tooltip-description-item')}>
<strong className={styles.strongItem}>Start</strong>
<span>{moment(new Date(this.getLastHistoryItem(user, 'suspension').created_at)).format('MMMM Do YYYY, h:mm:ss a')}</span>
</li>
<li className={cn(styles.descriptionItem, 'talk-admin-user-info-tooltip-description-item')}>
<strong className={styles.strongItem}>End</strong>
<span>{moment(new Date(this.getLastHistoryItem(user, 'suspension').until)).format('MMMM Do YYYY, h:mm:ss a')}</span>
</li>
</ul>
</div>
)
}
</div>
)}
</div>
</ClickOutside>
);
}
}
UserInfoTooltip.propTypes = {
user: PropTypes.object,
banned: PropTypes.bool,
suspended: PropTypes.bool,
};
export default UserInfoTooltip;
@@ -6,8 +6,6 @@ export const FETCH_USERS_FAILURE = `${prefix}_FETCH_USERS_FAILURE`;
export const SORT_UPDATE = `${prefix}_SORT_UPDATE`;
export const SET_PAGE = `${prefix}_SET_PAGE`;
export const SET_ROLE = `${prefix}_SET_ROLE`;
export const SET_COMMENTER_STATUS = `${prefix}_SET_COMMENTER_STATUS`;
export const FETCH_FLAGGED_COMMENTERS_REQUEST = `${prefix}_FETCH_FLAGGED_COMMENTERS_REQUEST`;
export const FETCH_FLAGGED_COMMENTERS_SUCCESS = `${prefix}_FETCH_FLAGGED_COMMENTERS_SUCCESS`;
@@ -1,6 +1,6 @@
export const VIEW_USER_DETAIL = 'VIEW_USER_DETAIL';
export const HIDE_USER_DETAIL = 'HIDE_USER_DETAIL';
export const CHANGE_USER_DETAIL_STATUSES = 'CHANGE_USER_DETAIL_STATUSES';
export const CHANGE_TAB_USER_DETAIL = 'CHANGE_TAB_USER_DETAIL';
export const SELECT_USER_DETAIL_COMMENT = 'SELECT_USER_DETAIL_COMMENT';
export const UNSELECT_USER_DETAIL_COMMENT = 'UNSELECT_USER_DETAIL_COMMENT';
export const CLEAR_USER_DETAIL_SELECTIONS = 'CLEAR_USER_DETAIL_SELECTIONS';
@@ -1,9 +1,10 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import BanUserDialog from '../components/BanUserDialog';
import {hideBanUserDialog} from '../actions/banUserDialog';
import {withSetUserStatus, withSetCommentStatus} from 'coral-framework/graphql/mutations';
import {withBanUser, withSetCommentStatus} from 'coral-framework/graphql/mutations';
import {compose} from 'react-apollo';
import t from 'coral-framework/services/i18n';
import {getErrorMessages} from 'coral-framework/utils';
@@ -12,9 +13,9 @@ import {notify} from 'coral-framework/actions/notification';
class BanUserDialogContainer extends Component {
banUser = async () => {
const {userId, commentId, commentStatus, setUserStatus, setCommentStatus, hideBanUserDialog, notify} = this.props;
const {userId, commentId, commentStatus, banUser, setCommentStatus, hideBanUserDialog, notify} = this.props;
try {
await setUserStatus({userId, status: 'BANNED'});
await banUser({id: userId, message: ''});
hideBanUserDialog();
if (commentId && commentStatus && commentStatus !== 'REJECTED') {
await setCommentStatus({commentId, status: 'REJECTED'});
@@ -46,6 +47,14 @@ class BanUserDialogContainer extends Component {
}
}
BanUserDialogContainer.propTypes = {
banUser: PropTypes.func.isRequired,
hideBanUserDialog: PropTypes.func,
open: PropTypes.bool,
username: PropTypes.string,
commentStatus: PropTypes.string,
};
const mapStateToProps = ({banUserDialog: {open, userId, username, commentId, commentStatus}}) => ({
open,
userId,
@@ -62,7 +71,7 @@ const mapDispatchToProps = (dispatch) => ({
});
export default compose(
withSetUserStatus,
withBanUser,
withSetCommentStatus,
connect(
mapStateToProps,
@@ -25,7 +25,7 @@ export default withFragments({
}
user {
id
roles
role
}
}
${getSlotFragmentSpreads(slots, 'comment')}
+5 -1
View File
@@ -14,7 +14,11 @@ export default withQuery(gql`
})
flaggedUsernamesCount: userCount(query: {
action_type: FLAG,
statuses: [PENDING]
state: {
status: {
username: [SET, CHANGED]
}
}
})
}
`, {
@@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import {compose, gql} from 'react-apollo';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
@@ -8,15 +9,17 @@ import {getDefinitionName, getSlotFragmentSpreads} from 'coral-framework/utils';
import {
viewUserDetail,
hideUserDetail,
changeUserDetailStatuses,
changeTab,
clearUserDetailSelections,
toggleSelectCommentInUserDetail,
toggleSelectAllCommentInUserDetail
} from 'coral-admin/src/actions/userDetail';
import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
import {withSetCommentStatus, withUnbanUser, withUnsuspendUser} from 'coral-framework/graphql/mutations';
import UserDetailComment from './UserDetailComment';
import update from 'immutability-helper';
import {notify} from 'coral-framework/actions/notification';
import {showBanUserDialog} from 'actions/banUserDialog';
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
const commentConnectionFragment = gql`
fragment CoralAdmin_UserDetail_CommentConnection on CommentConnection {
@@ -114,7 +117,7 @@ class UserDetailContainer extends React.Component {
return <UserDetail
bulkReject={this.bulkReject}
bulkAccept={this.bulkAccept}
changeStatus={this.props.changeUserDetailStatuses}
changeTab={this.props.changeTab}
toggleSelect={this.props.toggleSelectCommentInUserDetail}
toggleSelectAll={this.props.toggleSelectAllCommentInUserDetail}
acceptComment={this.acceptComment}
@@ -125,6 +128,19 @@ class UserDetailContainer extends React.Component {
}
}
UserDetailContainer.propTypes = {
changeTab: PropTypes.func,
toggleSelectCommentInUserDetail: PropTypes.func,
toggleSelectAllCommentInUserDetail: PropTypes.func,
data: PropTypes.object,
root: PropTypes.object,
setCommentStatus: PropTypes.func,
clearUserDetailSelections: PropTypes.func,
selectedCommentIds: PropTypes.array,
unbanUser: PropTypes.func.isRequired,
unsuspendUser: PropTypes.func.isRequired,
};
const LOAD_MORE_QUERY = gql`
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $author_id: ID!, $statuses: [COMMENT_STATUS!]) {
comments(query: {limit: $limit, cursor: $cursor, author_id: $author_id, statuses: $statuses}) {
@@ -147,8 +163,45 @@ export const withUserDetailQuery = withQuery(gql`
reliable {
flagger
}
state {
status {
suspension {
until
history {
until
created_at
assigned_by {
username
}
}
}
banned {
status
history {
status
assigned_by {
username
}
created_at
}
}
username {
status
history {
status
assigned_by {
username
}
created_at
}
}
}
}
${getSlotFragmentSpreads(slots, 'user')}
}
me {
id
}
totalComments: commentCount(query: {author_id: $author_id, statuses: []})
rejectedComments: commentCount(query: {author_id: $author_id, statuses: [REJECTED]})
comments: comments(query: {
@@ -177,11 +230,14 @@ const mapStateToProps = (state) => ({
selectedCommentIds: state.userDetail.selectedCommentIds,
statuses: state.userDetail.statuses,
activeTab: state.userDetail.activeTab,
modal: state.ui.modal
});
const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
changeUserDetailStatuses,
showBanUserDialog,
showSuspendUserDialog,
changeTab,
clearUserDetailSelections,
toggleSelectCommentInUserDetail,
viewUserDetail,
@@ -195,4 +251,6 @@ export default compose(
connect(mapStateToProps, mapDispatchToProps),
withUserDetailQuery,
withSetCommentStatus,
withUnbanUser,
withUnsuspendUser,
)(UserDetailContainer);
+125 -7
View File
@@ -1,35 +1,154 @@
import update from 'immutability-helper';
import {mapLeaves} from 'coral-framework/utils';
import {gql} from 'react-apollo';
const userStatusFragment = gql`
fragment Talk_UpdateUserStatus on User {
state {
status {
banned {
status
}
suspension {
until
}
}
}
}`;
const userRoleFragment = gql`
fragment Talk_UpdateUserRole on User {
role
}`;
export default {
mutations: {
SetUserStatus: ({variables: {status, userId}}) => ({
SetUserRole: ({variables: {id, role}}) => ({
update: (proxy) => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({fragment: userRoleFragment, id: fragmentId});
const updated = update(data, {
role: {
$set: role
}
});
proxy.writeFragment({fragment: userRoleFragment, id: fragmentId, data: updated});
},
}),
SuspendUser: ({variables: {input: {id, until}}}) => ({
update: (proxy) => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({fragment: userStatusFragment, id: fragmentId});
const updated = update(data, {
state : {
status: {
suspension: {
until: {$set: until}
}
}
}
});
proxy.writeFragment({fragment: userStatusFragment, id: fragmentId, data: updated});
}
}),
UnsuspendUser: ({variables: {input: {id}}}) => ({
update: (proxy) => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({fragment: userStatusFragment, id: fragmentId});
const updated = update(data, {
state : {
status: {
suspension: {
until: {$set: null}
}
}
}
});
proxy.writeFragment({fragment: userStatusFragment, id: fragmentId, data: updated});
},
}),
BanUser: ({variables: {input: {id}}}) => ({
update: (proxy) => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({fragment: userStatusFragment, id: fragmentId});
const updated = update(data, {
state : {
status: {
banned: {
status: {$set: true}
}
}
}
});
proxy.writeFragment({fragment: userStatusFragment, id: fragmentId, data: updated});
}
}),
UnbanUser: ({variables: {input: {id}}}) => ({
update: (proxy) => {
const fragmentId = `User_${id}`;
const data = proxy.readFragment({fragment: userStatusFragment, id: fragmentId});
const updated = update(data, {
state : {
status: {
banned: {
status: {$set: false}
}
}
}
});
proxy.writeFragment({fragment: userStatusFragment, id: fragmentId, data: updated});
}
}),
SetUserBanStatus: ({variables: {status, id}}) => ({
updateQueries: {
TalkAdmin_Community: (prev) => {
if (status !== 'APPROVED') {
if (!status) {
return prev;
}
const updated = update(prev, {
users: {
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)},
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== id)},
},
});
return updated;
}
}
}),
RejectUsername: ({variables: {input: {id: userId}}}) => ({
ApproveUsername: ({variables: {id}}) => ({
updateQueries: {
TalkAdmin_Community: (prev) => {
const updated = update(prev, {
users: {
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)},
flaggedUsers: {
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== id)},
},
});
return updated;
}
}
}),
RejectUsername: ({variables: {id: userId}}) => ({
updateQueries: {
TalkAdmin_Community: (prev) => {
const updated = update(prev, {
flaggedUsers: {
nodes: {$apply: (nodes) => nodes.filter((node) => node.id !== userId)},
},
});
return updated;
}
},
}),
UpdateSettings: ({variables: {input}}) => ({
updateQueries: {
TalkAdmin_Configure: (prev) => {
@@ -42,4 +161,3 @@ export default {
}),
},
};
@@ -5,8 +5,6 @@ import {
SORT_UPDATE,
SET_PAGE,
SET_SEARCH_VALUE,
SET_ROLE,
SET_COMMENTER_STATUS,
SHOW_BANUSER_DIALOG,
HIDE_BANUSER_DIALOG,
SHOW_REJECT_USERNAME_DIALOG,
@@ -60,27 +58,6 @@ export default function community (state = initialState, action) {
...state,
pagePeople: action.page,
};
case SET_ROLE : {
const commenters = state.users;
const idx = commenters.findIndex((el) => el.id === action.id);
commenters[idx].roles[0] = action.role;
return {
...state,
users: commenters.map((id) => id),
};
}
case SET_COMMENTER_STATUS: {
const commenters = state.users;
const idx = commenters.findIndex((el) => el.id === action.id);
commenters[idx].status = action.status;
return {
...state,
users: commenters.map((id) => id),
};
}
case SORT_UPDATE :
return {
...state,
+2
View File
@@ -8,6 +8,7 @@ import config from './config';
import banUserDialog from './banUserDialog';
import suspendUserDialog from './suspendUserDialog';
import userDetail from './userDetail';
import ui from './ui';
export default {
auth,
@@ -20,4 +21,5 @@ export default {
moderation,
install,
config,
ui,
};
+33
View File
@@ -0,0 +1,33 @@
import {SHOW_BAN_USER_DIALOG, HIDE_BAN_USER_DIALOG} from '../constants/banUserDialog';
import {SHOW_SUSPEND_USER_DIALOG, HIDE_SUSPEND_USER_DIALOG} from '../constants/suspendUserDialog';
const initialState = {
modal: false
};
export default function config (state = initialState, action) {
switch (action.type) {
case SHOW_BAN_USER_DIALOG:
return {
...state,
modal: true,
};
case SHOW_SUSPEND_USER_DIALOG:
return {
...state,
modal: true,
};
case HIDE_BAN_USER_DIALOG:
return {
...state,
modal: false,
};
case HIDE_SUSPEND_USER_DIALOG:
return {
...state,
modal: false,
};
default:
return state;
}
}
@@ -25,7 +25,7 @@ export default function banUserDialog(state = initialState, action) {
...state,
selectedCommentIds: [],
};
case actions.CHANGE_USER_DETAIL_STATUSES:
case actions.CHANGE_TAB_USER_DETAIL:
return {
...state,
activeTab: action.tab,
@@ -12,7 +12,11 @@ class Community extends Component {
const activeTab = route.path === ':id' ? 'flagged' : route.path;
if (activeTab === 'people') {
return <People community={community} />;
return <People
community={community}
data={this.props.data}
root={this.props.root}
/>;
}
return (
@@ -13,8 +13,6 @@ class FlaggedAccounts extends React.Component {
const {
users,
loadMore,
showBanUserDialog,
showSuspendUserDialog,
showRejectUsernameDialog,
approveUser,
me,
@@ -48,8 +46,6 @@ class FlaggedAccounts extends React.Component {
<FlaggedUser
user={user}
key={user.id}
showBanUserDialog={showBanUserDialog}
showSuspendUserDialog={showSuspendUserDialog}
showRejectUsernameDialog={showRejectUsernameDialog}
approveUser={approveUser}
me={me}
@@ -74,8 +70,6 @@ class FlaggedAccounts extends React.Component {
FlaggedAccounts.propTypes = {
users: PropTypes.object,
loadMore: PropTypes.func,
showBanUserDialog: PropTypes.func,
showSuspendUserDialog: PropTypes.func,
showRejectUsernameDialog: PropTypes.func,
approveUser: PropTypes.func,
me: PropTypes.object,
@@ -4,8 +4,6 @@ import PropTypes from 'prop-types';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
import {username} from 'talk-plugin-flags/helpers/flagReasons';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import ApproveButton from 'coral-admin/src/components/ApproveButton';
import RejectButton from 'coral-admin/src/components/RejectButton';
@@ -19,18 +17,10 @@ const shortReasons = {
class User extends React.Component {
showSuspenUserDialog = () => this.props.showSuspendUserDialog({
userId: this.props.user.id,
username: this.props.user.username,
});
showBanUserDialog = () => this.props.showBanUserDialog({
userId: this.props.user.id,
username: this.props.user.username,
});
viewAuthorDetail = () => this.props.viewUserDetail(this.props.user.id);
showRejectUsernameDialog = () => this.props.showRejectUsernameDialog({id: this.props.user.id});
approveUser = () => this.props.approveUser({
userId: this.props.user.id,
});
@@ -40,7 +30,6 @@ class User extends React.Component {
user,
viewUserDetail,
selected,
me,
className,
} = this.props;
@@ -55,20 +44,6 @@ class User extends React.Component {
className={styles.button}>
{user.username}
</button>
{me.id !== user.id &&
<ActionsMenu icon="not_interested">
<ActionsMenuItem
disabled={user.status === 'BANNED'}
onClick={this.showSuspenUserDialog}>
Suspend User
</ActionsMenuItem>
<ActionsMenuItem
disabled={user.status === 'BANNED'}
onClick={this.showBanUserDialog}>
Ban User
</ActionsMenuItem>
</ActionsMenu>
}
</div>
</div>
<div className={cn('talk-admin-community-flagged-user-body', styles.body)}>
@@ -136,8 +111,6 @@ class User extends React.Component {
}
User.propTypes = {
showSuspendUserDialog: PropTypes.func,
showBanUserDialog: PropTypes.func,
viewUserDetail: PropTypes.func,
showRejectUsernameDialog: PropTypes.func,
approveUser: PropTypes.func,
@@ -1,3 +1,45 @@
.container {
padding: 10px;
display: flex;
padding-bottom: 200px;
}
.leftColumn {
padding: 42px 56px;
width: 234px;
}
.searchBox {
width: 100%;
padding: 9px;
border: 1px solid #ccc;
border-radius: 2px;
display: flex;
background: white;
box-sizing: border-box;
height: 40px;
i {
color: #A1A1A1
}
input {
display: block;
width: 100%;
height: 100%;
border: none;
font-size: 16px;
padding: 0 2px 0 15px;
box-sizing: border-box;
}
}
.mainContent {
width: calc(100% - 300px);
padding: 34px 14px;
box-sizing: border-box;
}
.dataTable {
width: 100%;
border-left: none;
@@ -8,10 +50,6 @@ th.header {
font-size: 1.1em;
}
th.header:hover {
cursor: pointer;
}
th.header:nth-child(2), th.header:nth-child(3) {
width: 100px;
}
@@ -67,3 +105,28 @@ th.header:nth-child(2), th.header:nth-child(3) {
}
}
.actionsMenu {
font-size: 1em;
}
.actionsMenu .actionsMenuButton {
-webkit-transform: scale(1);
transform: scale(1);
font-size: 0.98em;
}
.actionsMenuSuspended {
background-color: #F29336;
border-color: #F29336;
color: white;
}
.actionsMenuBanned {
background-color: #E45241;
border-color: #E45241;
color: white;
}
.loadMore {
margin-top: 24px;
}
@@ -1,75 +1,191 @@
import React from 'react';
import cn from 'classnames';
import styles from './styles.css';
import Table from './Table';
import {Icon} from 'coral-ui';
import styles from './People.css';
import {Icon, Dropdown, Option} from 'coral-ui';
import EmptyCard from '../../../components/EmptyCard';
import t from 'coral-framework/services/i18n';
import LoadMore from '../../../components/LoadMore';
import PropTypes from 'prop-types';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import {isSuspended, isBanned} from 'coral-framework/utils/user';
import moment from 'moment';
const People = (props) => {
const {
users = [],
searchValue,
onSearchChange,
onHeaderClickHandler,
onPageChange,
totalPages,
page,
setRole,
setCommenterStatus,
viewUserDetail,
} = props;
const headers = [
{
title: t('community.username_and_email'),
field: 'username'
},
{
title: t('community.account_creation_date'),
field: 'created_at'
},
{
title: t('community.status'),
field: 'status'
},
{
title: t('community.newsroom_role'),
field: 'role'
}
];
const hasResults = !!users.length;
class People extends React.Component {
return (
<div className={cn(styles.container, 'talk-admin-community-people-container')}>
<div className={styles.leftColumn}>
<div className={styles.searchBox}>
<Icon name='search' className={styles.searchIcon}/>
<input
id="commenters-search"
type="text"
className={styles.searchBoxInput}
value={searchValue}
onChange={onSearchChange}
placeholder={t('streams.search')}
/>
getActionMenuLabel = (user) => {
if (isBanned(user)) {
return 'Banned';
} else if (isSuspended(user)) {
return 'Suspended';
}
return '';
};
unsuspendUser = (input) => {
this.props.unsuspendUser(input);
}
unbanUser = (input) => {
this.props.unbanUser(input);
}
showBanUserDialog = (input) => {
this.props.showBanUserDialog(input);
}
showSuspendUserDialog = (input) => {
this.props.showSuspendUserDialog(input);
}
render () {
const {
onSearchChange,
users = [],
setUserRole,
viewUserDetail,
loadMore,
} = this.props;
const hasResults = !!users.nodes.length;
return (
<div className={cn(styles.container, 'talk-admin-community-people-container')}>
<div className={styles.leftColumn}>
<div className={styles.searchBox}>
<Icon name='search' className={styles.searchIcon}/>
<input
id="commenters-search"
type="text"
className={styles.searchBoxInput}
defaultValue=''
onChange={onSearchChange}
placeholder={t('streams.search')}
/>
</div>
</div>
<div className={styles.mainContent}>
{
hasResults
? <div>
<div>
<table className={`mdl-data-table ${styles.dataTable}`}>
<thead>
<tr>
{headers.map((header, i) =>(
<th
key={i}
className={cn('mdl-data-table__cell--non-numeric', styles.header)}
scope="col" >
{header.title}
</th>
))}
</tr>
</thead>
<tbody>
{users.nodes.map((user)=> (
<tr key={user.id} className="talk-admin-community-people-row">
<td className="mdl-data-table__cell--non-numeric">
<button onClick={() => {viewUserDetail(user.id);}} className={cn(styles.username, styles.button)}>{user.username}</button>
<span className={styles.email}>{user.profiles.map(({id}) => id)}</span>
</td>
<td className="mdl-data-table__cell--non-numeric">
{moment(new Date(user.created_at)).format('MMMM Do YYYY, h:mm:ss a')}
</td>
<td className="mdl-data-table__cell--non-numeric">
<ActionsMenu
icon="person"
className={cn(styles.actionsMenu, 'talk-admin-community-people-dd-status')}
buttonClassNames={cn(styles.actionsMenuButton, {
[styles.actionsMenuSuspended]: isSuspended(user),
[styles.actionsMenuBanned]: isBanned(user),
}, 'talk-admin-user-detail-actions-button')}
label={this.getActionMenuLabel(user)} >
{isSuspended(user) ? <ActionsMenuItem
onClick={() => this.unsuspendUser({id: user.id})}>
Remove Suspension
</ActionsMenuItem> : <ActionsMenuItem
onClick={() => this.showSuspendUserDialog({
userId: user.id,
username: user.username,
})}>
Suspend User
</ActionsMenuItem>}
{isBanned(user) ? <ActionsMenuItem
onClick={() => this.unbanUser({id: user.id})}>
Remove Ban
</ActionsMenuItem> : <ActionsMenuItem
onClick={() => this.showBanUserDialog({
userId: user.id,
username: user.username,
})}>
Ban User
</ActionsMenuItem>}
</ActionsMenu>
</td>
<td className="mdl-data-table__cell--non-numeric">
<Dropdown
containerClassName="talk-admin-community-people-dd-role"
value={user.role}
placeholder={t('community.role')}
onChange={(role) => setUserRole(user.id, role)}>
<Option value={'COMMENTER'} label={t('community.commenter')} />
<Option value={'STAFF'} label={t('community.staff')} />
<Option value={'MODERATOR'} label={t('community.moderator')} />
<Option value={'ADMIN'} label={t('community.admin')} />
</Dropdown>
</td>
</tr>
))}
</tbody>
</table>
</div>
<LoadMore
className={styles.loadMore}
loadMore={loadMore}
showLoadMore={users.hasNextPage}
/>
</div>
: <EmptyCard>{t('community.no_results')}</EmptyCard>
}
</div>
</div>
<div className={styles.mainContent}>
{
hasResults
? <Table
users={users}
setRole={setRole}
viewUserDetail={viewUserDetail}
setCommenterStatus={setCommenterStatus}
onHeaderClickHandler={onHeaderClickHandler}
pageCount={totalPages}
onPageChange={onPageChange}
page={page}
/>
: <EmptyCard>{t('community.no_results')}</EmptyCard>
}
</div>
</div>
);
};
);
}
}
People.propTypes = {
onHeaderClickHandler: PropTypes.func,
users: PropTypes.array,
page: PropTypes.number.isRequired,
searchValue: PropTypes.string,
onSearchChange: PropTypes.func,
totalPages: PropTypes.number,
onPageChange: PropTypes.func,
setCommenterStatus: PropTypes.func.isRequired,
setRole: PropTypes.func.isRequired,
users: PropTypes.object.isRequired,
onSearchChange: PropTypes.func.isRequired,
setUserRole: PropTypes.func.isRequired,
viewUserDetail: PropTypes.func.isRequired,
unbanUser: PropTypes.func.isRequired,
unsuspendUser: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func,
showBanUserDialog: PropTypes.func,
loadMore: PropTypes.func.isRequired,
};
export default People;
@@ -2,7 +2,7 @@
.modalButtons {
display: flex;
justify-content: flex-end;
margin-top: 50px;
margin-top: 20px;
}
.emailInput {
@@ -45,7 +45,7 @@ class RejectUsernameDialog extends Component {
const next = () => this.setState({stage: stage + 1});
const suspend = async () => {
try {
await rejectUsername({id: user.id, message: this.state.email});
await rejectUsername(user.id);
this.props.handleClose();
} catch (err) {
@@ -70,7 +70,7 @@ class RejectUsernameDialog extends Component {
const {stage} = this.state;
return <Dialog
className={cn(styles.suspendDialog, 'talk-reject-username-dialog')}
className={cn(styles.suspendDialog, 'talk-admin-reject-username-dialog')}
id="rejectUsernameDialog"
open={open}
onClose={handleClose}
@@ -79,28 +79,31 @@ class RejectUsernameDialog extends Component {
<div className={styles.title}>
{t(stages[stage].title, t('reject_username.username'))}
</div>
<div className={styles.container}>
<div className={cn(styles.container, `talk-admin-reject-username-dialog-step-${stage}`)}>
<div className={styles.description}>
{t(stages[stage].description, t('reject_username.username'))}
</div>
{
{/* {
// Suspension Message: This functionality it's not entirely done on the BE - It will be released soon.
stage === 1 &&
<div className={styles.writeContainer}>
<div className={styles.emailMessage}>{t('reject_username.write_message')}</div>
<div className={styles.emailContainer}>
<textarea
rows={5}
className={cn(styles.emailInput, 'talk-reject-username-dialog-suspension-message')}
className={cn(styles.emailInput, 'talk-admin-reject-username-dialog-suspension-message')}
value={this.state.email}
onChange={this.onEmailChange}/>
</div>
</div>
}
<div className={cn(styles.modalButtons, 'talk-reject-username-dialog-buttons')}>
} */}
<div className={cn(styles.modalButtons, 'talk-admin-reject-username-dialog-buttons')}>
{Object.keys(stages[stage].options).map((key, i) => (
<Button
key={i}
className={cn('talk-reject-username-dialog-button', `talk-reject-username-dialog-button-${key}`)}
className={cn('talk-admin-username-dialog-button', `talk-admin-reject-username-dialog-button-${key}`)}
onClick={this.onActionClick(stage, i)} >
{t(stages[stage].options[key], t('reject_username.username'))}
</Button>
@@ -1,98 +0,0 @@
import React from 'react';
import styles from './Table.css';
import t from 'coral-framework/services/i18n';
import PropTypes from 'prop-types';
import {Paginate, Dropdown, Option} from 'coral-ui';
import cn from 'classnames';
const headers = [
{
title: t('community.username_and_email'),
field: 'username'
},
{
title: t('community.account_creation_date'),
field: 'created_at'
},
{
title: t('community.status'),
field: 'status'
},
{
title: t('community.newsroom_role'),
field: 'role'
}
];
const Table = ({users, setRole, onHeaderClickHandler, setCommenterStatus, viewUserDetail, pageCount, page, onPageChange}) => (
<div>
<table className={`mdl-data-table ${styles.dataTable}`}>
<thead>
<tr>
{headers.map((header, i) =>(
<th
key={i}
className={cn('mdl-data-table__cell--non-numeric', styles.header)}
scope="col"
onClick={() => onHeaderClickHandler({field: header.field})}>
{header.title}
</th>
))}
</tr>
</thead>
<tbody>
{users.map((row, i)=> (
<tr key={i} className="talk-admin-community-people-row">
<td className="mdl-data-table__cell--non-numeric">
<button onClick={() => {viewUserDetail(row.id);}} className={cn(styles.username, styles.button)}>{row.username}</button>
<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">
<Dropdown
containerClassName="talk-admin-community-people-dd-status"
value={row.status}
placeholder={t('community.status')}
onChange={(status) => setCommenterStatus(row.id, status)}>
<Option value={'ACTIVE'} label={t('community.active')} />
<Option value={'BANNED'} label={t('community.banned')} />
</Dropdown>
</td>
<td className="mdl-data-table__cell--non-numeric">
<Dropdown
containerClassName="talk-admin-community-people-dd-role"
value={row.roles[0] || ''}
placeholder={t('community.role')}
onChange={(role) => setRole(row.id, role)}>
<Option value={''} label={t('community.none')} />
<Option value={'STAFF'} label={t('community.staff')} />
<Option value={'MODERATOR'} label={t('community.moderator')} />
<Option value={'ADMIN'} label={t('community.admin')} />
</Dropdown>
</td>
</tr>
))}
</tbody>
</table>
<Paginate
pageCount={pageCount}
page={page - 1}
onPageChange={onPageChange}
/>
</div>
);
Table.propTypes = {
users: PropTypes.array,
onHeaderClickHandler: PropTypes.func.isRequired,
setRole: PropTypes.func.isRequired,
setCommenterStatus: PropTypes.func.isRequired,
viewUserDetail: PropTypes.func.isRequired,
pageCount: PropTypes.number.isRequired,
page: PropTypes.number.isRequired,
onPageChange: PropTypes.func.isRequired,
};
export default Table;
@@ -240,3 +240,6 @@
}
}
/**
** This stylesheet is deprecated. DO NOT ADD CODE.
**/
@@ -3,9 +3,10 @@ import {bindActionCreators} from 'redux';
import {compose, gql} from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import {getDefinitionName} from 'coral-framework/utils';
import {withSetUserStatus, withRejectUsername} from 'coral-framework/graphql/mutations';
import {withRejectUsername} from 'coral-framework/graphql/mutations';
import FlaggedAccounts from '../containers/FlaggedAccounts';
import FlaggedUser from '../containers/FlaggedUser';
import People from '../containers/People';
import {hideRejectUsernameDialog} from '../../../actions/community';
import Community from '../components/Community';
@@ -20,17 +21,25 @@ const mapDispatchToProps = (dispatch) =>
const withData = withQuery(gql`
query TalkAdmin_Community {
flaggedUsernamesCount: userCount(query: {
action_type: FLAG,
statuses: [PENDING]
})
flaggedUsernamesCount: userCount(
query:{
action_type: FLAG,
state: {
status: {
username: [SET, CHANGED]
}
}
}
)
...${getDefinitionName(FlaggedAccounts.fragments.root)}
...${getDefinitionName(FlaggedUser.fragments.root)}
...${getDefinitionName(People.fragments.root)}
me {
...${getDefinitionName(FlaggedUser.fragments.me)}
__typename
}
}
${People.fragments.root}
${FlaggedAccounts.fragments.root}
${FlaggedUser.fragments.root}
${FlaggedUser.fragments.me}
@@ -42,7 +51,6 @@ const withData = withQuery(gql`
export default compose(
connect(mapStateToProps, mapDispatchToProps),
withSetUserStatus,
withRejectUsername,
withData
)(Community);
@@ -5,10 +5,7 @@ import {compose, gql} from 'react-apollo';
import {withFragments} from 'plugin-api/beta/client/hocs';
import {Spinner} from 'coral-ui';
import PropTypes from 'prop-types';
import {withSetUserStatus} from 'coral-framework/graphql/mutations';
import {showBanUserDialog} from 'actions/banUserDialog';
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
import {withApproveUsername} from 'coral-framework/graphql/mutations';
import {showRejectUsernameDialog} from '../../../actions/community';
import {viewUserDetail} from '../../../actions/userDetail';
import {getDefinitionName} from 'coral-framework/utils';
@@ -24,11 +21,8 @@ class FlaggedAccountsContainer extends Component {
super(props);
}
approveUser = ({userId}) => {
return this.props.setUserStatus({
userId,
status: 'APPROVED'
});
approveUser = ({userId: id}) => {
return this.props.approveUsername(id);
}
loadMore = () => {
@@ -36,16 +30,16 @@ class FlaggedAccountsContainer extends Component {
query: LOAD_MORE_QUERY,
variables: {
limit: 5,
cursor: this.props.root.users.endCursor,
cursor: this.props.root.flaggedUsers.endCursor,
},
updateQuery: (previous, {fetchMoreResult:{users}}) => {
updateQuery: (previous, {fetchMoreResult:{flaggedUsers}}) => {
const updated = update(previous, {
users: {
flaggedUsers: {
nodes: {
$apply: (nodes) => appendNewNodes(nodes, users.nodes),
$apply: (nodes) => appendNewNodes(nodes, flaggedUsers.nodes),
},
hasNextPage: {$set: users.hasNextPage},
endCursor: {$set: users.endCursor},
hasNextPage: {$set: flaggedUsers.hasNextPage},
endCursor: {$set: flaggedUsers.endCursor},
},
});
return updated;
@@ -61,17 +55,16 @@ class FlaggedAccountsContainer extends Component {
if (this.props.data.loading) {
return <div><Spinner/></div>;
}
return (
<FlaggedAccounts
showBanUserDialog={this.props.showBanUserDialog}
showSuspendUserDialog={this.props.showSuspendUserDialog}
showRejectUsernameDialog={this.props.showRejectUsernameDialog}
viewUserDetail={this.props.viewUserDetail}
approveUser={this.approveUser}
loadMore={this.loadMore}
data={this.props.data}
root={this.props.root}
users={this.props.root.users}
users={this.props.root.flaggedUsers}
me={this.props.root.me}
/>
);
@@ -79,18 +72,25 @@ class FlaggedAccountsContainer extends Component {
}
FlaggedAccountsContainer.propTypes = {
showBanUserDialog: PropTypes.func,
showSuspendUserDialog: PropTypes.func,
showRejectUsernameDialog: PropTypes.func,
viewUserDetail: PropTypes.func,
setUserStatus: PropTypes.func,
approveUsername: PropTypes.func,
data: PropTypes.object,
root: PropTypes.object
root: PropTypes.object,
};
const LOAD_MORE_QUERY = gql`
query TalkAdmin_LoadMoreFlaggedAccounts($limit: Int, $cursor: Cursor) {
users(query:{action_type: FLAG, statuses: [PENDING], limit: $limit, cursor: $cursor}){
flaggedUsers: users(query:{
action_type: FLAG,
state: {
status: {
username: [SET, CHANGED]
}
},
limit: $limit,
cursor: $cursor
}){
hasNextPage
endCursor
nodes {
@@ -104,19 +104,25 @@ const LOAD_MORE_QUERY = gql`
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
showBanUserDialog,
showSuspendUserDialog,
showRejectUsernameDialog,
viewUserDetail,
}, dispatch);
export default compose(
connect(null, mapDispatchToProps),
withSetUserStatus,
withApproveUsername,
withFragments({
root: gql`
fragment TalkAdminCommunity_FlaggedAccounts_root on RootQuery {
users(query:{action_type: FLAG, statuses: [PENDING], limit: 10}){
flaggedUsers: users(query:{
action_type: FLAG,
state: {
status: {
username: [SET, CHANGED]
}
}
limit: 10
}){
hasNextPage
endCursor
nodes {
@@ -17,8 +17,20 @@ export default withFragments({
fragment TalkAdminCommunity_FlaggedUser_user on User {
id
username
status
roles
state {
status {
username {
status
}
banned {
status
}
suspension {
until
}
}
}
role
actions{
id
created_at
@@ -1,104 +1,230 @@
import React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose, gql} from 'react-apollo';
import People from '../components/People';
import PropTypes from 'prop-types';
import {withFragments} from 'plugin-api/beta/client/hocs';
import {withUnbanUser, withUnsuspendUser, withSetUserRole} from 'coral-framework/graphql/mutations';
import {showBanUserDialog} from 'actions/banUserDialog';
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
import {viewUserDetail} from '../../../actions/userDetail';
import {
fetchUsers,
updateSorting,
setPage,
hideRejectUsernameDialog,
setCommenterStatus,
setRole,
setSearchValue,
} from '../../../actions/community';
import {appendNewNodes} from 'plugin-api/beta/client/utils';
import update from 'immutability-helper';
import {Spinner} from 'coral-ui';
class PeopleContainer extends React.Component {
timer=null;
timer = null;
fetchUsers = (query = {}) => {
const {community} = this.props;
state = {
searchValue: ''
};
this.props.fetchUsers({
value: community.searchValue,
field: community.fieldPeople,
asc: community.ascPeople,
...query
onSearchChange = (e) => {
const {value} = e.target;
this.setState({searchValue: value}, () => {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.search(value);
}, 350);
});
}
componentWillMount() {
this.fetchUsers();
search = async (value) => {
return this.props.data.fetchMore({
query: SEARCH_QUERY,
variables: {
value,
limit: 5,
},
updateQuery: (previous, {fetchMoreResult:{users}}) => {
const updated = update(previous, {
users: {
nodes: {
$set: users.nodes,
},
hasNextPage: {$set: users.hasNextPage},
endCursor: {$set: users.endCursor},
},
});
return updated;
},
});
};
setUserRole = async (id, role) => {
await this.props.setUserRole(id, role);
}
onKeyDownHandler = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.fetchUsers();
}
}
onSearchChange = (e) => {
const value = e.target.value;
this.props.setSearchValue(value);
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.fetchUsers({value});
}, 350);
}
onHeaderClickHandler = (sort) => {
this.props.updateSorting(sort);
this.fetchUsers();
}
onPageChange = ({selected}) => {
const page = selected + 1;
this.props.setPage(page);
this.fetchUsers({page});
}
loadMore = () => {
return this.props.data.fetchMore({
query: LOAD_MORE_QUERY,
variables: {
value: this.state.searchValue,
limit: 5,
cursor: this.props.root.users.endCursor,
},
updateQuery: (previous, {fetchMoreResult:{users}}) => {
const updated = update(previous, {
users: {
nodes: {
$apply: (nodes) => appendNewNodes(nodes, users.nodes),
},
hasNextPage: {$set: users.hasNextPage},
endCursor: {$set: users.endCursor},
},
});
return updated;
},
});
};
render() {
if (this.props.data.error) {
return <div>{this.props.data.error.message}</div>;
}
if (this.props.data.loading) {
return <div><Spinner/></div>;
}
return <People
users={this.props.community.users}
searchValue={this.props.community.searchValue}
onSearchChange={this.onSearchChange}
onHeaderClickHandler={this.onHeaderClickHandler}
onPageChange={this.onPageChange}
totalPages={this.props.community.totalPagesPeople}
setCommenterStatus={this.props.setCommenterStatus}
setRole={this.props.setRole}
page={this.props.community.pagePeople}
onSearchChange={this.onSearchChange}
viewUserDetail={this.props.viewUserDetail}
setUserRole={this.setUserRole}
showSuspendUserDialog={this.props.showSuspendUserDialog}
showBanUserDialog={this.props.showBanUserDialog}
unbanUser={this.props.unbanUser}
unsuspendUser={this.props.unsuspendUser}
data={this.props.data}
root={this.props.root}
users={this.props.root.users}
loadMore={this.loadMore}
/>;
}
}
PeopleContainer.propTypes = {
setPage: PropTypes.func,
fetchUsers: PropTypes.func,
updateSorting: PropTypes.func,
setRole: PropTypes.func.isRequired,
setCommenterStatus: PropTypes.func.isRequired,
setSearchValue: PropTypes.func.isRequired,
viewUserDetail: PropTypes.func.isRequired,
community: PropTypes.object,
setUserRole: PropTypes.func.isRequired,
unbanUser: PropTypes.func.isRequired,
unsuspendUser: PropTypes.func.isRequired,
viewUserDetail: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func,
showBanUserDialog: PropTypes.func,
data: PropTypes.object,
root: PropTypes.object,
};
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
setPage,
fetchUsers,
updateSorting,
hideRejectUsernameDialog,
setCommenterStatus,
setRole,
viewUserDetail,
setSearchValue,
showSuspendUserDialog,
showBanUserDialog,
}, dispatch);
export default connect(null, mapDispatchToProps)(PeopleContainer);
const LOAD_MORE_QUERY = gql`
query TalkAdminCommunity_People_LoadMoreUsers($limit: Int, $cursor: Cursor, $value: String) {
users(query: {
value: $value,
limit: $limit,
cursor: $cursor
}){
hasNextPage
endCursor
nodes {
__typename
id
username
role
created_at
profiles {
id
provider
}
state {
status {
banned {
status
}
suspension {
until
}
}
}
}
}
}
`;
const SEARCH_QUERY = gql`
query TalkAdminCommunity_People_SearchUsers($value: String, $limit: Int) {
users(query: {
value: $value,
limit: $limit,
}){
hasNextPage
endCursor
nodes {
__typename
id
username
role
created_at
profiles {
id
provider
}
state {
status {
banned {
status
}
suspension {
until
}
}
}
}
}
}
`;
export default compose(
connect(null, mapDispatchToProps),
withSetUserRole,
withUnsuspendUser,
withUnbanUser,
withFragments({
root: gql`
fragment TalkAdminCommunity_People_root on RootQuery {
users(query: {}){
hasNextPage
endCursor
nodes {
__typename
id
username
role
created_at
profiles {
id
provider
}
state {
status {
banned {
status
}
suspension {
until
}
}
}
}
}
}
`,
}),
)(PeopleContainer);
@@ -5,6 +5,7 @@ import styles from './EmbedLink.css';
import {Button} from 'coral-ui';
import {BASE_URL} from 'coral-framework/constants/url';
import ConfigureCard from 'coral-framework/components/ConfigureCard';
import {stripIndent} from 'common-tags';
class EmbedLink extends Component {
@@ -23,17 +24,16 @@ class EmbedLink extends Component {
}
render () {
const coralJsUrl = join(BASE_URL, '/embed.js');
const nonce = String(Math.random()).slice(2);
const streamElementId = `coral_talk_${nonce}`;
const embedText = `
<div id="${streamElementId}"></div>
<script src="${coralJsUrl}" async onload="
Coral.Talk.render(document.getElementById('${streamElementId}'), {
talk: '${BASE_URL}'
});
"></script>
`.trim();
const coralJsUrl = join(BASE_URL, '/static/embed.js');
const embedText = stripIndent`
<div id="coral_talk_stream"></div>
<script src="${coralJsUrl}" async onload="
Coral.Talk.render(document.getElementById('coral_talk_stream'), {
talk: '${BASE_URL}'
});
"></script>
`;
return (
<ConfigureCard title={'Embed Comment Stream'}>
<p>{t('configure.copy_and_paste')}</p>
@@ -8,8 +8,6 @@ import styles from './Comment.css';
import CommentLabels from 'coral-admin/src/components/CommentLabels';
import CommentAnimatedEdit from 'coral-admin/src/components/CommentAnimatedEdit';
import Slot from 'coral-framework/components/Slot';
import ActionsMenu from 'coral-admin/src/components/ActionsMenu';
import ActionsMenuItem from 'coral-admin/src/components/ActionsMenuItem';
import CommentBodyHighlighter from 'coral-admin/src/components/CommentBodyHighlighter';
import IfHasLink from 'coral-admin/src/components/IfHasLink';
import cn from 'classnames';
@@ -19,10 +17,9 @@ import RejectButton from 'coral-admin/src/components/RejectButton';
import t, {timeago} from 'coral-framework/services/i18n';
class Comment extends React.Component {
ref = null;
handleRef = (ref) => this.ref = ref;
handleRef = (ref) => (this.ref = ref);
handleFocusOrClick = () => {
if (!this.props.selected) {
@@ -30,40 +27,20 @@ class Comment extends React.Component {
}
};
showSuspendUserDialog = () => {
const {comment, showSuspendUserDialog} = this.props;
return showSuspendUserDialog({
userId: comment.user.id,
username: comment.user.username,
commentId: comment.id,
commentStatus: comment.status,
});
};
showBanUserDialog = () => {
const {comment, showBanUserDialog} = this.props;
return showBanUserDialog({
userId: comment.user.id,
username: comment.user.username,
commentId: comment.id,
commentStatus: comment.status,
});
};
viewUserDetail = () => {
const {viewUserDetail, comment} = this.props;
return viewUserDetail(comment.user.id);
};
approve = () => (this.props.comment.status === 'ACCEPTED'
? null
: this.props.acceptComment({commentId: this.props.comment.id})
);
approve = () =>
this.props.comment.status === 'ACCEPTED'
? null
: this.props.acceptComment({commentId: this.props.comment.id});
reject = () => (this.props.comment.status === 'REJECTED'
? null
: this.props.rejectComment({commentId: this.props.comment.id})
);
reject = () =>
this.props.comment.status === 'REJECTED'
? null
: this.props.rejectComment({commentId: this.props.comment.id});
componentDidUpdate(prev) {
if (!prev.selected && this.props.selected) {
@@ -79,7 +56,6 @@ class Comment extends React.Component {
data,
root,
root: {settings},
currentUserId,
currentAsset,
clearHeightCache,
dangling,
@@ -91,7 +67,14 @@ class Comment extends React.Component {
return (
<li
tabIndex={0}
className={cn(className, 'mdl-card', selectionStateCSS, styles.root, {[styles.selected]: selected, [styles.dangling]: dangling}, 'talk-admin-moderate-comment')}
className={cn(
className,
'mdl-card',
selectionStateCSS,
styles.root,
{[styles.selected]: selected, [styles.dangling]: dangling},
'talk-admin-moderate-comment'
)}
id={`comment_${comment.id}`}
onClick={this.handleFocusOrClick}
ref={this.handleRef}
@@ -100,38 +83,28 @@ class Comment extends React.Component {
<div className={styles.container}>
<div className={styles.itemHeader}>
<div className={styles.author}>
{
(
<span className={styles.username} onClick={this.viewUserDetail}>
{comment.user.username}
</span>
)
}
<span
className={cn(
styles.username,
'talk-admin-moderate-comment-username'
)}
onClick={this.viewUserDetail}
>
{comment.user.username}
</span>
<span className={styles.created}>
{timeago(comment.created_at)}
</span>
{
(comment.editing && comment.editing.edited)
? <span>&nbsp;<span className={styles.editedMarker}>({t('comment.edited')})</span></span>
: null
}
{currentUserId !== comment.user.id &&
<ActionsMenu icon="not_interested" className="talk-admin-moderate-comment-actions-menu">
<ActionsMenuItem
disabled={comment.user.status === 'BANNED'}
onClick={this.showSuspendUserDialog}>
Suspend User</ActionsMenuItem>
<ActionsMenuItem
disabled={comment.user.status === 'BANNED'}
onClick={this.showBanUserDialog}>
Ban User
</ActionsMenuItem>
</ActionsMenu>
}
{comment.editing && comment.editing.edited ? (
<span>
&nbsp;<span className={styles.editedMarker}>
({t('comment.edited')})
</span>
</span>
) : null}
<div className={styles.adminCommentInfoBar}>
<CommentLabels
comment={comment}
/>
<CommentLabels comment={comment} />
<Slot
fill="adminCommentInfoBar"
data={data}
@@ -144,8 +117,11 @@ class Comment extends React.Component {
<div className={styles.moderateArticle}>
Story: {comment.asset.title}
{!currentAsset &&
<Link to={`/admin/moderate/${comment.asset.id}`}>{t('modqueue.moderate')}</Link>}
{!currentAsset && (
<Link to={`/admin/moderate/${comment.asset.id}`}>
{t('modqueue.moderate')}
</Link>
)}
</div>
<CommentAnimatedEdit body={comment.body}>
<div className={styles.itemBody}>
@@ -154,8 +130,7 @@ class Comment extends React.Component {
suspectWords={settings.wordlist.suspect}
bannedWords={settings.wordlist.banned}
body={comment.body}
/>
{' '}
/>{' '}
<a
className={styles.external}
href={`${comment.asset.url}?commentId=${comment.id}`}
@@ -216,8 +191,6 @@ Comment.propTypes = {
className: PropTypes.string,
dangling: PropTypes.bool,
currentAsset: PropTypes.object,
showBanUserDialog: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func.isRequired,
currentUserId: PropTypes.string.isRequired,
clearHeightCache: PropTypes.func,
comment: PropTypes.shape({
@@ -229,12 +202,12 @@ Comment.propTypes = {
created_at: PropTypes.string.isRequired,
user: PropTypes.shape({
id: PropTypes.string,
status: PropTypes.string
status: PropTypes.string,
}).isRequired,
asset: PropTypes.shape({
title: PropTypes.string,
url: PropTypes.string,
id: PropTypes.string
id: PropTypes.string,
}),
}),
data: PropTypes.object.isRequired,
@@ -12,7 +12,6 @@ import Slot from 'coral-framework/components/Slot';
import ViewOptions from './ViewOptions';
class Moderation extends Component {
state = {
isLoadingMore: false,
};
@@ -27,13 +26,14 @@ class Moderation extends Component {
key('t', () => this.nextQueue());
key('f', () => this.moderate(false));
key('d', () => this.moderate(true));
this.getMenuItems()
.forEach((menuItem, idx) => key(`${idx + 1}`, () => this.selectQueue(menuItem)));
this.getMenuItems().forEach((menuItem, idx) =>
key(`${idx + 1}`, () => this.selectQueue(menuItem))
);
}
onClose = () => {
this.props.toggleModal(false);
}
};
nextQueue = () => {
const activeTab = this.props.activeTab;
@@ -41,38 +41,45 @@ class Moderation extends Component {
const menuItems = this.getMenuItems();
const activeTabIndex = menuItems.findIndex((item) => item === activeTab);
const nextQueueIndex = (activeTabIndex === menuItems.length - 1) ? 0 : activeTabIndex + 1;
const nextQueueIndex =
activeTabIndex === menuItems.length - 1 ? 0 : activeTabIndex + 1;
this.selectQueue(menuItems[nextQueueIndex]);
}
};
selectQueue = (key) => {
const assetId = this.props.data.variables.asset_id;
this.props.router.push(this.props.getModPath(key, assetId));
}
};
getMenuItems = () => Object.keys(this.props.queueConfig);
closeSearch = () => {
const {toggleStorySearch} = this.props;
toggleStorySearch(false);
}
};
openSearch = () => {
this.props.toggleStorySearch(true);
}
};
getActiveTabCount = (props = this.props) => {
return props.root[`${props.activeTab}Count`];
}
};
moderate = (accept) => {
const {acceptComment, rejectComment, moderation: {selectedCommentId}} = this.props;
const {
acceptComment,
rejectComment,
moderation: {selectedCommentId},
} = this.props;
// Accept or reject only if there's a selected comment
if(selectedCommentId != null){
if (selectedCommentId != null) {
const comments = this.getComments();
const commentIdx = comments.findIndex((comment) => comment.id === selectedCommentId);
const commentIdx = comments.findIndex(
(comment) => comment.id === selectedCommentId
);
const comment = comments[commentIdx];
if (accept) {
@@ -81,12 +88,12 @@ class Moderation extends Component {
comment.status !== 'REJECTED' && rejectComment({commentId: comment.id});
}
}
}
};
getComments = (props = this.props) => {
const {root, activeTab} = props;
return root[activeTab].nodes;
}
};
loadMore = async () => {
if (!this.state.isLoadingMore) {
@@ -95,14 +102,13 @@ class Moderation extends Component {
const result = await this.props.loadMore(this.props.activeTab);
this.setState({isLoadingMore: false});
return result;
}
catch (e) {
} catch (e) {
this.setState({isLoadingMore: false});
throw e;
}
}
return false;
}
};
componentWillUnmount() {
key.unbind('s');
@@ -112,12 +118,21 @@ class Moderation extends Component {
key.unbind('t');
key.unbind('f');
key.unbind('d');
this.getMenuItems()
.forEach((menuItem, idx) => key.unbind(`${idx + 1}`));
this.getMenuItems().forEach((menuItem, idx) => key.unbind(`${idx + 1}`));
}
render () {
const {root, data, moderation, viewUserDetail, activeTab, getModPath, queueConfig, handleCommentChange, ...props} = this.props;
render() {
const {
root,
data,
moderation,
viewUserDetail,
activeTab,
getModPath,
queueConfig,
handleCommentChange,
...props
} = this.props;
const {asset} = root;
const assetId = asset && asset.id;
@@ -128,7 +143,7 @@ class Moderation extends Component {
key: queue,
name: queueConfig[queue].name,
icon: queueConfig[queue].icon,
count: root[`${queue}Count`]
count: root[`${queue}Count`],
}));
return (
@@ -145,7 +160,9 @@ class Moderation extends Component {
items={menuItems}
activeTab={activeTab}
/>
<div className={cn(styles.container, 'talk-admin-moderation-container')}>
<div
className={cn(styles.container, 'talk-admin-moderation-container')}
>
<ViewOptions
selectSort={this.props.setSortOrder}
sort={this.props.moderation.sortOrder}
@@ -159,9 +176,7 @@ class Moderation extends Component {
hasNextPage={comments.hasNextPage}
activeTab={activeTab}
singleView={moderation.singleView}
selectedCommentId={moderation.selectedCommentId}
showBanUserDialog={props.showBanUserDialog}
showSuspendUserDialog={props.showSuspendUserDialog}
selectedCommentId={this.state.selectedCommentId}
acceptComment={props.acceptComment}
rejectComment={props.rejectComment}
loadMore={this.loadMore}
@@ -192,7 +207,7 @@ class Moderation extends Component {
queryData={{root, asset}}
activeTab={activeTab}
handleCommentChange={handleCommentChange}
fill='adminModeration'
fill="adminModeration"
/>
</div>
);
@@ -213,8 +228,6 @@ Moderation.propTypes = {
commentBelongToQueue: PropTypes.func.isRequired,
handleCommentChange: PropTypes.func.isRequired,
setSortOrder: PropTypes.func.isRequired,
showBanUserDialog: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
acceptComment: PropTypes.func.isRequired,
loadMore: PropTypes.func.isRequired,
@@ -7,7 +7,12 @@ import EmptyCard from '../../../components/EmptyCard';
import AutoLoadMore from './AutoLoadMore';
import ViewMore from './ViewMore';
import t from 'coral-framework/services/i18n';
import {WindowScroller, CellMeasurer, CellMeasurerCache, List} from 'react-virtualized';
import {
WindowScroller,
CellMeasurer,
CellMeasurerCache,
List,
} from 'react-virtualized';
import throttle from 'lodash/throttle';
import key from 'keymaster';
@@ -49,7 +54,6 @@ function invalidateCursor(invalidated, state, props) {
// getVisibileComments returns a list containing comments
// which comes after the `idCursor`.
function getVisibleComments(comments, idCursor) {
if (!comments) {
return [];
}
@@ -97,8 +101,7 @@ class ModerationQueue extends React.Component {
const view = this.state.view;
if (index < view.length) {
return view[index].id;
}
else if (index === view.length) {
} else if (index === view.length) {
return 'loadMore';
}
throw new Error(`unknown index ${index}`);
@@ -133,7 +136,10 @@ class ModerationQueue extends React.Component {
const {comments: nextComments} = next;
// New comments where added and our cursor list is incomplete.
if (this.state.idCursors.length < 2 && nextComments.length > this.state.idCursors.length) {
if (
this.state.idCursors.length < 2 &&
nextComments.length > this.state.idCursors.length
) {
this.setState(resetCursors(this.state, next));
return;
}
@@ -142,17 +148,24 @@ class ModerationQueue extends React.Component {
// Comments have been removed.
if (
prevComments && nextComments &&
prevComments &&
nextComments &&
nextComments.length < prevComments.length
) {
// Invalidate first cursor if referenced comment was removed.
if (this.state.idCursors[0] && !hasComment(nextComments, this.state.idCursors[0])) {
if (
this.state.idCursors[0] &&
!hasComment(nextComments, this.state.idCursors[0])
) {
idCursors = invalidateCursor(0, this.state, next);
}
// Invalidate second cursor if referenced comment was removed.
if (this.state.idCursors[1] && !hasComment(nextComments, this.state.idCursors[1])) {
if (
this.state.idCursors[1] &&
!hasComment(nextComments, this.state.idCursors[1])
) {
idCursors = invalidateCursor(1, this.state, next);
}
@@ -165,10 +178,12 @@ class ModerationQueue extends React.Component {
let nextSelectedCommentId = null;
// Determine a comment to select.
const prevIndex = view.findIndex((comment) => comment.id === this.props.selectedCommentId);
const prevIndex = view.findIndex(
(comment) => comment.id === this.props.selectedCommentId
);
if (prevIndex !== view.length - 1) {
nextSelectedCommentId = view[prevIndex + 1].id;
} else if(prevIndex > 0) {
} else if (prevIndex > 0) {
nextSelectedCommentId = view[prevIndex - 1].id;
}
this.props.selectCommentId(nextSelectedCommentId);
@@ -182,12 +197,14 @@ class ModerationQueue extends React.Component {
// TODO: removing or adding a comment from the list seems to render incorrect, is this a bug?
// Find first changed comment and perform a reflow.
const index = this.state.view.findIndex((comment, i) => !nextView[i] || nextView[i].id !== comment.id);
const index = this.state.view.findIndex(
(comment, i) => !nextView[i] || nextView[i].id !== comment.id
);
this.reflowList(index);
}
}
componentDidUpdate (prev) {
componentDidUpdate(prev) {
const {commentCount, selectedCommentId} = this.props;
const switchedToMultiMode = prev.singleView && !this.props.singleView;
@@ -213,13 +230,18 @@ class ModerationQueue extends React.Component {
// Returns comment counts without dangling comments.
getCommentCountWithoutDagling(props = this.props) {
return props.comments.filter((comment) => props.commentBelongToQueue(props.activeTab, comment)).length;
return props.comments.filter((comment) =>
props.commentBelongToQueue(props.activeTab, comment)
).length;
}
async selectDown() {
const view = this.state.view;
const index = view.findIndex(({id}) => id === this.props.selectedCommentId);
if (index === view.length - 1 && this.getCommentCountWithoutDagling() !== this.props.commentCount) {
if (
index === view.length - 1 &&
this.getCommentCountWithoutDagling() !== this.props.commentCount
) {
await this.props.loadMore();
this.selectDown();
return;
@@ -268,17 +290,16 @@ class ModerationQueue extends React.Component {
if (index >= 0) {
cache.clear(index);
this.listRef && this.listRef.recomputeRowHeights(index);
}
else {
} else {
cache.clearAll();
this.listRef && this.listRef.recomputeRowHeights();
}
}, 500);
rowRenderer = ({
index, // Index of row within collection
index, // Index of row within collection
parent,
style // Style object to be applied to row (to position it)
style, // Style object to be applied to row (to position it)
}) => {
const view = this.state.view;
const rowCount = view.length + 1;
@@ -291,43 +312,41 @@ class ModerationQueue extends React.Component {
if (index === rowCount - 1) {
key = 'end-of-comment-list';
child = (
<div
style={style}
id={'end-of-comment-list'}
>
{this.props.hasNextPage && <AutoLoadMore
loadMore={this.props.loadMore}
loading={this.props.isLoadingMore}
/>}
<div style={style} id={'end-of-comment-list'}>
{this.props.hasNextPage && (
<AutoLoadMore
loadMore={this.props.loadMore}
loading={this.props.isLoadingMore}
/>
)}
</div>
);
}
else {
} else {
const comment = view[index];
// Use callback cache so not to change the identity of these arrow functions.
// Otherwise shallow compare will fail to optimize.
if (!this.callbackCaches.clearHeightCache[index]) {
this.callbackCaches.clearHeightCache[index] = () => this.reflowList(index);
this.callbackCaches.clearHeightCache[index] = () =>
this.reflowList(index);
}
if (!this.callbackCaches.selectCommentId[comment.id]) {
this.callbackCaches.selectCommentId[comment.id] = () => this.props.selectCommentId(comment.id);
this.callbackCaches.selectCommentId[comment.id] = () =>
this.props.selectCommentId(comment.id);
}
key = comment.id;
child = (
<div
style={style}
>
<div style={style}>
<Comment
data={this.props.data}
root={this.props.root}
comment={comment}
dangling={!this.props.commentBelongToQueue(this.props.activeTab, comment)}
dangling={
!this.props.commentBelongToQueue(this.props.activeTab, comment)
}
selected={comment.id === this.props.selectedCommentId}
viewUserDetail={this.props.viewUserDetail}
showBanUserDialog={this.props.showBanUserDialog}
showSuspendUserDialog={this.props.showSuspendUserDialog}
acceptComment={this.props.acceptComment}
rejectComment={this.props.rejectComment}
currentAsset={this.props.currentAsset}
@@ -352,7 +371,7 @@ class ModerationQueue extends React.Component {
);
};
render () {
render() {
const {
comments,
selectedCommentId,
@@ -370,7 +389,9 @@ class ModerationQueue extends React.Component {
}
if (singleView) {
const index = comments.findIndex((comment) => comment.id === selectedCommentId);
const index = comments.findIndex(
(comment) => comment.id === selectedCommentId
);
const comment = comments[index];
return (
<div className={styles.root}>
@@ -381,8 +402,6 @@ class ModerationQueue extends React.Component {
comment={comment}
selected={true}
viewUserDetail={viewUserDetail}
showBanUserDialog={props.showBanUserDialog}
showSuspendUserDialog={props.showSuspendUserDialog}
acceptComment={props.acceptComment}
rejectComment={props.rejectComment}
currentAsset={props.currentAsset}
@@ -428,18 +447,16 @@ class ModerationQueue extends React.Component {
}
ModerationQueue.propTypes = {
selectCommentId: PropTypes.func.isRequired,
selectedCommentId: PropTypes.string,
viewUserDetail: PropTypes.func.isRequired,
currentAsset: PropTypes.object,
showBanUserDialog: PropTypes.func.isRequired,
selectCommentId: PropTypes.func.isRequired,
showSuspendUserDialog: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
acceptComment: PropTypes.func.isRequired,
commentBelongToQueue: PropTypes.func.isRequired,
cleanUpQueue: PropTypes.func.isRequired,
commentCount: PropTypes.number.isRequired,
loadMore: PropTypes.func.isRequired,
selectedCommentId: PropTypes.string,
singleView: PropTypes.bool,
isLoadingMore: PropTypes.bool,
hasNextPage: PropTypes.bool,
@@ -9,7 +9,7 @@
height: 144px;
min-height: auto;
margin-top: 16px;
z-index: 10;
z-index: 4;
@media (--tablet) {
width: 650px;
@@ -37,7 +37,6 @@ export default withFragments({
user {
id
username
status
}
asset {
id
@@ -11,10 +11,12 @@ import NotFoundAsset from '../components/NotFoundAsset';
import {isPremod, getModPath} from '../../../utils';
import {withSetCommentStatus} from 'coral-framework/graphql/mutations';
import {handleCommentChange, commentBelongToQueue, cleanUpQueue} from '../graphql';
import {
handleCommentChange,
commentBelongToQueue,
cleanUpQueue,
} from '../graphql';
import {showBanUserDialog} from 'actions/banUserDialog';
import {showSuspendUserDialog} from 'actions/suspendUserDialog';
import {viewUserDetail} from '../../../actions/userDetail';
import {
toggleModal,
@@ -67,13 +69,15 @@ class ModerationContainer extends Component {
};
get activeTab() {
const {root: {asset, settings}} = this.props;
const id = getAssetId(this.props);
const tab = getTab(this.props);
// Grab premod from asset or from settings if it's defined.
const setting = id && asset && asset.settings ? asset.settings.moderation : settings.moderation;
const setting =
id && asset && asset.settings
? asset.settings.moderation
: settings.moderation;
const queue = isPremod(setting) ? 'premod' : 'new';
const activeTab = tab ? tab : queue;
@@ -86,60 +90,101 @@ class ModerationContainer extends Component {
{
document: COMMENT_ADDED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentAdded: comment}}}) => {
updateQuery: (
prev,
{subscriptionData: {data: {commentAdded: comment}}}
) => {
return this.handleCommentChange(prev, comment);
},
},
{
document: COMMENT_ACCEPTED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentAccepted: comment}}}) => {
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
const notifyText = this.props.auth.user.id === user.id
? ''
: t('modqueue.notify_accepted', user.username, prepareNotificationText(comment.body));
updateQuery: (
prev,
{subscriptionData: {data: {commentAccepted: comment}}}
) => {
const user =
comment.status_history[comment.status_history.length - 1]
.assigned_by;
const notifyText =
this.props.auth.user.id === user.id
? ''
: t(
'modqueue.notify_accepted',
user.username,
prepareNotificationText(comment.body)
);
return this.handleCommentChange(prev, comment, notifyText);
},
},
{
document: COMMENT_REJECTED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentRejected: comment}}}) => {
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
const notifyText = this.props.auth.user.id === user.id
? ''
: t('modqueue.notify_rejected', user.username, prepareNotificationText(comment.body));
updateQuery: (
prev,
{subscriptionData: {data: {commentRejected: comment}}}
) => {
const user =
comment.status_history[comment.status_history.length - 1]
.assigned_by;
const notifyText =
this.props.auth.user.id === user.id
? ''
: t(
'modqueue.notify_rejected',
user.username,
prepareNotificationText(comment.body)
);
return this.handleCommentChange(prev, comment, notifyText);
},
},
{
document: COMMENT_RESET_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentReset: comment}}}) => {
const user = comment.status_history[comment.status_history.length - 1].assigned_by;
const notifyText = this.props.auth.user.id === user.id
? ''
: t('modqueue.notify_reset', user.username, prepareNotificationText(comment.body));
updateQuery: (
prev,
{subscriptionData: {data: {commentReset: comment}}}
) => {
const user =
comment.status_history[comment.status_history.length - 1]
.assigned_by;
const notifyText =
this.props.auth.user.id === user.id
? ''
: t(
'modqueue.notify_reset',
user.username,
prepareNotificationText(comment.body)
);
return this.handleCommentChange(prev, comment, notifyText);
},
},
{
document: COMMENT_EDITED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentEdited: comment}}}) => {
updateQuery: (
prev,
{subscriptionData: {data: {commentEdited: comment}}}
) => {
return this.handleCommentChange(prev, comment);
},
},
{
document: COMMENT_FLAGGED_SUBSCRIPTION,
variables,
updateQuery: (prev, {subscriptionData: {data: {commentFlagged: comment}}}) => {
updateQuery: (
prev,
{subscriptionData: {data: {commentFlagged: comment}}}
) => {
return this.handleCommentChange(prev, comment);
},
},
];
this.subscriptions = parameters.map((param) => this.props.data.subscribeToMoreThrottled(param));
this.subscriptions = parameters.map((param) =>
this.props.data.subscribeToMoreThrottled(param)
);
}
unsubscribe() {
@@ -164,7 +209,9 @@ class ModerationContainer extends Component {
componentWillReceiveProps(nextProps) {
// Resubscribe when we change between assets.
if(this.props.data.variables.asset_id !== nextProps.data.variables.asset_id) {
if (
this.props.data.variables.asset_id !== nextProps.data.variables.asset_id
) {
this.resubscribe(nextProps.data.variables);
}
}
@@ -172,22 +219,27 @@ class ModerationContainer extends Component {
cleanUpQueue = (queue) => {
if (!this.props.data.loading) {
this.props.data.updateQuery((query) => {
return cleanUpQueue(query, queue, this.props.moderation.sortOrder, this.props.queueConfig);
return cleanUpQueue(
query,
queue,
this.props.moderation.sortOrder,
this.props.queueConfig
);
});
}
}
};
acceptComment = ({commentId}) => {
return this.props.setCommentStatus({commentId, status: 'ACCEPTED'});
}
};
rejectComment = ({commentId}) => {
return this.props.setCommentStatus({commentId, status: 'REJECTED'});
}
};
commentBelongToQueue = (queue, comment) => {
return commentBelongToQueue(queue, comment, this.props.queueConfig);
}
};
loadMore = (tab) => {
const variables = {
@@ -202,7 +254,7 @@ class ModerationContainer extends Component {
return this.props.data.fetchMore({
query: LOAD_MORE_QUERY,
variables,
updateQuery: (prev, {fetchMoreResult:{comments}}) => {
updateQuery: (prev, {fetchMoreResult: {comments}}) => {
return update(prev, {
[tab]: {
nodes: {$push: comments.nodes},
@@ -210,11 +262,11 @@ class ModerationContainer extends Component {
endCursor: {$set: comments.endCursor},
},
});
}
},
});
};
render () {
render() {
const {root, root: {asset, settings}, data} = this.props;
const assetId = getAssetId(this.props);
@@ -230,14 +282,15 @@ class ModerationContainer extends Component {
}
}
if(data.loading) {
if (data.loading) {
// loading.
return <Spinner />;
}
const premodEnabled = assetId ? isPremod(asset.settings.moderation) :
isPremod(settings.moderation);
const premodEnabled = assetId
? isPremod(asset.settings.moderation)
: isPremod(settings.moderation);
const currentQueueConfig = Object.assign({}, this.props.queueConfig);
@@ -249,19 +302,21 @@ class ModerationContainer extends Component {
delete currentQueueConfig.premod;
}
return <Moderation
{...this.props}
getModPath={getModPath}
loadMore={this.loadMore}
acceptComment={this.acceptComment}
rejectComment={this.rejectComment}
activeTab={this.activeTab}
queueConfig={currentQueueConfig}
handleCommentChange={this.handleCommentChange}
selectedCommentId={this.props.selectedCommentId}
commentBelongToQueue={this.commentBelongToQueue}
cleanUpQueue={this.cleanUpQueue}
/>;
return (
<Moderation
{...this.props}
getModPath={getModPath}
loadMore={this.loadMore}
acceptComment={this.acceptComment}
rejectComment={this.rejectComment}
activeTab={this.activeTab}
queueConfig={currentQueueConfig}
handleCommentChange={this.handleCommentChange}
selectedCommentId={this.props.selectedCommentId}
commentBelongToQueue={this.commentBelongToQueue}
cleanUpQueue={this.cleanUpQueue}
/>
);
}
}
const COMMENT_ADDED_SUBSCRIPTION = gql`
@@ -368,28 +423,57 @@ const commentConnectionFragment = gql`
${Comment.fragments.comment}
`;
const withModQueueQuery = withQuery(({queueConfig}) => gql`
const withModQueueQuery = withQuery(
({queueConfig}) => gql`
query CoralAdmin_Moderation($asset_id: ID, $sortOrder: SORT_ORDER, $allAssets: Boolean!, $nullStatuses: [COMMENT_STATUS!]) {
${Object.keys(queueConfig).map((queue) => `
${Object.keys(queueConfig).map(
(queue) => `
${queue}: comments(query: {
statuses: ${queueConfig[queue].statuses ? `[${queueConfig[queue].statuses.join(', ')}],` : '$nullStatuses'}
${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''}
${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''}
statuses: ${
queueConfig[queue].statuses
? `[${queueConfig[queue].statuses.join(', ')}],`
: '$nullStatuses'
}
${
queueConfig[queue].tags
? `tags: ["${queueConfig[queue].tags.join('", "')}"],`
: ''
}
${
queueConfig[queue].action_type
? `action_type: ${queueConfig[queue].action_type}`
: ''
}
asset_id: $asset_id,
sortOrder: $sortOrder,
limit: 20,
}) {
...CoralAdmin_Moderation_CommentConnection
}
`)}
${Object.keys(queueConfig).map((queue) => `
`
)}
${Object.keys(queueConfig).map(
(queue) => `
${queue}Count: commentCount(query: {
statuses: ${queueConfig[queue].statuses ? `[${queueConfig[queue].statuses.join(', ')}],` : '$nullStatuses'}
${queueConfig[queue].tags ? `tags: ["${queueConfig[queue].tags.join('", "')}"],` : ''}
${queueConfig[queue].action_type ? `action_type: ${queueConfig[queue].action_type}` : ''}
statuses: ${
queueConfig[queue].statuses
? `[${queueConfig[queue].statuses.join(', ')}],`
: '$nullStatuses'
}
${
queueConfig[queue].tags
? `tags: ["${queueConfig[queue].tags.join('", "')}"],`
: ''
}
${
queueConfig[queue].action_type
? `action_type: ${queueConfig[queue].action_type}`
: ''
}
asset_id: $asset_id,
})
`)}
`
)}
asset(id: $asset_id) @skip(if: $allAssets) {
id
title
@@ -409,20 +493,22 @@ const withModQueueQuery = withQuery(({queueConfig}) => gql`
}
${Comment.fragments.root}
${commentConnectionFragment}
`, {
options: (props) => {
const id = getAssetId(props);
return {
variables: {
asset_id: id,
sortOrder: props.moderation.sortOrder,
allAssets: id === null,
nullStatuses: null,
},
fetchPolicy: 'network-only'
};
},
});
`,
{
options: (props) => {
const id = getAssetId(props);
return {
variables: {
asset_id: id,
sortOrder: props.moderation.sortOrder,
allAssets: id === null,
nullStatuses: null,
},
fetchPolicy: 'network-only',
};
},
}
);
const mapStateToProps = (state) => ({
moderation: state.moderation,
@@ -430,25 +516,26 @@ const mapStateToProps = (state) => ({
});
const mapDispatchToProps = (dispatch) => ({
...bindActionCreators({
toggleModal,
singleView,
showBanUserDialog,
hideShortcutsNote,
toggleStorySearch,
showSuspendUserDialog,
viewUserDetail,
setSortOrder,
storySearchChange,
clearState,
notify,
selectCommentId,
}, dispatch),
...bindActionCreators(
{
toggleModal,
singleView,
hideShortcutsNote,
toggleStorySearch,
viewUserDetail,
setSortOrder,
storySearchChange,
clearState,
notify,
selectCommentId,
},
dispatch
),
});
export default compose(
withQueueConfig(baseQueueConfig),
connect(mapStateToProps, mapDispatchToProps),
withSetCommentStatus,
withModQueueQuery,
withModQueueQuery
)(ModerationContainer);
-1
View File
@@ -11,4 +11,3 @@ export const isPremod = (mod) => mod === 'PRE';
export const getModPath = (type = 'all', assetId) =>
assetId ? `/admin/moderate/${type}/${assetId}` : `/admin/moderate/${type}`;
+16 -33
View File
@@ -2,8 +2,13 @@ import jwtDecode from 'jwt-decode';
import bowser from 'bowser';
import * as actions from '../constants/auth';
import {notify} from 'coral-framework/actions/notification';
import t from 'coral-framework/services/i18n';
import get from 'lodash/get';
export const updateStatus = (status) => ({
type: actions.UPDATE_STATUS,
status,
});
export const showSignInDialog = () => ({
type: actions.SHOW_SIGNIN_DIALOG,
@@ -35,45 +40,19 @@ export const blurSignInDialog = () => ({
type: actions.BLUR_SIGNIN_DIALOG,
});
export const createUsernameRequest = () => ({
type: actions.CREATE_USERNAME_REQUEST
});
export const showCreateUsernameDialog = () => ({
type: actions.SHOW_CREATEUSERNAME_DIALOG
});
export const hideCreateUsernameDialog = () => ({
type: actions.HIDE_CREATEUSERNAME_DIALOG
});
const createUsernameSuccess = () => ({
type: actions.CREATE_USERNAME_SUCCESS
});
const createUsernameFailure = (error) => ({
type: actions.CREATE_USERNAME_FAILURE,
error
});
export const updateUsername = ({username}) => ({
export const updateUsername = (username) => ({
type: actions.UPDATE_USERNAME,
username
});
export const createUsername = (userId, formData) => (dispatch, _, {rest}) => {
dispatch(createUsernameRequest());
rest('/account/username', {method: 'PUT', body: formData})
.then(() => {
dispatch(createUsernameSuccess());
dispatch(hideCreateUsernameDialog());
dispatch(updateUsername(formData));
})
.catch((error) => {
console.error(error);
const errorMessage = error.translation_key ? t(`error.${error.translation_key}`) : error.toString();
dispatch(createUsernameFailure(errorMessage));
});
};
export const changeView = (view) => (dispatch) => {
dispatch({
type: actions.CHANGE_VIEW,
@@ -304,6 +283,8 @@ const checkLoginSuccess = (user, isAdmin) => ({
isAdmin
});
const ErrNotLoggedIn = new Error('Not logged in');
export const checkLogin = () => (dispatch, _, {rest, client, pym, storage}) => {
dispatch(checkLoginRequest());
rest('/auth')
@@ -312,7 +293,7 @@ export const checkLogin = () => (dispatch, _, {rest, client, pym, storage}) => {
if (storage) {
storage.removeItem('token');
}
throw new Error('Not logged in');
throw ErrNotLoggedIn;
}
// Reset the websocket.
@@ -321,13 +302,15 @@ export const checkLogin = () => (dispatch, _, {rest, client, pym, storage}) => {
dispatch(checkLoginSuccess(result.user));
pym.sendMessage('coral-auth-changed', JSON.stringify(result.user));
// Display create username dialog if necessary.
if (result.user.canEditName && result.user.status !== 'BANNED') {
// This is for login via social. Usernames should be set.
if (get(result.user, 'status.username.status') === 'UNSET' && !get(result.user, 'status.banned.status')) {
dispatch(showCreateUsernameDialog());
}
})
.catch((error) => {
console.error(error);
if (error !== ErrNotLoggedIn) {
console.error(error);
}
if (error.status && error.status === 401 && storage) {
// Unauthorized.
@@ -0,0 +1,17 @@
import React, {Component} from 'react';
import t from 'coral-framework/services/i18n';
import RestrictedMessageBox from 'coral-framework/components/RestrictedMessageBox';
class BannedAccount extends Component {
render () {
return (
<RestrictedMessageBox>
<span>
<b>{t('framework.banned_account_header')}</b><br/> {t('framework.banned_account_body')}
</span>
</RestrictedMessageBox>
);
}
}
export default BannedAccount;
@@ -54,3 +54,4 @@ export const SET_REQUIRE_EMAIL_VERIFICATION = 'SET_REQUIRE_EMAIL_VERIFICATION';
export const SET_REDIRECT_URI = 'SET_REDIRECT_URI';
export const RESET_SIGNIN_DIALOG = 'RESET_SIGNIN_DIALOG';
export const UPDATE_STATUS = 'UPDATE_STATUS';
@@ -21,7 +21,14 @@ import t from 'coral-framework/services/i18n';
import PropTypes from 'prop-types';
import {setActiveTab} from '../actions/embed';
const {logout, checkLogin, focusSignInDialog, blurSignInDialog, hideSignInDialog} = authActions;
const {
logout,
checkLogin,
focusSignInDialog,
blurSignInDialog,
hideSignInDialog,
updateStatus,
} = authActions;
const {fetchAssetSuccess} = assetActions;
class EmbedContainer extends React.Component {
@@ -35,20 +42,23 @@ class EmbedContainer extends React.Component {
if (props.auth.loggedIn) {
const newSubscriptions = [{
document: USER_BANNED_SUBSCRIPTION,
updateQuery: () => {
updateQuery: (_, {subscriptionData: {data: {userBanned: {state}}}}) => {
notify('info', t('your_account_has_been_banned'));
props.updateStatus(state.status);
},
},
{
document: USER_SUSPENDED_SUBSCRIPTION,
updateQuery: () => {
updateQuery: (_, {subscriptionData: {data: {userSuspended: {state}}}}) => {
notify('info', t('your_account_has_been_suspended'));
props.updateStatus(state.status);
},
},
{
document: USERNAME_REJECTED_SUBSCRIPTION,
updateQuery: () => {
updateQuery: (_, {subscriptionData: {data: {usernameRejected: {state}}}}) => {
notify('info', t('your_username_has_been_rejected'));
props.updateStatus(state.status);
},
}];
@@ -116,10 +126,18 @@ const USER_BANNED_SUBSCRIPTION = gql`
subscription UserBanned($user_id: ID!) {
userBanned(user_id: $user_id){
id
status
canEditName
suspension {
until
state {
status {
username {
status
}
banned {
status
}
suspension {
until
}
}
}
}
}
@@ -129,10 +147,18 @@ const USER_SUSPENDED_SUBSCRIPTION = gql`
subscription UserSuspended($user_id: ID!) {
userSuspended(user_id: $user_id){
id
status
canEditName
suspension {
until
state {
status {
username {
status
}
banned {
status
}
suspension {
until
}
}
}
}
}
@@ -142,10 +168,18 @@ const USERNAME_REJECTED_SUBSCRIPTION = gql`
subscription UsernameRejected($user_id: ID!) {
usernameRejected(user_id: $user_id){
id
status
canEditName
suspension {
until
state {
status {
username {
status
}
banned {
status
}
suspension {
until
}
}
}
}
}
@@ -170,7 +204,19 @@ const EMBED_QUERY = gql`
) {
me {
id
status
state {
status {
username {
status
}
banned {
status
}
suspension {
until
}
}
}
}
asset(id: $assetId, url: $assetUrl) {
...${getDefinitionName(Configure.fragments.asset)}
@@ -224,6 +270,7 @@ const mapDispatchToProps = (dispatch) =>
focusSignInDialog,
blurSignInDialog,
hideSignInDialog,
updateStatus,
},
dispatch
);
+34 -22
View File
@@ -1,7 +1,10 @@
import {gql} from 'react-apollo';
import update from 'immutability-helper';
import uuid from 'uuid/v4';
import {insertCommentIntoEmbedQuery, removeCommentFromEmbedQuery} from './utils';
import {
insertCommentIntoEmbedQuery,
removeCommentFromEmbedQuery,
} from './utils';
import {mapLeaves} from 'coral-framework/utils';
export default {
@@ -45,7 +48,7 @@ export default {
}
}
`,
CreateDontAgreeResponse : gql`
CreateDontAgreeResponse: gql`
fragment CoralEmbedStream_CreateDontAgreeResponse on CreateDontAgreeResponse {
dontagree {
id
@@ -143,13 +146,13 @@ export default {
tag: {
name: tag,
created_at: new Date().toISOString(),
__typename: 'Tag'
__typename: 'Tag',
},
assigned_by: {
id: auth.user.id,
__typename: 'User'
__typename: 'User',
},
__typename: 'TagLink'
__typename: 'TagLink',
})),
},
created_at: new Date().toISOString(),
@@ -159,9 +162,9 @@ export default {
tag: {
name: tag,
created_at: new Date().toISOString(),
__typename: 'Tag'
__typename: 'Tag',
},
__typename: 'TagLink'
__typename: 'TagLink',
})),
status: 'NONE',
replyCount: 0,
@@ -171,9 +174,7 @@ export default {
title: '',
url: '',
},
parent: parent_id
? {__typename: 'Comment', id: parent_id}
: null,
parent: parent_id ? {__typename: 'Comment', id: parent_id} : null,
replies: {
__typename: 'CommentConnection',
nodes: [],
@@ -188,13 +189,17 @@ export default {
},
status_history: [],
id: `pending-${uuid()}`,
}
}
},
},
},
updateQueries: {
CoralEmbedStream_Embed: (prev, {mutationResult: {data: {createComment: {comment}}}}) => {
CoralEmbedStream_Embed: (
prev,
{mutationResult: {data: {createComment: {comment}}}}
) => {
if (
prev.me.roles.indexOf('ADMIN') === -1 && prev.asset.settings.moderation === 'PRE' ||
(prev.me.role !== 'ADMIN' &&
prev.asset.settings.moderation === 'PRE') ||
comment.status === 'PREMOD' ||
comment.status === 'REJECTED' ||
comment.status === 'SYSTEM_WITHHELD'
@@ -203,7 +208,10 @@ export default {
}
return insertCommentIntoEmbedQuery(prev, comment);
},
CoralEmbedStream_Profile: (prev, {mutationResult: {data: {createComment: {comment}}}}) => {
CoralEmbedStream_Profile: (
prev,
{mutationResult: {data: {createComment: {comment}}}}
) => {
return update(prev, {
me: {
comments: {
@@ -212,19 +220,24 @@ export default {
},
});
},
}
},
}),
EditComment: () => ({
updateQueries: {
CoralEmbedStream_Embed: (prev, {mutationResult: {data: {editComment: {comment}}}}) => {
if (!['PREMOD', 'REJECTED', 'SYSTEM_WITHHELD'].includes(comment.status)) {
CoralEmbedStream_Embed: (
prev,
{mutationResult: {data: {editComment: {comment}}}}
) => {
if (
!['PREMOD', 'REJECTED', 'SYSTEM_WITHHELD'].includes(comment.status)
) {
return null;
}
return removeCommentFromEmbedQuery(prev, comment.id);
},
},
}),
UpdateAssetSettings: ({variables: {input}}) => ({
UpdateAssetSettings: ({variables: {input}}) => ({
updateQueries: {
CoralEmbedStream_Embed: (prev) => {
const updated = update(prev, {
@@ -233,9 +246,8 @@ export default {
},
});
return updated;
}
}
},
},
}),
},
};
+11 -29
View File
@@ -1,5 +1,6 @@
import * as actions from '../constants/auth';
import pym from 'coral-framework/services/pym';
import merge from 'lodash/merge';
const initialState = {
isLoading: false,
@@ -198,6 +199,7 @@ export default function auth (state = initialState, action) {
user: {
...state.user,
username: action.username,
lowercaseUsername: action.username.toLowerCase(),
}
};
case actions.VERIFY_EMAIL_FAILURE:
@@ -227,35 +229,15 @@ export default function auth (state = initialState, action) {
...state,
redirectUri: action.uri,
};
case 'APOLLO_SUBSCRIPTION_RESULT':
if (action.operationName === 'UserBanned' && state.getIn(['user', 'id']) === action.variables.user_id) {
return {
...state,
user: {
...state.user,
...action.result.data.userBanned,
},
};
}
if (action.operationName === 'UserSuspended' && state.getIn(['user', 'id']) === action.variables.user_id) {
return {
...state,
user: {
...state.user,
...action.result.data.userSuspended,
},
};
}
if (action.operationName === 'UsernameRejected' && state.getIn(['user', 'id']) === action.variables.user_id) {
return {
...state,
user: {
...state.user,
...action.result.data.usernameRejected,
},
};
}
return state;
case actions.UPDATE_STATUS: {
return {
...state,
user: {
...state.user,
status: merge({}, state.user.status, action.status),
},
};
}
default :
return state;
}
@@ -15,23 +15,6 @@ export default function stream(state = initialState, action) {
activeTab: action.tab,
previousTab: state.activeTab,
};
case 'APOLLO_QUERY_INIT':
if (action.queryString.indexOf('query CoralEmbedStream_Embed(') >= 0) {
return {
...state,
refetching: action.isRefetch ? true : state.refetching,
refetchRequestId: action.isRefetch ? action.requestId : state.refetchRequestId,
};
}
return state;
case 'APOLLO_QUERY_RESULT':
if (action.operationName === 'CoralEmbedStream_Embed') {
return {
...state,
refetching: action.requestId === state.refetchRequestId ? false : state.refetching,
};
}
return state;
default:
return state;
}
@@ -0,0 +1,13 @@
.editNameInput {
margin-top: 10px;
margin-bottom: 10px;
padding: 10px;
border-radius: 3px;
border: solid 1px #d8d8d8;
font-size: 0.9em;
}
.alert {
margin-top: 10px;
color: #B71C1C;
}
@@ -0,0 +1,77 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import t from 'coral-framework/services/i18n';
import styles from './ChangeUsername.css';
import {Button} from 'coral-ui';
import validate from 'coral-framework/helpers/validate';
import RestrictedMessageBox from 'coral-framework/components/RestrictedMessageBox';
import {forEachError} from 'plugin-api/beta/client/utils';
class ChangeUsername extends Component {
static propTypes = {
changeUsername: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
}
state = {
username: '',
alert: '',
}
onSubmitClick = (e) => {
const {changeUsername, user} = this.props;
const {username} = this.state;
e.preventDefault();
if (username === this.props.user.username) {
this.setState({alert: t('error.SAME_USERNAME_PROVIDED')});
}
else if (validate.username(username)) {
changeUsername(user.id, username)
.then(() => location.reload())
.catch((error) => {
let errorMsg = '';
forEachError(error, ({msg}) => errorMsg = errorMsg ? `${errorMsg}, ${msg}` : msg);
this.setState({alert: errorMsg});
});
} else {
this.setState({alert: t('framework.edit_name.error')});
}
}
render () {
const {username, alert} = this.state;
return <RestrictedMessageBox>
<div className="talk-change-username">
<span>
{t('framework.edit_name.msg')}
</span>
<div className={styles.alert}>
{alert}
</div>
<label
htmlFor='username'
className="screen-reader-text">
{t('framework.edit_name.label')}
</label>
<input
type='text'
className={cn(styles.editNameInput, 'talk-change-username-username-input')}
value={username}
placeholder={t('framework.edit_name.label')}
onChange={(e) => this.setState({username: e.target.value})}
/><br/>
<Button
className="talk-change-username-submit-button"
onClick={this.onSubmitClick} >
{t('framework.edit_name.button')}
</Button>
</div>
</RestrictedMessageBox>;
}
}
export default ChangeUsername;
@@ -2,7 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import {StreamError} from './StreamError';
import Comment from '../containers/Comment';
import SuspendedAccount from './SuspendedAccount';
import BannedAccount from '../../../components/BannedAccount';
import ChangeUsername from '../containers/ChangeUsername';
import Slot from 'coral-framework/components/Slot';
import InfoBox from 'talk-plugin-infobox/InfoBox';
import {can} from 'coral-framework/services/perms';
@@ -15,6 +16,7 @@ import QuestionBox from '../../../components/QuestionBox';
import {isCommentActive} from 'coral-framework/utils';
import {Tab, TabCount, TabPane} from 'coral-ui';
import cn from 'classnames';
import get from 'lodash/get';
import {getTopLevelParent, attachCommentToParent} from '../../../graphql/utils';
import AllCommentsPane from './AllCommentsPane';
@@ -225,19 +227,20 @@ class Stream extends React.Component {
notify,
updateItem,
auth: {loggedIn, user},
editName,
} = this.props;
const {keepCommentBox} = this.state;
const open = !asset.isClosed;
const banned = user && user.status === 'BANNED';
const banned = get(user, 'status.banned.status');
const suspensionUntil = get(user, 'status.suspension.until');
const rejectedUsername = get(user, 'status.username.status') === 'REJECTED';
const temporarilySuspended =
user &&
user.suspension.until &&
new Date(user.suspension.until) > new Date();
suspensionUntil &&
new Date(suspensionUntil) > new Date();
const showCommentBox = loggedIn && ((!banned && !temporarilySuspended && !highlightedComment) || keepCommentBox);
const showCommentBox = loggedIn && ((!banned && !temporarilySuspended && !rejectedUsername && !highlightedComment) || keepCommentBox);
const slotProps = {data};
const slotQueryData = {root, asset};
@@ -270,15 +273,11 @@ class Stream extends React.Component {
{t(
'stream.temporarily_suspended',
root.settings.organizationName,
timeago(user.suspension.until)
timeago(suspensionUntil)
)}
</RestrictedMessageBox>}
{banned &&
<SuspendedAccount
canEditName={user && user.canEditName}
editName={editName}
currentUsername={user.username}
/>}
{!banned && rejectedUsername && <ChangeUsername user={user} />}
{banned && <BannedAccount />}
{showCommentBox &&
<CommentBox
notify={notify}
@@ -0,0 +1,7 @@
import {compose} from 'react-apollo';
import {withChangeUsername} from 'coral-framework/graphql/mutations';
import ChangeUsername from '../components/ChangeUsername';
export default compose(
withChangeUsername,
)(ChangeUsername);
@@ -2,15 +2,25 @@ import React from 'react';
import {gql, compose} from 'react-apollo';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {ADDTL_COMMENTS_ON_LOAD_MORE, THREADING_LEVEL} from '../../../constants/stream';
import {
withPostComment, withPostFlag, withPostDontAgree,
withDeleteAction, withEditComment
ADDTL_COMMENTS_ON_LOAD_MORE,
THREADING_LEVEL,
} from '../../../constants/stream';
import {
withPostComment,
withPostFlag,
withPostDontAgree,
withDeleteAction,
withEditComment,
} from 'coral-framework/graphql/mutations';
import * as authActions from 'coral-embed-stream/src/actions/auth';
import * as notificationActions from 'coral-framework/actions/notification';
import {setActiveReplyBox, setActiveTab, viewAllComments} from '../../../actions/stream';
import {
setActiveReplyBox,
setActiveTab,
viewAllComments,
} from '../../../actions/stream';
import Stream from '../components/Stream';
import Comment from './Comment';
import {withFragments, withEmit} from 'coral-framework/hocs';
@@ -43,7 +53,10 @@ class StreamContainer extends React.Component {
// Ignore mutations from me.
// TODO: need way to detect mutations created by this client, and allow mutations from other clients.
if (this.props.auth.user && commentEdited.user.id === this.props.auth.user.id) {
if (
this.props.auth.user &&
commentEdited.user.id === this.props.auth.user.id
) {
return prev;
}
@@ -52,7 +65,11 @@ class StreamContainer extends React.Component {
return prev;
}
if (['PREMOD', 'REJECTED', 'SYSTEM_WITHHELD'].includes(commentEdited.status)) {
if (
['PREMOD', 'REJECTED', 'SYSTEM_WITHHELD'].includes(
commentEdited.status
)
) {
return removeCommentFromEmbedQuery(prev, commentEdited.id);
}
},
@@ -69,14 +86,20 @@ class StreamContainer extends React.Component {
// Ignore mutations from me.
// TODO: need way to detect mutations created by this client, and allow mutations from other clients.
if (this.props.auth.user && commentAdded.user.id === this.props.auth.user.id) {
if (
this.props.auth.user &&
commentAdded.user.id === this.props.auth.user.id
) {
return prev;
}
// Exit if author is ignored.
if (
this.props.root.me &&
this.props.root.me.ignoredUsers.some(({id}) => id === commentAdded.user.id)) {
this.props.root.me.ignoredUsers.some(
({id}) => id === commentAdded.user.id
)
) {
return prev;
}
@@ -121,11 +144,11 @@ class StreamContainer extends React.Component {
sortOrder: 'ASC',
excludeIgnored: this.props.data.variables.excludeIgnored,
},
updateQuery: (prev, {fetchMoreResult:{comments}}) => {
updateQuery: (prev, {fetchMoreResult: {comments}}) => {
return insertFetchedCommentsIntoEmbedQuery(prev, comments, parent_id);
},
});
}
};
loadMoreComments = () => {
return this.props.data.fetchMore({
@@ -139,7 +162,7 @@ class StreamContainer extends React.Component {
sortBy: this.props.data.variables.sortBy,
excludeIgnored: this.props.data.variables.excludeIgnored,
},
updateQuery: (prev, {fetchMoreResult:{comments}}) => {
updateQuery: (prev, {fetchMoreResult: {comments}}) => {
return insertFetchedCommentsIntoEmbedQuery(prev, comments);
},
});
@@ -168,7 +191,10 @@ class StreamContainer extends React.Component {
}
componentWillReceiveProps(nextProps) {
if (this.props.sortOrder !== nextProps.sortOrder || this.props.sortBy !== nextProps.sortBy) {
if (
this.props.sortOrder !== nextProps.sortOrder ||
this.props.sortBy !== nextProps.sortBy
) {
nextProps.data.refetch();
}
}
@@ -178,23 +204,26 @@ class StreamContainer extends React.Component {
}
render() {
if (!this.props.asset
|| this.props.asset.comment === undefined
&& !this.props.asset.comments
if (
!this.props.asset ||
(this.props.asset.comment === undefined && !this.props.asset.comments)
) {
return <Spinner />;
}
const streamLoading = this.props.refetching || this.props.data.loading;
// @TODO: Detect refetch when we have apollo 2.0.
const streamLoading = this.props.data.loading;
return <Stream
{...this.props}
loadMore={this.loadMore}
loadMoreComments={this.loadMoreComments}
loadNewReplies={this.loadNewReplies}
userIsDegraged={this.userIsDegraged()}
loading={streamLoading}
/>;
return (
<Stream
{...this.props}
loadMore={this.loadMore}
loadMoreComments={this.loadMoreComments}
loadNewReplies={this.loadNewReplies}
userIsDegraged={this.userIsDegraged()}
loading={streamLoading}
/>
);
}
}
@@ -211,8 +240,8 @@ const commentFragment = gql`
`;
const COMMENTS_ADDED_SUBSCRIPTION = gql`
subscription CommentAdded($assetId: ID!, $excludeIgnored: Boolean){
commentAdded(asset_id: $assetId){
subscription CommentAdded($assetId: ID!, $excludeIgnored: Boolean) {
commentAdded(asset_id: $assetId) {
parent {
id
}
@@ -223,8 +252,8 @@ const COMMENTS_ADDED_SUBSCRIPTION = gql`
`;
const COMMENTS_EDITED_SUBSCRIPTION = gql`
subscription CommentEdited($assetId: ID!){
commentEdited(asset_id: $assetId){
subscription CommentEdited($assetId: ID!) {
commentEdited(asset_id: $assetId) {
id
body
status
@@ -281,11 +310,23 @@ const fragments = {
root: gql`
fragment CoralEmbedStream_Stream_root on RootQuery {
me {
status
state {
status {
username {
status
}
banned {
status
}
suspension {
until
}
}
}
ignoredUsers {
id
}
roles
role
}
settings {
organizationName
@@ -299,12 +340,15 @@ const fragments = {
fragment CoralEmbedStream_Stream_asset on Asset {
comment(id: $commentId) @include(if: $hasComment) {
...CoralEmbedStream_Stream_comment
${nest(`
${nest(
`
parent {
...CoralEmbedStream_Stream_comment
...nest
}
`, THREADING_LEVEL)}
`,
THREADING_LEVEL
)}
}
id
title
@@ -344,7 +388,6 @@ const fragments = {
const mapStateToProps = (state) => ({
auth: state.auth,
refetching: state.embed.refetching,
activeReplyBox: state.stream.activeReplyBox,
commentId: state.stream.commentId,
assetId: state.stream.assetId,
@@ -360,14 +403,17 @@ const mapStateToProps = (state) => ({
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({
showSignInDialog,
notify,
setActiveReplyBox,
editName,
viewAllComments,
setActiveStreamTab: setActiveTab,
}, dispatch);
bindActionCreators(
{
showSignInDialog,
notify,
setActiveReplyBox,
editName,
viewAllComments,
setActiveStreamTab: setActiveTab,
},
dispatch
);
export default compose(
withFragments(fragments),
@@ -377,5 +423,5 @@ export default compose(
withPostFlag,
withPostDontAgree,
withDeleteAction,
withEditComment,
withEditComment
)(StreamContainer);
@@ -4,7 +4,8 @@ import {findDOMNode} from 'react-dom';
export default class ClickOutside extends Component {
static propTypes = {
onClickOutside: PropTypes.func.isRequired
onClickOutside: PropTypes.func,
children: PropTypes.node,
};
static contextTypes = {
@@ -16,7 +17,7 @@ export default class ClickOutside extends Component {
handleClick = (e) => {
const {onClickOutside} = this.props;
if (!e || !this.domNode.contains(e.target)) {
onClickOutside(e);
onClickOutside && onClickOutside(e);
}
};
+8 -2
View File
@@ -3,11 +3,17 @@ import {createDefaultResponseFragments} from '../utils';
// fragments defined here are automatically registered.
export default {
...createDefaultResponseFragments(
'SetUserRoleResponse',
'ChangeUsernameResponse',
'SetUsernameResponse',
'BanUsersResponse',
'UnbanUserResponse',
'SetUserSuspensionStatusResponse',
'SetCommentStatusResponse',
'SetUsernameStatusResponse',
'UnsuspendUserResponse',
'SuspendUserResponse',
'RejectUsernameResponse',
'CreateCommentResponse',
'SetUserStatusResponse',
'CreateFlagResponse',
'EditCommentResponse',
'PostFlagResponse',
+151 -14
View File
@@ -177,39 +177,176 @@ export const withSuspendUser = withMutation(
})
});
export const withRejectUsername = withMutation(
export const withUnsuspendUser = withMutation(
gql`
mutation RejectUsername($input: RejectUsernameInput!) {
rejectUsername(input: $input) {
...RejectUsernameResponse
mutation UnsuspendUser($input: UnsuspendUserInput!) {
unsuspendUser(input: $input) {
...UnsuspendUserResponse
}
}
`, {
props: ({mutate}) => ({
rejectUsername: (input) => {
unsuspendUser: (input) => {
return mutate({
variables: {
input,
},
});
}
})
}),
});
export const withSetUserStatus = withMutation(
export const withApproveUsername = withMutation(
gql`
mutation SetUserStatus($userId: ID!, $status: USER_STATUS!) {
setUserStatus(id: $userId, status: $status) {
...SetUserStatusResponse
mutation ApproveUsername($id: ID!) {
approveUsername(id: $id) {
...SetUsernameStatusResponse
}
}
`, {
props: ({mutate}) => ({
setUserStatus: ({userId, status}) => {
approveUsername: (id) => {
return mutate({
variables: {
userId,
status
id,
},
});
}
})
});
export const withRejectUsername = withMutation(
gql`
mutation RejectUsername($id: ID!) {
rejectUsername(id: $id) {
...SetUsernameStatusResponse
}
}
`, {
props: ({mutate}) => ({
rejectUsername: (id) => {
return mutate({
variables: {
id,
},
});
}
})
});
const SetUsernameFragment = gql`
fragment Talk_SetUsername on User {
username
}`;
export const withChangeUsername = withMutation(
gql`
mutation ChangeUsername($id: ID!, $username: String!) {
changeUsername(id: $id, username: $username) {
...ChangeUsernameResponse
}
}
`, {
props: ({mutate}) => ({
changeUsername: (id, username) => {
return mutate({
variables: {
id,
username,
},
update: (proxy) => {
const fragmentId = `User_${id}`;
const data = {
__typename: 'User',
username,
};
proxy.writeFragment({fragment: SetUsernameFragment, id: fragmentId, data});
}
});
}
})
});
export const withSetUsername = withMutation(
gql`
mutation SetUsername($id: ID!, $username: String!) {
setUsername(id: $id, username: $username) {
...SetUsernameResponse
}
}
`, {
props: ({mutate}) => ({
setUsername: (id, username) => {
return mutate({
variables: {
id,
username,
},
update: (proxy) => {
const fragmentId = `User_${id}`;
const data = {
__typename: 'User',
username,
};
proxy.writeFragment({fragment: SetUsernameFragment, id: fragmentId, data});
}
});
}
})
});
export const withBanUser = withMutation(
gql`
mutation BanUser($input: BanUserInput!) {
banUser(input: $input) {
...BanUsersResponse
}
}
`, {
props: ({mutate}) => ({
banUser: (input) => {
return mutate({
variables: {
input,
},
});
}
}),
});
export const withUnbanUser = withMutation(
gql`
mutation UnbanUser($input: UnbanUserInput!) {
unbanUser(input: $input) {
...UnbanUserResponse
}
}
`, {
props: ({mutate}) => ({
unbanUser: (input) => {
return mutate({
variables: {
input,
},
});
}
}),
});
export const withSetUserRole = withMutation(
gql`
mutation SetUserRole($id: ID!, $role: USER_ROLES!) {
setUserRole(id: $id, role: $role) {
...SetUserRoleResponse
}
}
`, {
props: ({mutate}) => ({
setUserRole: (id, role) => {
return mutate({
variables: {
id,
role,
},
});
}
@@ -228,7 +365,7 @@ export const withPostComment = withMutation(
postComment: (input) => {
return mutate({
variables: {
input
input,
},
});
}
+9
View File
@@ -3,6 +3,12 @@ import has from 'lodash/has';
import get from 'lodash/get';
import merge from 'lodash/merge';
import moment from 'moment';
import 'moment/locale/da';
import 'moment/locale/es';
import 'moment/locale/fr';
import 'moment/locale/pt-br';
import daTA from 'timeago.js/locales/da';
import esTA from 'timeago.js/locales/es';
import frTA from 'timeago.js/locales/fr';
@@ -40,6 +46,9 @@ function init() {
const locale = getLocale();
setLocale(locale);
// Setting moment
moment.locale(locale);
// Extract language key.
lang = locale.split('-')[0];
+35 -12
View File
@@ -1,40 +1,63 @@
import intersection from 'lodash/intersection';
import get from 'lodash/get';
// =========================================================================
// BASIC PERMISSIONS
// =========================================================================
const basicPerms = {
INTERACT_WITH_COMMUNITY: (user) => {
const banned = get(user, 'status.banned.status');
const suspensionUntil = get(user, 'status.suspension.until');
const suspended = suspensionUntil && new Date(suspensionUntil) > new Date();
return !banned && !suspended;
},
EDIT_NAME: (user) => {
const usernameStatus = user.status.username.status;
return usernameStatus === 'UNSET' || usernameStatus === 'REJECTED';
},
};
// =========================================================================
// PERMISSIONS BY ROLE
// =========================================================================
const basicRoles = {
HAS_STAFF_TAG: ['ADMIN', 'MODERATOR', 'STAFF']
HAS_STAFF_TAG: ['ADMIN', 'MODERATOR', 'STAFF'],
};
const queryRoles = {
UPDATE_CONFIG: ['ADMIN'],
ACCESS_ADMIN: ['ADMIN', 'MODERATOR'],
VIEW_USER_EMAILS: ['ADMIN']
VIEW_USER_EMAILS: ['ADMIN'],
};
const mutationRoles = {
CHANGE_ROLES: ['ADMIN'],
MODERATE_COMMENTS: ['ADMIN', 'MODERATOR']
MODERATE_COMMENTS: ['ADMIN', 'MODERATOR'],
};
const roles = {...basicRoles, ...queryRoles, ...mutationRoles};
export const can = (user, ...perms) => {
if (!user) {
return false;
}
const banned = user.status === 'BANNED';
const suspended = user.suspension.until && new Date(user.suspension.until) > new Date();
return perms.every((perm) => {
if (perm === 'INTERACT_WITH_COMMUNITY') {
return !banned && !suspended;
// Basic Permissions
const permAction = basicPerms[perm];
if (typeof permAction !== 'undefined') {
return permAction(user);
}
// Permissions by Role
const role = roles[perm];
if (typeof role === 'undefined') {
throw new Error(`${perm} is not a valid role`);
throw new Error(`${perm} is not a valid role or permission`);
}
return intersection(role, user.roles).length > 0;
return role.includes(user.role);
});
};
+21
View File
@@ -1,3 +1,5 @@
import get from 'lodash/get';
/**
* getReliability
* retrieves reliability value as string
@@ -12,3 +14,22 @@ export const getReliability = (reliabilityValue) => {
return 'unreliable';
}
};
/**
* isSuspended
* retrieves boolean based on the user suspension status
*/
export const isSuspended = (user) => {
const suspensionUntil = get(user, 'state.status.suspension.until');
return user && suspensionUntil && new Date(suspensionUntil) > new Date();
};
/**
* isBanned
* retrieves boolean based on the user ban status
*/
export const isBanned = (user) => {
return get(user, 'state.status.banned.status');
};
-21
View File
@@ -1,21 +0,0 @@
.bio textarea {
width: 100%;
box-sizing: border-box;
border-radius: 2px;
min-height: 100px;
margin: 10px 0;
border: solid 1px #d8d8d8;
}
.bio h1 {
font-size: 16px;
margin: 3px 0;
}
.bio p {
margin: 3px 0;
}
.actions {
text-align: right;
}
-18
View File
@@ -1,18 +0,0 @@
import React from 'react';
import styles from './Bio.css';
import {Button} from '../../coral-ui';
export default ({bio, handleSave, handleInput, handleCancel}) => (
<div className={styles.bio}>
<h1>Bio</h1>
<p>Tell the community about yourself</p>
<form>
<textarea value={bio} onChange={handleInput} />
<div className={styles.actions}>
<Button cStyle='cancel' type="button" onClick={handleCancel} raised>Cancel</Button>
<Button cStyle='success' type="submit" onClick={handleSave}>Save Changes</Button>
</div>
</form>
</div>
);
@@ -1,45 +0,0 @@
import React, {Component} from 'react';
import Bio from '../components/Bio';
export default class BioContainer extends Component {
constructor (props) {
super(props);
this.state = {
bio: props.bio
};
this.handleSave = this.handleSave.bind(this);
this.handleInput = this.handleInput.bind(this);
this.handleCancel = this.handleCancel.bind(this);
}
handleInput(e) {
this.setState({
bio: e.target.value
});
}
handleSave (e) {
e.preventDefault();
const {userData, saveBio} = this.props;
const {bio} = this.state;
saveBio(userData.id, {bio});
}
handleCancel () {
this.setState({
bio: this.props.bio
});
}
render () {
return <Bio
bio={this.state.bio}
userData={this.props.userData}
handleSave={this.handleSave}
handleInput={this.handleInput}
handleCancel={this.handleCancel}
/>;
}
}
+1
View File
@@ -150,6 +150,7 @@
box-shadow: none;
color: white;
background-color: #616161;
border-color: transparent;
}
> .icon {
+1 -1
View File
@@ -23,6 +23,7 @@
}
.closeButton {
composes: buttonReset from 'coral-framework/styles/reset.css';
position: absolute;
width: 40px;
height: 40px;
@@ -34,7 +35,6 @@
top: 60px;
box-shadow: -1px 3px 4px 0px rgba(0,0,0,0.15);
text-align: center;
padding-top: 10px;
cursor: pointer;
&:hover {
+7 -4
View File
@@ -1,11 +1,12 @@
import React from 'react';
import cn from 'classnames';
import PropTypes from 'prop-types';
import styles from './Drawer.css';
const Drawer = ({children, onClose}) => {
const Drawer = ({children, onClose, className = ''}) => {
return (
<div className={styles.drawer}>
<div className={styles.closeButton} onClick={onClose}>×</div>
<div className={cn(styles.drawer, className)}>
<button className={cn(styles.closeButton, [className, 'close-button'].join('-'))} onClick={onClose}>×</button>
<div className={styles.content}>
{children}
</div>
@@ -15,7 +16,9 @@ const Drawer = ({children, onClose}) => {
Drawer.propTypes = {
active: PropTypes.bool,
onClose: PropTypes.func.isRequired
onClose: PropTypes.func.isRequired,
children: PropTypes.node,
className: PropTypes.string,
};
export default Drawer;
+6 -6
View File
@@ -1,19 +1,19 @@
.dropdown {
position: relative;
width: 150px;
height: 36px;
height: 34px;
background: #2c2c2c;
box-sizing: border-box;
color: white;
border-radius: 2px;
border-radius: 3px;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
line-height: 1.4;
font-size: 13px;
font-size: 0.98em;
border-radius: 3px;
}
.toggle {
padding: 10px 15px;
padding: 8px 15px;
cursor: pointer;
outline: none;
@@ -58,5 +58,5 @@
right: 15px;
font-size: 1.2rem;
vertical-align: middle;
top: 13px;
top: 11px;
}
+2 -2
View File
@@ -17,14 +17,14 @@ class Tab extends React.Component {
getRootClassName({active, className, sub, classNames = {}} = this.props) {
return cn(
'talk-tab',
className,
{
[classNames.root || styles.root]: !sub,
[classNames.rootSub || styles.rootSub]: sub,
[classNames.rootActive || styles.rootActive]: active && !sub,
[classNames.rootSubActive || styles.rootSubActive]: active && sub,
'talk-tab-active': active,
}
},
className,
);
}
+2 -2
View File
@@ -13,11 +13,11 @@ class TabBar extends React.Component {
getRootClassName({className, classNames = {}, sub} = this.props) {
return cn(
'talk-tab-bar',
className,
{
[classNames.root || styles.root]: !sub,
[classNames.rootSub || styles.rootSub]: sub,
}
},
className,
);
}
@@ -7,6 +7,7 @@ import {PopupMenu, Button} from 'coral-ui';
import ClickOutside from 'coral-framework/components/ClickOutside';
import cn from 'classnames';
import styles from './styles.css';
import * as REASONS from '../helpers/flagReasons';
import {getErrorMessages} from 'coral-framework/utils';
@@ -90,10 +91,9 @@ export default class FlagButton extends Component {
let action = {
item_id,
item_type: itemType,
reason: null,
message
};
if (reason === 'COMMENT_NOAGREE') {
if (reason === REASONS.comment.noagree) {
postDontAgree(action)
.then(({data}) => {
if (itemType === 'COMMENTS') {
@@ -122,7 +122,7 @@ export default class FlagButton extends Component {
onPopupOptionClick = (sets) => (e) => {
// If flagging a user, indicate that this is referencing the username rather than the bio
if(sets === 'itemType' && e.target.value === 'users') {
if (sets === 'itemType' && e.target.value === 'users') {
this.setState({field: 'username'});
}