Merge branch 'master' into party-fixes

This commit is contained in:
Kim Gardner
2018-05-16 12:41:34 -04:00
committed by GitHub
33 changed files with 405 additions and 216 deletions
@@ -7,7 +7,6 @@ import styles from './Header.css';
import t from 'coral-framework/services/i18n';
import { Logo } from './Logo';
import { can } from 'coral-framework/services/perms';
import ModerationIndicator from '../routes/Moderation/containers/Indicator';
import CommunityIndicator from '../routes/Community/containers/Indicator';
const CoralHeader = ({
@@ -32,7 +31,6 @@ const CoralHeader = ({
activeClassName={styles.active}
>
{t('configure.moderate')}
<ModerationIndicator root={root} data={data} />
</IndexLink>
)}
<Link
@@ -1,10 +1,8 @@
.indicator {
display: inline-block;
background-color: #E46D59;
border-radius: 10px;
position: absolute;
top: 50%;
width: 7px;
height: 7px;
margin-top: -4px;
margin-left: 7px;
}
+4 -5
View File
@@ -2,21 +2,20 @@ import { gql } from 'react-apollo';
import withQuery from 'coral-framework/hocs/withQuery';
import Header from '../components/Header';
import CommunityIndicator from '../routes/Community/containers/Indicator';
import ModerationIndicator from '../routes/Moderation/containers/Indicator';
// TODO: eventually we will readd modqueue counts
// import ModerationIndicator from '../routes/Moderation/containers/Indicator';
import { getDefinitionName } from 'coral-framework/utils';
export default withQuery(
gql`
query TalkAdmin_Header($nullID: ID) {
...${getDefinitionName(ModerationIndicator.fragments.root)}
query TalkAdmin_Header {
...${getDefinitionName(CommunityIndicator.fragments.root)}
}
${ModerationIndicator.fragments.root}
${CommunityIndicator.fragments.root}
`,
{
options: {
variables: { nullID: null },
// variables: { nullID: null },
},
}
)(Header);
@@ -82,6 +82,20 @@ class StreamSettings extends React.Component {
this.props.updatePending({ updater });
};
updateDisableCommenting = () => {
const updater = {
disableCommenting: {
$set: !this.props.settings.disableCommenting,
},
};
this.props.updatePending({ updater });
};
updateDisableCommentingMessage = value => {
const updater = { disableCommentingMessage: { $set: value } };
this.props.updatePending({ updater });
};
updateAutoClose = () => {
const updater = {
autoCloseStream: { $set: !this.props.settings.autoCloseStream },
@@ -192,6 +206,25 @@ class StreamSettings extends React.Component {
&nbsp;
{t('configure.edit_comment_timeframe_text_post')}
</ConfigureCard>
<ConfigureCard
checked={settings.disableCommenting}
onCheckbox={this.updateDisableCommenting}
title={t('configure.disable_commenting_title')}
>
<p>{t('configure.disable_commenting_desc')}</p>
<div
className={cn(
styles.configSettingDisableCommenting,
settings.disableCommenting ? null : styles.hidden
)}
>
<MarkdownEditor
className={styles.descriptionBox}
onChange={this.updateDisableCommentingMessage}
value={settings.disableCommentingMessage}
/>
</div>
</ConfigureCard>
<ConfigureCard
checked={settings.autoCloseStream}
onCheckbox={this.updateAutoClose}
@@ -39,6 +39,8 @@ export default compose(
autoCloseStream
closedTimeout
closedMessage
disableCommenting
disableCommentingMessage
${getSlotFragmentSpreads(slots, 'settings')}
}
`,
@@ -63,10 +63,6 @@ class Moderation extends Component {
this.props.toggleStorySearch(true);
};
getActiveTabCount = (props = this.props) => {
return props.root[`${props.activeTab}Count`];
};
moderate = accept => {
const {
acceptComment,
@@ -139,12 +135,14 @@ class Moderation extends Component {
const comments = root[activeTab];
const activeTabCount = this.getActiveTabCount();
const menuItems = Object.keys(queueConfig).map(queue => ({
key: queue,
name: queueConfig[queue].name,
icon: queueConfig[queue].icon,
count: root[`${queue}Count`],
indicator:
['premod', 'reported'].includes(queue) && root[queue].nodes.length > 0,
// TODO: Eventually we'll reintroduce counting
// count: root[`${props.queue}Count`]
}));
const slotPassthrough = {
@@ -189,7 +187,6 @@ class Moderation extends Component {
loadMore={this.loadMore}
commentBelongToQueue={this.props.commentBelongToQueue}
isLoadingMore={this.state.isLoadingMore}
commentCount={activeTabCount}
currentUserId={this.props.currentUser.id}
viewUserDetail={viewUserDetail}
selectCommentId={props.selectCommentId}
@@ -1,6 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import CountBadge from '../../../components/CountBadge';
import Indicator from '../../../components/Indicator';
import styles from './ModerationMenu.css';
import { Icon } from 'coral-ui';
import { Link } from 'react-router';
@@ -32,7 +32,7 @@ const ModerationMenu = ({ asset = {}, items, getModPath, activeTab }) => {
activeClassName={styles.active}
>
<Icon name={queue.icon} className={styles.tabIcon} /> {queue.name}{' '}
<CountBadge count={queue.count} />
{queue.indicator && <Indicator />}
</Link>
))}
</div>
@@ -204,7 +204,7 @@ class ModerationQueue extends React.Component {
}
componentDidUpdate(prev) {
const { commentCount, selectedCommentId } = this.props;
const { selectedCommentId, hasNextPage } = this.props;
const switchedToMultiMode = prev.singleView && !this.props.singleView;
const switchedMode = prev.singleView !== this.props.singleView;
@@ -212,7 +212,6 @@ class ModerationQueue extends React.Component {
prev.selectedCommentId !== selectedCommentId && selectedCommentId;
const moderatedLastComment =
prev.comments.length > 0 && this.getCommentCountWithoutDagling() === 0;
const hasMoreComment = commentCount > 0;
if (switchedToMultiMode) {
// Reflow virtual list.
@@ -223,7 +222,7 @@ class ModerationQueue extends React.Component {
this.scrollToSelectedComment();
}
if (moderatedLastComment && hasMoreComment) {
if (moderatedLastComment && hasNextPage) {
this.props.loadMore();
}
}
@@ -240,10 +239,7 @@ class ModerationQueue extends React.Component {
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.props.hasNextPage) {
await this.props.loadMore();
this.selectDown();
return;
@@ -467,7 +463,6 @@ ModerationQueue.propTypes = {
acceptComment: PropTypes.func.isRequired,
commentBelongToQueue: PropTypes.func.isRequired,
cleanUpQueue: PropTypes.func.isRequired,
commentCount: PropTypes.number.isRequired,
loadMore: PropTypes.func.isRequired,
singleView: PropTypes.bool,
isLoadingMore: PropTypes.bool,
@@ -314,11 +314,11 @@ class ModerationContainer extends Component {
const currentQueueConfig = Object.assign({}, this.props.queueConfig);
if (premodEnabled && root.newCount === 0) {
if (premodEnabled && root.new.nodes.length === 0) {
delete currentQueueConfig.new;
}
if (!premodEnabled && root.premodCount === 0) {
if (!premodEnabled && root.premod.nodes.length === 0) {
delete currentQueueConfig.premod;
}
@@ -402,7 +402,7 @@ const COMMENT_RESET_SUBSCRIPTION = gql`
const LOAD_MORE_QUERY = gql`
query CoralAdmin_Moderation_LoadMore($limit: Int = 10, $cursor: Cursor, $sortOrder: SORT_ORDER, $asset_id: ID, $tags:[String!], $statuses:[COMMENT_STATUS!], $action_type: ACTION_TYPE) {
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sortOrder: $sortOrder, action_type: $action_type, tags: $tags}) {
comments(query: {limit: $limit, cursor: $cursor, asset_id: $asset_id, statuses: $statuses, sortOrder: $sortOrder, action_type: $action_type, tags: $tags, excludeDeleted: true}) {
nodes {
...${getDefinitionName(Comment.fragments.comment)}
}
@@ -456,7 +456,11 @@ const withModQueueQuery = withQuery(
}
`
)}
${Object.keys(queueConfig).map(
${
''
/*
TODO: eventually we'll reintroduce counting..
Object.keys(queueConfig).map(
queue => `
${queue}Count: commentCount(query: {
excludeDeleted: true,
@@ -478,7 +482,8 @@ const withModQueueQuery = withQuery(
asset_id: $asset_id,
})
`
)}
)*/
}
asset(id: $asset_id) @skip(if: $allAssets) {
id
title
@@ -4,6 +4,7 @@ import StreamError from './StreamError';
import Comment from '../containers/Comment';
import BannedAccount from '../../../components/BannedAccount';
import ChangeUsername from '../containers/ChangeUsername';
import Markdown from 'coral-framework/components/Markdown';
import Slot from 'coral-framework/components/Slot';
import InfoBox from './InfoBox';
import { can } from 'coral-framework/services/perms';
@@ -181,7 +182,9 @@ class Stream extends React.Component {
setActiveReplyBox={setActiveReplyBox}
activeReplyBox={activeReplyBox}
notify={notify}
disableReply={asset.isClosed}
disableReply={
asset.isClosed || asset.settings.disableCommenting
}
postComment={postComment}
currentUser={currentUser}
postFlag={postFlag}
@@ -215,7 +218,7 @@ class Stream extends React.Component {
currentUser,
} = this.props;
const { keepCommentBox } = this.state;
const open = !asset.isClosed;
const open = !(asset.isClosed || asset.settings.disableCommenting);
const banned = get(currentUser, 'status.banned.status');
const suspensionUntil = get(currentUser, 'status.suspension.until');
@@ -293,7 +296,13 @@ class Stream extends React.Component {
)}
</div>
) : (
<p>{asset.settings.closedMessage}</p>
<div>
{asset.isClosed ? (
<p>{asset.settings.closedMessage}</p>
) : (
<Markdown content={asset.settings.disableCommentingMessage} />
)}
</div>
)}
<Slot fill="stream" passthrough={slotPassthrough} />
@@ -434,6 +434,8 @@ const fragments = {
questionBoxIcon
closedTimeout
closedMessage
disableCommenting
disableCommentingMessage
charCountEnable
charCount
requireEmailConfirmation
@@ -59,6 +59,7 @@ const withSetUsername = hoistStatics(WrappedComponent => {
}
const changeSet = { success: false, loading: false, error };
this.setState(changeSet);
throw error;
}
};
+19
View File
@@ -161,6 +161,24 @@ class ErrAssetCommentingClosed extends TalkError {
}
}
// ErrCommentingDisabled is returned when a comment or action is attempted while
// commenting has been disabled site-wide.
class ErrCommentingDisabled extends TalkError {
constructor(message = null) {
super(
'asset commenting is closed',
{
status: 400,
translation_key: 'COMMENTING_DISABLED',
},
{
// Include the closedMessage in the metadata piece of the error.
message,
}
);
}
}
/**
* ErrAuthentication is returned when there is an error authenticating and the
* message is provided.
@@ -387,6 +405,7 @@ module.exports = {
ErrAuthentication,
ErrCannotIgnoreStaff,
ErrCommentTooShort,
ErrCommentingDisabled,
ErrContainsProfanity,
ErrEditWindowHasEnded,
ErrEmailAlreadyVerified,
+12
View File
@@ -55,6 +55,18 @@ class SettingsLoader {
// assembled Settings object.
return zipObject(fields, values);
}
/**
* get, like select, will retrieve the settings, but get will only return a
* single setting.
*
* @param {String} field the field to get
*/
async get(field) {
const value = await this._loader.load(field);
return value;
}
}
module.exports = () => ({ Settings: new SettingsLoader() });
+2 -2
View File
@@ -57,8 +57,8 @@ const Comment = {
asset({ asset_id }, _, { loaders: { Assets } }) {
return Assets.getByID.load(asset_id);
},
async editing(comment, _, { loaders: { Settings } }) {
const { editCommentWindowLength } = await Settings.select(
editing: async (comment, _, { loaders: { Settings } }) => {
const editCommentWindowLength = await Settings.get(
'editCommentWindowLength'
);
+14
View File
@@ -837,6 +837,13 @@ type Settings {
# closed.
closedMessage: String
# disableCommenting will disable commenting site-wide.
disableCommenting: Boolean
# disableCommentingMessage will be shown above the comment stream while
# commenting is disabled site-wide.
disableCommentingMessage: String
# editCommentWindowLength is the length of time (in milliseconds) after a
# comment is posted that it can still be edited by the author.
editCommentWindowLength: Int
@@ -1300,6 +1307,13 @@ input UpdateSettingsInput {
# closed.
closedMessage: String
# disableCommenting will disable commenting site-wide.
disableCommenting: Boolean
# disableCommentingMessage will be shown above the comment stream while
# commenting is disabled site-wide.
disableCommentingMessage: String
# charCountEnable is true when the character count restriction is enabled.
charCountEnable: Boolean
+3
View File
@@ -121,6 +121,8 @@ de:
custom_css_url_desc: "URL eines CSS-Stylesheets zum Überschreiben des Standard-Designs"
days: Tage
description: "Als Administrator können Sie die Einstellungen für den Kommentarbereich dieses Artikels anpassen:"
disable_commenting_title: "Kommentieren global deaktivieren"
disable_commenting_desc: "Verfassen Sie eine Nachricht, die angezeigt wird, solange das Kommentieren deaktiviert ist."
domain_list_text: "Geben Sie Domains an, für die diese Talk-Instanz freigegeben werden soll, z.B. für lokale Test- oder Produktionsumgebungen (Bsp.: localhost:3000 staging.domain.com domain.com)."
domain_list_title: "Zugelassene Domains"
edit_comment_timeframe_heading: "Zeitlimit zur Bearbeitung von Kommentaren"
@@ -223,6 +225,7 @@ de:
LOGIN_MAXIMUM_EXCEEDED: "Sie haben zu häufig erfolglos versucht, sich anzumelden. Bitte warten Sie."
PASSWORD_REQUIRED: "Passwort ist erforderlich"
COMMENTING_CLOSED: "Kommentarbereich ist bereits geschlossen"
COMMENTING_DISABLED: "Die Kommentarfunktion ist derzeit abgeschaltet"
NOT_FOUND: "Ressource nicht gefunden"
ALREADY_EXISTS: "Ressource existiert bereits"
INVALID_ASSET_URL: "Asset-URL ist ungültig"
+3
View File
@@ -124,6 +124,8 @@ en:
custom_css_url_desc: "URL of a CSS stylesheet that will override default Embed Stream styles. Can be internal or external."
days: Days
description: "Change the comment settings on this story."
disable_commenting_title: "Deactivate commenting site-wide"
disable_commenting_desc: "Write a message that will be displayed while commenting is deactivated."
domain_list_text: "Enter the domains you would like to permit for Talk e.g. your local staging and production environments (ex. localhost:3000 staging.domain.com domain.com)."
domain_list_title: "Permitted Domains"
edit_info: "Edit Info"
@@ -247,6 +249,7 @@ en:
LOGIN_MAXIMUM_EXCEEDED: "You have made too many unsuccessful password attempts. Please wait."
PASSWORD_REQUIRED: "Must input a password"
COMMENTING_CLOSED: "Commenting is already closed"
COMMENTING_DISABLED: "Commenting is currently disabled on this site"
NOT_FOUND: "Resource not found"
ALREADY_EXISTS: "Resource already exists"
INVALID_ASSET_URL: "Assert URL is invalid"
+6 -13
View File
@@ -9,7 +9,8 @@ const Action = new Schema(
id: {
type: String,
default: uuid.v4,
unique: true,
unique: 1,
index: 1,
},
action_type: {
type: String,
@@ -19,7 +20,10 @@ const Action = new Schema(
type: String,
enum: ITEM_TYPES,
},
item_id: String,
item_id: {
type: String,
index: 1,
},
user_id: String,
// The element that summaries will additionally group on in addtion to their action_type, item_type, and
@@ -37,15 +41,4 @@ const Action = new Schema(
}
);
// Create an index on the `item_id` field so that queries looking for
// actions based on the item id can resolve faster.
Action.index(
{
item_id: 1,
},
{
background: true,
}
);
module.exports = Action;
+55 -104
View File
@@ -55,12 +55,16 @@ const Comment = new Schema(
type: String,
default: uuid.v4,
unique: true,
index: true,
},
body: {
type: String,
},
body_history: [BodyHistoryItemSchema],
asset_id: String,
asset_id: {
type: String,
index: true,
},
author_id: String,
status_history: [Status],
status: {
@@ -90,7 +94,6 @@ const Comment = new Schema(
// deleted_at stores the date that the given comment was deleted.
deleted_at: {
type: Date,
default: null,
},
// Additional metadata stored on the field.
@@ -110,95 +113,67 @@ const Comment = new Schema(
}
);
// Add the indexes for the id of the comment.
Comment.index(
{
id: 1,
},
{
unique: true,
background: false,
}
);
Comment.index(
{
status: 1,
created_at: 1,
},
{
background: true,
}
);
Comment.index(
{
status: 1,
created_at: 1,
asset_id: 1,
},
{
background: true,
}
);
// Create a sparse index to search across.
Comment.index(
{
created_at: 1,
'action_counts.flag': 1,
status: 1,
},
{
background: true,
sparse: true,
}
);
// Create a sparse index to search across.
Comment.index(
{
'action_counts.flag': 1,
status: 1,
},
{
background: true,
sparse: true,
}
);
// Add an index that is optimized for finding flagged comments.
Comment.index(
{
asset_id: 1,
created_at: 1,
'action_counts.flag': 1,
},
{
background: true,
}
);
// Add an index for the reply sort.
Comment.index(
{
asset_id: 1,
deleted_at: 1,
created_at: -1,
reply_count: -1,
},
{
background: true,
}
{ partialFilterExpression: { deleted_at: null } }
);
// Add an index that is optimized for finding a user's comments.
Comment.index(
{
author_id: 1,
deleted_at: 1,
status: 1,
created_at: -1,
},
{ partialFilterExpression: { deleted_at: null } }
);
Comment.index({
asset_id: 1,
created_at: -1,
});
Comment.index({
asset_id: 1,
created_at: 1,
});
Comment.index({
author_id: 1,
created_at: -1,
});
Comment.index({
asset_id: 1,
status: 1,
});
Comment.index({
asset_id: 1,
parent_id: 1,
reply_count: -1,
created_at: -1,
});
Comment.index({
asset_id: 1,
reply_count: -1,
created_at: -1,
});
Comment.index(
{
'action_counts.flag': 1,
status: 1,
created_at: -1,
},
{
background: true,
partialFilterExpression: {
'action_counts.flag': { $exists: true, $gt: 0 },
deleted_at: null,
},
}
);
@@ -210,34 +185,10 @@ Comment.index(
status: 1,
},
{
background: true,
}
);
// Optimize for tag searches/counts.
Comment.index(
{
'tags.tag.name': 1,
status: 1,
},
{
background: true,
sparse: true,
}
);
// Add an index that is optimized for sorting based on the created_at timestamp
// but also good at locating comments that have a specific asset id.
Comment.index(
{
asset_id: 1,
created_at: 1,
},
{
background: true,
}
);
Comment.virtual('edited').get(function() {
return this.body_history.length > 1;
});
+10
View File
@@ -12,6 +12,8 @@ const Setting = new Schema(
id: {
type: String,
default: '1',
unique: 1,
index: true,
},
moderation: {
type: String,
@@ -66,6 +68,14 @@ const Setting = new Schema(
type: String,
default: 'Expired',
},
disableCommenting: {
type: Boolean,
default: false,
},
disableCommentingMessage: {
type: String,
default: '',
},
wordlist: {
banned: {
type: Array,
+19 -28
View File
@@ -58,6 +58,7 @@ const User = new Schema(
default: uuid.v4,
unique: true,
required: true,
index: true,
},
// This is sourced from the social provider or set manually during user setup
@@ -107,6 +108,7 @@ const User = new Schema(
status: {
type: String,
enum: USER_STATUS_USERNAME,
index: true,
},
// History stores the history of username status changes.
@@ -135,6 +137,7 @@ const User = new Schema(
type: Boolean,
required: true,
default: false,
index: true,
},
history: [
{
@@ -226,41 +229,26 @@ User.index(
}
);
User.index(
{
lowercaseUsername: 1,
'profiles.id': 1,
created_at: -1,
},
{
background: true,
}
);
User.index({
lowercaseUsername: 1,
'profiles.id': 1,
created_at: -1,
});
// This query is executed often, to count the number of flagged accounts with
// usernames.
User.index(
{
'action_counts.flag': 1,
'status.username.status': 1,
},
{
background: true,
}
);
User.index({
'action_counts.flag': 1,
'status.username.status': 1,
});
// Sorting users by created at is the default people search.
User.index(
{
created_at: -1,
},
{
background: true,
}
);
User.index({
created_at: -1,
});
/**
* returns true if a commenter is staff
* returns true if a commenter is staff.
*/
User.method('isStaff', function() {
return this.role !== 'COMMENTER';
@@ -330,6 +318,9 @@ User.virtual('hasVerifiedEmail').get(function() {
});
});
/**
* system returns true when the user is a system user.
*/
User.virtual('system')
.get(function() {
return this._system;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "talk",
"version": "4.4.0",
"version": "4.4.1",
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
"main": "app.js",
"private": true,
+15 -3
View File
@@ -11,10 +11,22 @@ function getReactionConfig(reaction) {
if (CREATE_MONGO_INDEXES) {
// Create the index on the comment model based on the reaction config.
Comment.collection.createIndex(
Comment.collection.ensureIndex(
{
created_at: 1,
[`action_counts.${sc(reaction)}`]: 1,
asset_id: 1,
[`action_counts.${sc(reaction)}`]: -1,
created_at: -1,
},
{
background: true,
}
);
Comment.collection.ensureIndex(
{
asset_id: 1,
[`action_counts.${sc(reaction)}`]: -1,
created_at: -1,
},
{
background: true,
+1 -1
View File
@@ -71,7 +71,7 @@ module.exports = {
permalink: asset.url,
comment_type: 'comment',
comment_content: input.body,
is_test: true,
is_test: false,
});
debug(`comment analyzed as ${spam ? 'being' : 'not being'} spam`);
@@ -4,10 +4,74 @@ import styles from './ChangeEmailContentDialog.css';
import InputField from './InputField';
import { Button } from 'plugin-api/beta/client/components/ui';
import { t } from 'plugin-api/beta/client/services';
import validate from 'coral-framework/helpers/validate';
import errorMsj from 'coral-framework/helpers/error';
const initialState = {
showError: false,
formData: {
confirmPassword: '',
},
errors: {},
};
class ChangeEmailContentDialog extends React.Component {
state = {
showError: false,
state = initialState;
clearForm = () => {
this.setState(initialState);
};
addError = err => {
this.setState(({ errors }) => ({
errors: { ...errors, ...err },
}));
};
removeError = errKey => {
this.setState(state => {
const { [errKey]: _, ...errors } = state.errors;
return {
errors,
};
});
};
fieldValidation = (value, type, name) => {
if (!value.length) {
this.addError({
[name]: t('talk-plugin-local-auth.change_password.required_field'),
});
} else if (!validate[type](value)) {
this.addError({ [name]: errorMsj[type] });
} else {
this.removeError(name);
}
};
onChange = e => {
const { name, value, type, dataset } = e.target;
const validationType = dataset.validationType || type;
this.setState(
state => ({
formData: {
...state.formData,
[name]: value,
},
}),
() => {
this.fieldValidation(value, validationType, name);
}
);
};
hasError = err => {
return Object.keys(this.state.errors).indexOf(err) !== -1;
};
getError = errorKey => {
return this.state.errors[errorKey];
};
showError = () => {
@@ -16,24 +80,31 @@ class ChangeEmailContentDialog extends React.Component {
});
};
cancel = () => {
this.clearForm();
this.props.closeDialog();
};
confirmChanges = async e => {
e.preventDefault();
const { confirmPassword = '' } = this.state.formData;
if (this.formHasError()) {
this.showError();
return;
}
await this.props.save();
await this.props.save(confirmPassword);
this.props.next();
};
formHasError = () => this.props.hasError('confirmPassword');
formHasError = () => this.hasError('confirmPassword');
render() {
return (
<div>
<span className={styles.close} onClick={this.props.cancel}>
<span className={styles.close} onClick={this.cancel}>
×
</span>
<h1 className={styles.title}>
@@ -59,17 +130,17 @@ class ChangeEmailContentDialog extends React.Component {
label={t('talk-plugin-local-auth.change_email.enter_password')}
name="confirmPassword"
type="password"
onChange={this.props.onChange}
defaultValue=""
hasError={this.props.hasError('confirmPassword')}
errorMsg={this.props.getError('confirmPassword')}
onChange={this.onChange}
value={this.state.formData.confirmPassword}
hasError={this.hasError('confirmPassword')}
errorMsg={this.getError('confirmPassword')}
showError={this.state.showError}
columnDisplay
/>
<div className={styles.bottomActions}>
<Button
className={styles.cancel}
onClick={this.props.cancel}
onClick={this.cancel}
type="button"
>
{t('talk-plugin-local-auth.change_email.cancel')}
@@ -86,14 +157,11 @@ class ChangeEmailContentDialog extends React.Component {
}
ChangeEmailContentDialog.propTypes = {
save: PropTypes.func,
next: PropTypes.func,
cancel: PropTypes.func,
onChange: PropTypes.func,
save: PropTypes.func,
formData: PropTypes.object,
email: PropTypes.string,
hasError: PropTypes.func,
getError: PropTypes.func,
closeDialog: PropTypes.func,
};
export default ChangeEmailContentDialog;
@@ -138,8 +138,8 @@ class Profile extends React.Component {
}
};
saveEmail = async () => {
const { newEmail, confirmPassword } = this.state.formData;
saveEmail = async confirmPassword => {
const { newEmail } = this.state.formData;
try {
await this.props.updateEmailAddress({
@@ -202,12 +202,10 @@ class Profile extends React.Component {
)}
<ChangeEmailContentDialog
save={this.saveEmail}
onChange={this.onChange}
formData={this.state.formData}
email={email}
enable={formData.newEmail && email !== formData.newEmail}
hasError={this.hasError}
getError={this.getError}
closeDialog={this.closeDialog}
/>
</ConfirmChangesDialog>
@@ -225,11 +223,21 @@ class Profile extends React.Component {
disabled={!usernameCanBeUpdated}
columnDisplay
>
<span className={styles.bottomText}>
{t(
'talk-plugin-local-auth.change_username.change_username_note'
<div className={styles.bottomText}>
<span>
{t(
'talk-plugin-local-auth.change_username.change_username_note'
)}
</span>
{!usernameCanBeUpdated && (
<b>
{' '}
{t(
'talk-plugin-local-auth.change_username.is_not_eligible'
)}
</b>
)}
</span>
</div>
</InputField>
<InputField
icon="email"
@@ -19,7 +19,8 @@ en:
changed_password_msg: "Changed Password - Your password has been successfully changed"
forgot_password_sent: "Forgot Password - We sent you an email to recover your password"
change_username:
change_username_note: "Usernames can only be changed once every 14 days. Your username is not currently eligible to be updated."
change_username_note: "Usernames can only be changed once every 14 days."
is_not_eligible: "You cannot currently change your username."
save: "Save"
edit_profile: "Edit Profile"
cancel: "Cancel"
@@ -81,7 +82,8 @@ es:
changed_password_msg: "Contraseña Actualizada - Tu contraseña ha sido exitosamente actualizada"
forgot_password_sent: "Contraseña Olvidada - Te enviamos un email para recuperar tu contraseña"
change_username:
change_username_note: "El usuario puede ser cambiado cada 14 días"
change_username_note: "El usuario puede ser cambiado cada 14 días."
is_not_eligible: "Ahora mismo no se puede cambiar su nombre de usuario."
save: "Guardar"
edit_profile: "Editar Perfil"
cancel: "Cancelar"
+2
View File
@@ -6,6 +6,7 @@ const {
wordlist,
commentLength,
assetClosed,
commentingDisabled,
karma,
staff,
links,
@@ -36,6 +37,7 @@ const applyStatus = status => () => ({ status });
const phases = [
commentLength,
assetClosed,
commentingDisabled,
wordlist,
staff,
links,
@@ -0,0 +1,9 @@
const { ErrCommentingDisabled } = require('../../../errors');
// This phase checks to see if commenting is site-wide disabled.
module.exports = (ctx, comment, { asset }) => {
// Check to see if the asset has closed commenting...
if (asset.settings.disableCommenting) {
throw new ErrCommentingDisabled(asset.settings.disableCommentingMessage);
}
};
+1
View File
@@ -1,6 +1,7 @@
module.exports.wordlist = require('./wordlist');
module.exports.commentLength = require('./commentLength');
module.exports.assetClosed = require('./assetClosed');
module.exports.commentingDisabled = require('./commentingDisabled');
module.exports.karma = require('./karma');
module.exports.staff = require('./staff');
module.exports.links = require('./links');
+7 -3
View File
@@ -1,13 +1,17 @@
const Setting = require('../models/setting');
const { ErrSettingsNotInit } = require('../errors');
const { dotize } = require('./utils');
const { isEmpty, zipObject, uniq } = require('lodash');
const { isEmpty, zipObject } = require('lodash');
const DataLoader = require('dataloader');
const selector = { id: '1' };
async function loadFn(fields = []) {
const model = await Setting.findOne(selector).select(uniq(fields));
async function loadFn(/* fields = [] */) {
// Originally, we used the projection operation, turns out this isn't that
// fast. We should utilize the redis cache instead here.
// const model = await Setting.findOne(selector).select(uniq(fields));
const model = await Setting.findOne(selector);
if (!model) {
throw new ErrSettingsNotInit();
}
@@ -179,6 +179,54 @@ describe('graph.mutations.createComment', () => {
});
});
describe('assets while commenting is disabled', () => {
[
{
disabled: false,
error: null,
},
{
disabled: true,
error: 'COMMENTING_DISABLED',
},
].forEach(({ disabled, error }) => {
describe(`commentingDisabled=${disabled}`, () => {
beforeEach(() =>
AssetModel.create({
id: '123',
settings: { disableCommenting: disabled },
})
);
it(
error ? 'does not create the comment' : 'creates the comment',
() => {
const context = new Context({ user: new UserModel({}) });
return graphql(schema, query, {}, context).then(
({ data, errors }) => {
expect(errors).to.be.undefined;
if (error) {
expect(data.createComment).to.have.property('comment').null;
expect(data.createComment).to.have.property('errors').not
.null;
expect(data.createComment.errors[0]).to.have.property(
'translation_key',
error
);
} else {
expect(data.createComment).to.have.property('comment').not
.null;
expect(data.createComment).to.have.property('errors').null;
}
}
);
}
);
});
});
});
describe('comments made with different asset moderation settings', () => {
[
{ moderation: 'PRE', status: 'PREMOD' },