diff --git a/client/.babelrc b/client/.babelrc index 63b1c53de..60be246eb 100644 --- a/client/.babelrc +++ b/client/.babelrc @@ -11,4 +11,4 @@ "transform-async-to-generator", "transform-react-jsx" ] -} +} \ No newline at end of file diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 8bece5dc5..34f672d8a 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -20,8 +20,9 @@ import {TopRightMenu} from './TopRightMenu'; import CommentContent from './CommentContent'; import Slot from 'coral-framework/components/Slot'; import IgnoredCommentTombstone from './IgnoredCommentTombstone'; +import InactiveCommentLabel from './InactiveCommentLabel'; import {EditableCommentContent} from './EditableCommentContent'; -import {getActionSummary, iPerformedThisAction, forEachError} from 'coral-framework/utils'; +import {getActionSummary, iPerformedThisAction, forEachError, isCommentActive} from 'coral-framework/utils'; import t from 'coral-framework/services/i18n'; const isStaff = (tags) => !tags.every((t) => t.tag.name !== 'STAFF'); @@ -317,7 +318,10 @@ export default class Comment extends React.Component { } = this.props; const view = this.getVisibileReplies(); - const isActive = ['NONE', 'ACCEPTED'].indexOf(comment.status) >= 0; + + // Inactive comments can be viewed by moderators and admins (e.g. using permalinks). + const isActive = isCommentActive(comment.status); + const {loadingState} = this.state; const isPending = comment.id.indexOf('pending') >= 0; const isHighlighted = highlighted === comment.id; @@ -435,7 +439,7 @@ export default class Comment extends React.Component { } } - { (currentUser && (comment.user.id !== currentUser.id)) && + { isActive && (currentUser && (comment.user.id !== currentUser.id)) && /* TopRightMenu allows currentUser to ignore other users' comments */ @@ -445,6 +449,9 @@ export default class Comment extends React.Component { addNotification={addNotification} /> } + { !isActive && + + }
{ diff --git a/client/coral-embed-stream/src/components/InactiveCommentLabel.css b/client/coral-embed-stream/src/components/InactiveCommentLabel.css new file mode 100644 index 000000000..01ba197b5 --- /dev/null +++ b/client/coral-embed-stream/src/components/InactiveCommentLabel.css @@ -0,0 +1,32 @@ +.root { + display: inline-block; + color: white; + background: grey; + height: 22px; + box-sizing: border-box; + line-height: 19px; + padding: 2px 6px 2px 4px; + border-radius: 2px; + font-size: 12px; + text-transform: capitalize; +} + +.icon { + font-size: 14px; + vertical-align: text-top; + margin: 0; + margin-right: 4px; +} + +.label { + display: inline-block; +} + +.premod { + background: #063B9A; +} + +.rejected { + background: #d03235; +} + diff --git a/client/coral-embed-stream/src/components/InactiveCommentLabel.js b/client/coral-embed-stream/src/components/InactiveCommentLabel.js new file mode 100644 index 000000000..824692558 --- /dev/null +++ b/client/coral-embed-stream/src/components/InactiveCommentLabel.js @@ -0,0 +1,36 @@ +import React, {PropTypes} from 'react'; +import t from 'coral-framework/services/i18n'; +import styles from './InactiveCommentLabel.css'; +import {Icon} from 'coral-ui'; +import cn from 'classnames'; + +const InactiveCommentLabel = ({status, className, ...rest}) => { + let label; + let icon; + + switch (status) { + case 'PREMOD': + label = t('modqueue.premod'); + icon = 'query_builder'; + break; + case 'REJECTED': + label = t('modqueue.rejected'); + icon = 'close'; + break; + default: + throw new Error(`Unknown inactive status ${status}`); + } + + return ( + + + {label} + + ); +}; + +InactiveCommentLabel.propTypes = { + status: PropTypes.string.isRequired, +}; + +export default InactiveCommentLabel; diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 66ffc0fc8..421a0cf7a 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -13,6 +13,7 @@ import t, {timeago} from 'coral-framework/services/i18n'; import {getSlotComponents} from 'coral-framework/helpers/plugins'; import CommentBox from 'talk-plugin-commentbox/CommentBox'; import QuestionBox from 'talk-plugin-questionbox/QuestionBox'; +import {isCommentActive} from 'coral-framework/utils'; import {Button, TabBar, Tab, TabCount, TabContent, TabPane} from 'coral-ui'; import cn from 'classnames'; @@ -105,7 +106,9 @@ class Stream extends React.Component { // even though the permalinked comment is the highlighted one, we're displaying its parent + replies let highlightedComment = comment && getTopLevelParent(comment); if (highlightedComment) { - const isInactive = ['NONE', 'ACCEPTED'].indexOf(comment.status) === -1; + + // Inactive comments can be viewed by moderators and admins (e.g. using permalinks). + const isInactive = !isCommentActive(comment.status); if (comment.parent && isInactive) { // the highlighted comment is not active and as such not in the replies, so we diff --git a/client/coral-embed/src/index.js b/client/coral-embed/src/index.js index 64c274660..dd44e72cd 100644 --- a/client/coral-embed/src/index.js +++ b/client/coral-embed/src/index.js @@ -267,6 +267,11 @@ Talk.render = function(el, opts) { console.warn( 'This page does not include a canonical link tag. Talk has inferred this asset_url from the window object. Query params have been stripped, which may cause a single thread to be present across multiple pages.' ); + + if (!window.location.origin) { + window.location.origin = `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}`; + } + query.asset_url = window.location.origin + window.location.pathname; } } diff --git a/client/coral-framework/utils/index.js b/client/coral-framework/utils/index.js index 3768b3940..96d9ed629 100644 --- a/client/coral-framework/utils/index.js +++ b/client/coral-framework/utils/index.js @@ -187,3 +187,7 @@ export function buildUrl({protocol, hostname, port, pathname, search, hash} = wi export function getSlotFragmentSpreads(slots, resource) { return `...${slots.map((s) => `TalkSlot_${capitalize(s)}_${resource}`).join('\n...')}\n`; } + +export function isCommentActive(commentStatus) { + return ['NONE', 'ACCEPTED'].indexOf(commentStatus) >= 0; +} diff --git a/config.js b/config.js index c6ea774a3..02b40681d 100644 --- a/config.js +++ b/config.js @@ -78,6 +78,12 @@ const CONFIG = { // messages through the websocket to keep the socket alive. KEEP_ALIVE: process.env.TALK_KEEP_ALIVE || '30s', + //------------------------------------------------------------------------------ + // Cache configuration + //------------------------------------------------------------------------------ + + CACHE_EXPIRY_COMMENT_COUNT: process.env.TALK_CACHE_EXPIRY_COMMENT_COUNT || '1hr', + //------------------------------------------------------------------------------ // Recaptcha configuration //------------------------------------------------------------------------------ diff --git a/docs/_docs/02-01-configuration.md b/docs/_docs/02-01-configuration.md index ab644e5e3..d2e990375 100644 --- a/docs/_docs/02-01-configuration.md +++ b/docs/_docs/02-01-configuration.md @@ -45,6 +45,7 @@ These are only used during the webpack build. thread. (Default `3`) - `TALK_DEFAULT_STREAM_TAB` (_optional_) - specify the default stream tab in the admin. (Default `all`) +- `TALK_DISABLE_EMBED_POLYFILL` (_optional_) - when set to `TRUE`, the build process will not include the [babel-polyfill](https://babeljs.io/docs/usage/polyfill/) in the embed.js target. (Default `FALSE`) ### Database @@ -127,6 +128,11 @@ The default could be read as: - At the moment of writing, beheviour is not attached to the flagging reliability, but it is recorded. +### Cache + +- `TALK_CACHE_EXPIRY_COMMENT_COUNT` (_optional_) - configure the duration for which + comment counts are cached for. (Default `1hr`) + ### Plugins Plugins configuration can be found on the [Plugins]({{ "/docs/running/plugins/" | absolute_url }}) page. \ No newline at end of file diff --git a/graph/loaders/comments.js b/graph/loaders/comments.js index 16d2101f7..7a17cfabd 100644 --- a/graph/loaders/comments.js +++ b/graph/loaders/comments.js @@ -8,6 +8,10 @@ const { SEARCH_NON_NULL_OR_ACCEPTED_COMMENTS, SEARCH_OTHERS_COMMENTS } = require('../../perms/constants'); +const { + CACHE_EXPIRY_COMMENT_COUNT +} = require('../../config'); +const ms = require('ms'); const CommentModel = require('../../models/comment'); const UsersService = require('../../services/users'); @@ -481,11 +485,11 @@ module.exports = (context) => ({ get: new DataLoader((ids) => getComments(context, ids)), getByQuery: (query) => getCommentsByQuery(context, query), getCountByQuery: (query) => getCommentCountByQuery(context, query), - countByAssetID: new SharedCounterDataLoader('Comments.totalCommentCount', 3600, (ids) => getCountsByAssetID(context, ids)), + countByAssetID: new SharedCounterDataLoader('Comments.totalCommentCount', ms(CACHE_EXPIRY_COMMENT_COUNT), (ids) => getCountsByAssetID(context, ids)), countByAssetIDPersonalized: (query) => getCountsByAssetIDPersonalized(context, query), - parentCountByAssetID: new SharedCounterDataLoader('Comments.countByAssetID', 3600, (ids) => getParentCountsByAssetID(context, ids)), + parentCountByAssetID: new SharedCounterDataLoader('Comments.countByAssetID', ms(CACHE_EXPIRY_COMMENT_COUNT), (ids) => getParentCountsByAssetID(context, ids)), parentCountByAssetIDPersonalized: (query) => getParentCountByAssetIDPersonalized(context, query), - countByParentID: new SharedCounterDataLoader('Comments.countByParentID', 3600, (ids) => getCountsByParentID(context, ids)), + countByParentID: new SharedCounterDataLoader('Comments.countByParentID', ms(CACHE_EXPIRY_COMMENT_COUNT), (ids) => getCountsByParentID(context, ids)), countByParentIDPersonalized: (query) => getCountByParentIDPersonalized(context, query), genRecentReplies: new DataLoader((ids) => genRecentReplies(context, ids)), genRecentComments: new DataLoader((ids) => genRecentComments(context, ids)) diff --git a/webpack.config.js b/webpack.config.js index 1b3c4b763..873d20472 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -33,6 +33,7 @@ const buildEmbeds = [ const config = { devtool: 'cheap-module-source-map', + target: 'web', output: { path: path.join(__dirname, 'dist'), publicPath: '/client/', @@ -153,11 +154,15 @@ if (process.env.NODE_ENV === 'production') { // Applies the base configuration to the following entries. const applyConfig = (entries, root = {}) => _.merge({}, config, { - entry: entries.reduce((entry, {name, path}) => { - entry[name] = [ - 'babel-polyfill', - path - ]; + entry: entries.reduce((entry, {name, path, disablePolyfill = false}) => { + if (disablePolyfill) { + entry[name] = path; + } else { + entry[name] = [ + 'babel-polyfill', + path + ]; + } return entry; }, {}) @@ -171,7 +176,8 @@ module.exports = [ // Load in the root embed. { name: 'embed', - path: path.join(__dirname, 'client/coral-embed/src/index') + path: path.join(__dirname, 'client/coral-embed/src/index'), + disablePolyfill: process.env.TALK_DISABLE_EMBED_POLYFILL === 'TRUE' } ], { @@ -183,7 +189,7 @@ module.exports = [ // All framework targets/embeds/plugins. applyConfig([ - // // Load in all the targets. + // Load in all the targets. ...buildTargets.map((target) => ({ name: `${target}/bundle`, path: path.join(__dirname, 'client/', target, '/src/index')