mirror of
https://github.com/wassname/talk.git
synced 2026-07-21 12:51:03 +08:00
Merge branch 'master' into party-fixes
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
{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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() });
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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');
|
||||
|
||||
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user