diff --git a/app.js b/app.js index 868ceee4c..16840f965 100644 --- a/app.js +++ b/app.js @@ -43,8 +43,15 @@ if (process.env.NODE_ENV === 'production') { app.get('*.js', (req, res, next) => { const accept = accepts(req); if (accept.encoding(['gzip']) === 'gzip') { - req.url = `${req.url}.gz`; + + // Adjsut the headers on the request by adding a content type header + // because express won't be able to detect the mime-type with the .gz + // extension and we need to decalre support for the gzip encoding. + res.set('Content-Type', 'application/javascript'); res.set('Content-Encoding', 'gzip'); + + // Rewrite the url so that the gzip version will be served instead. + req.url = `${req.url}.gz`; } next(); diff --git a/client/coral-admin/src/containers/Configure/Configure.css b/client/coral-admin/src/containers/Configure/Configure.css index 04ef96dd2..3d5750693 100644 --- a/client/coral-admin/src/containers/Configure/Configure.css +++ b/client/coral-admin/src/containers/Configure/Configure.css @@ -96,24 +96,27 @@ } } +.inlineTextfield { + border-color: #ccc; + border-style: solid; + border-width: 0px 0px 1px 0px; + text-align: center; + font-size: inherit; +} + +.inlineTextfield:focus { + outline: none; +} + .charCountTexfield { width: 4em; padding: 0px; - border-color: #ccc; - border-style: solid; - border-width: 0px 0px 1px 0px; - font-size: 14px; - text-align: center; } .charCountTexfieldEnabled { border-color: #00796b; } -.charCountTexfield:focus { - outline: none; -} - .changedSave { background-color: #00796B; color: white; diff --git a/client/coral-admin/src/containers/Configure/ModerationSettings.js b/client/coral-admin/src/containers/Configure/ModerationSettings.js index 5b7286b98..c7df573e8 100644 --- a/client/coral-admin/src/containers/Configure/ModerationSettings.js +++ b/client/coral-admin/src/containers/Configure/ModerationSettings.js @@ -27,6 +27,12 @@ const ModerationSettings = ({settings, updateSettings, onChangeWordlist}) => { const on = styles.enabledSetting; const off = styles.disabledSetting; + const onChangeEditCommentWindowLength = (e) => { + const value = e.target.value; + const valueAsNumber = parseFloat(value); + const milliseconds = (!isNaN(valueAsNumber)) && (valueAsNumber * 1000); + updateSettings({editCommentWindowLength: milliseconds || value}); + }; return (
@@ -72,6 +78,27 @@ const ModerationSettings = ({settings, updateSettings, onChangeWordlist}) => { bannedWords={settings.wordlist.banned} suspectWords={settings.wordlist.suspect} onChangeWordlist={onChangeWordlist} /> + + {/* Edit Comment Timeframe */} + +
{lang.t('configure.edit-comment-timeframe-heading')}
+

+ {lang.t('configure.edit-comment-timeframe-text-pre')} +   + +   + {lang.t('configure.edit-comment-timeframe-text-post')} +

+
); }; diff --git a/client/coral-admin/src/containers/Configure/StreamSettings.js b/client/coral-admin/src/containers/Configure/StreamSettings.js index 646eb9d9e..341c47ae9 100644 --- a/client/coral-admin/src/containers/Configure/StreamSettings.js +++ b/client/coral-admin/src/containers/Configure/StreamSettings.js @@ -81,7 +81,7 @@ const StreamSettings = ({updateSettings, settingsError, settings, errors}) => {

{lang.t('configure.comment-count-text-pre')}

{user.username}

- {profile && this.profile = ref} value={profile} />} + {profile && this.profile = ref} value={profile} />}

Member since {new Date(user.created_at).toLocaleString()}


diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index 023270f14..2cb228dd4 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -106,6 +106,9 @@ "include-text": "Include your text here.", "enable-premod-links": "Pre-Moderate Comments Containing Links", "enable-premod-links-text": "Moderators must approve any comment containing a link before its published.", + "edit-comment-timeframe-heading": "Edit Comment Timeframe", + "edit-comment-timeframe-text-pre": "Commenters will have", + "edit-comment-timeframe-text-post": "seconds to edit their comments.", "comment-settings": "Settings", "embed-comment-stream": "Embed Stream", "banned-word-text": "Comments which contain these words or phrases (not case-sensitive) will be automatically removed from the comment stream. Type a word and press Enter or Tab to add. Optionally paste a comma-separated list.", @@ -310,6 +313,9 @@ "embed-comment-stream": "Colocar Hilo de Comentarios", "enable-premod-links": "Pre-Moderar Commentarios que contienen Enlaces", "enable-premod-links-text": "Los y las Moderadoras deben aprobar cualquier comentario que contengan links antes de su publicación.", + "edit-comment-timeframe-heading": "Editar Tiempo de Comentario", + "edit-comment-timeframe-text-pre": "Los comentaristas tendrán", + "edit-comment-timeframe-text-post": "segundos para editar sus comentarios.", "wordlist": "Palabras Suspendidas y Sospechosas", "banned-word-text": "Comentarios que contengan estas palabras o frases, no separadas por comas y en mayusculas o minusuculas, serán automaticamente marcadas para separar los comentarios publicados.", "suspect-word-text": "Comentarios que contengan estas palabras o frases, considerando mayusculas y minusculas, serán automaticamente destacadas en los comentarios publicados. Escribir una palabra y apretar Enter o Tabulador para agergarla. Opcionalmente pegar una lista separada por coma.", diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 89723f0c0..93bc516c4 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -30,7 +30,7 @@ class Stream extends React.Component { render() { const { - root: {asset, asset: {comments}, comment, myIgnoredUsers}, + root: {asset, asset: {comments}, comment, me}, postComment, addNotification, postFlag, @@ -64,8 +64,9 @@ class Stream extends React.Component { const firstCommentDate = asset.comments[0] ? asset.comments[0].created_at : new Date(Date.now() - 1000 * 60 * 60 * 24 * 7).toISOString(); - const commentIsIgnored = (comment) => - myIgnoredUsers && myIgnoredUsers.includes(comment.user.id); + const commentIsIgnored = (comment) => { + return me && me.ignoredUsers && me.ignoredUsers.find((u) => u.id === comment.user.id); + }; return (
{open @@ -162,8 +163,8 @@ class Stream extends React.Component { />
{comments.map( - (comment) => - (commentIsIgnored(comment) + (comment) => { + return (commentIsIgnored(comment) ? : ) + /> + ); + } )}
{ + return this.props.setCommentCountCache(commentCount); + }); } this.countPoll = setInterval(() => { this.getCounts(this.props.data.variables); @@ -205,6 +208,9 @@ const fragments = { } me { status + ignoredUsers { + id + } } settings { organizationName diff --git a/client/coral-embed-stream/src/graphql/index.js b/client/coral-embed-stream/src/graphql/index.js index 5ab1f0c37..ccf6195f6 100644 --- a/client/coral-embed-stream/src/graphql/index.js +++ b/client/coral-embed-stream/src/graphql/index.js @@ -122,19 +122,40 @@ const extension = { `, }, mutations: { - IgnoreUser: () => ({ - - // TODO: don't rely on refetching. - refetchQueries: [ - 'EmbedQuery', 'EmbedStreamProfileQuery', - ], + IgnoreUser: ({variables}) => ({ + updateQueries: { + EmbedQuery: (previousData, {mutationResult}) => { + const ignoredUserId = variables.id; + const response = mutationResult.data.ignoreUser; + if (ignoredUserId && !response.errors) { + const updated = update(previousData, {me: {ignoredUsers: {$push: [{ + id: ignoredUserId, + __typename: 'User', + }]}}}); + return updated; + } + return previousData; + } + } }), - StopIgnoringUser: () => ({ + StopIgnoringUser: ({variables}) => ({ + updateQueries: { + EmbedStreamProfileQuery: (previousData, {mutationResult}) => { + const noLongerIgnoredUserId = variables.id; + const response = mutationResult.data.stopIgnoringUser; + if (noLongerIgnoredUserId && !response.errors) { - // TODO: don't rely on refetching. - refetchQueries: [ - 'EmbedQuery', 'EmbedStreamProfileQuery', - ], + // remove noLongerIgnoredUserId from ignoredUsers + const updated = update(previousData, {me: {ignoredUsers: { + $apply: (ignoredUsers) => { + return ignoredUsers.filter((u) => u.id !== noLongerIgnoredUserId); + } + }}}); + return updated; + } + return previousData; + } + } }), PostComment: ({ variables: {comment: {asset_id, body, parent_id, tags = []}}, @@ -200,7 +221,13 @@ const extension = { variables: {id, edit}, }) => ({ updateQueries: { - EmbedQuery: (previousData, {mutationResult: {data: {editComment: {comment: {status}}}}}) => { + EmbedQuery: (previousData, {mutationResult: {data: {editComment: {comment, errors}}}}) => { + + // @TODO (kiwi) revisit after streamlining error handling + if (errors && errors.length) { + return previousData; + } + const {status} = comment; const updateCommentWithEdit = (comment, edit) => { const {body} = edit; const editedComment = update(comment, { diff --git a/client/coral-framework/reducers/user.js b/client/coral-framework/reducers/user.js index 2969665e3..bc40cf3ae 100644 --- a/client/coral-framework/reducers/user.js +++ b/client/coral-framework/reducers/user.js @@ -1,4 +1,4 @@ -import {Map, Set} from 'immutable'; +import {Map} from 'immutable'; import * as authActions from '../constants/auth'; import * as actions from '../constants/user'; import * as assetActions from '../constants/assets'; @@ -9,7 +9,6 @@ const initialState = Map({ settings: {}, myComments: [], myAssets: [], // the assets from which myComments (above) originated - ignoredUsers: Set(), }); const purge = (user) => { @@ -39,14 +38,6 @@ export default function user (state = initialState, action) { return state.set('myAssets', action.assets); case actions.LOGOUT_SUCCESS: return initialState; - case 'APOLLO_MUTATION_RESULT': - switch (action.operationName) { - case 'ignoreUser': - return state.updateIn(['ignoredUsers'], (i) => i.add(action.variables.id)); - case 'stopIgnoringUser': - return state.updateIn(['ignoredUsers'], (i) => i.delete(action.variables.id)); - } - break; } return state; } diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js index 8074abe67..7273bf58a 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -48,10 +48,12 @@ const Comment = { asset({asset_id}, _, {loaders: {Assets}}) { return Assets.getByID.load(asset_id); }, - editing(comment) { + async editing(comment, _, {loaders: {Settings}}) { + const settings = await Settings.load(); + const editableUntil = new Date(Number(comment.created_at) + settings.editCommentWindowLength); return { edited: comment.edited, - editableUntil: comment.editableUntil + editableUntil: editableUntil }; } }; diff --git a/models/comment.js b/models/comment.js index ddd39c68e..27a2bfae3 100644 --- a/models/comment.js +++ b/models/comment.js @@ -2,8 +2,6 @@ const mongoose = require('../services/mongoose'); const Schema = mongoose.Schema; const uuid = require('uuid'); -const EDIT_WINDOW_MS = 30 * 1000; // 30 seconds - const STATUSES = [ 'ACCEPTED', 'REJECTED', @@ -113,12 +111,7 @@ CommentSchema.virtual('edited').get(function() { return this.body_history.length > 1; }); -CommentSchema.virtual('editableUntil').get(function() { - return new Date(Number(this.created_at) + EDIT_WINDOW_MS); -}); - // Comment model. const Comment = mongoose.model('Comment', CommentSchema); module.exports = Comment; -module.exports.EDIT_WINDOW_MS = EDIT_WINDOW_MS; diff --git a/models/setting.js b/models/setting.js index e63fa8f3e..ecd9dfd26 100644 --- a/models/setting.js +++ b/models/setting.js @@ -88,6 +88,13 @@ const SettingSchema = new Schema({ type: Array, default: ['localhost'] } + }, + + // Length of time (in milliseconds) after a comment is posted that it can still be edited by the author + editCommentWindowLength: { + type: Number, + min: [0, 'Edit Comment Window length must be greater than zero'], + default: 30 * 1000, } }, { timestamps: { diff --git a/services/comments.js b/services/comments.js index 272b19cd0..def557c96 100644 --- a/services/comments.js +++ b/services/comments.js @@ -1,8 +1,8 @@ const CommentModel = require('../models/comment'); -const EDIT_WINDOW_MS = CommentModel.EDIT_WINDOW_MS; const ActionModel = require('../models/action'); const ActionsService = require('./actions'); +const SettingsService = require('./settings'); const errors = require('../errors'); @@ -53,8 +53,10 @@ module.exports = class CommentsService { // Establish the edit window (if it exists) and add the condition to the // original query. - const lastEditableCommentCreatedAt = new Date((new Date()).getTime() - EDIT_WINDOW_MS); + let lastEditableCommentCreatedAt; if (!ignoreEditWindow) { + const {editCommentWindowLength: editWindowMs} = await SettingsService.retrieve(); + lastEditableCommentCreatedAt = new Date((new Date()).getTime() - editWindowMs); query.created_at = { $gt: lastEditableCommentCreatedAt, }; diff --git a/services/users.js b/services/users.js index 2608e24b0..8c193cd5c 100644 --- a/services/users.js +++ b/services/users.js @@ -903,6 +903,5 @@ module.exports = class UsersService { ignoresUsers: usersToStopIgnoring } }); - console.log('Mongo wrote stopIgnoringUsers', usersToStopIgnoring); } };