Merge branch 'master' of github.com:coralproject/talk into ignore_rb2

This commit is contained in:
Benjamin Goering
2017-04-13 12:12:10 -05:00
18 changed files with 179 additions and 36 deletions
@@ -14,13 +14,13 @@ const ModerationHeader = props => (
<span>{props.asset.title}</span>
<Icon className={styles.settingsButton} name="open_in_new"/>
</a>
<Link className="mdl-tabs__tab" to="/admin/streams">Select Stream</Link>
<Link className="mdl-tabs__tab" to="/admin/stories">Select Stream</Link>
</div>
:
<div className={`mdl-tabs__tab-bar ${styles.moderateAsset}`}>
<a className="mdl-tabs__tab" />
<a className="mdl-tabs__tab">All Streams</a>
<Link className="mdl-tabs__tab" to="/admin/streams">Select Stream</Link>
<Link className="mdl-tabs__tab" to="/admin/stories">Select Stream</Link>
</div>
}
</div>
@@ -6,7 +6,7 @@ const NotFound = props => (
<div className={`mdl-card mdl-shadow--2dp ${styles.notFound}`}>
<p>
The provided asset id <Link to={`/admin/moderate/${props.assetId}`}>{props.assetId}</Link> does not exist.
<Link className={styles.goToStreams} to="/admin/streams">Go to Streams</Link>
<Link className={styles.goToStreams} to="/admin/stories">Go to Streams</Link>
</p>
</div>
);
+23 -8
View File
@@ -1,4 +1,4 @@
import React, {Component} from 'react';
import React from 'react';
import {compose} from 'react-apollo';
import {connect} from 'react-redux';
import isEqual from 'lodash/isEqual';
@@ -36,15 +36,22 @@ import HighlightedComment from './Comment';
import LoadMore from './LoadMore';
import NewCount from './NewCount';
class Embed extends Component {
class Embed extends React.Component {
state = {activeTab: 0, showSignInDialog: false, activeReplyBox: ''};
constructor(props) {
super(props);
this.state = {
activeTab: 0,
showSignInDialog: false,
activeReplyBox: ''
};
}
changeTab = (tab) => {
const {isAdmin} = this.props.auth;
// Everytime the comes from another tab, the Stream needs to be updated.
if (tab === 0 && isAdmin) {
if (tab === 0) {
this.props.viewAllComments();
this.props.data.refetch();
}
@@ -82,10 +89,12 @@ class Embed extends Component {
if(!isEqual(nextProps.data.asset, this.props.data.asset)) {
loadAsset(nextProps.data.asset);
const {getCounts, updateCountCache} = this.props;
const {getCounts, updateCountCache, asset: {countCache}} = this.props;
const {asset} = nextProps.data;
updateCountCache(asset.id, asset.commentCount);
if (!countCache) {
updateCountCache(asset.id, asset.commentCount);
}
this.setState({
countPoll: setInterval(() => {
@@ -130,6 +139,12 @@ class Embed extends Component {
const banned = user && user.status === 'BANNED';
const hasOlderComments = !!(
asset &&
asset.lastComment &&
asset.lastComment.id !== asset.comments[asset.comments.length - 1].id
);
const expandForLogin = showSignInDialog ? {
minHeight: document.body.scrollHeight + 200
} : {};
@@ -266,7 +281,7 @@ class Embed extends Component {
topLevel={true}
assetId={asset.id}
comments={asset.comments}
moreComments={countCache[asset.id] > asset.comments.length}
moreComments={hasOlderComments}
loadMore={this.props.loadMore} />
</div>
}
+2 -2
View File
@@ -17,10 +17,10 @@ const onLoadMoreClick = ({loadMore, commentCount, firstCommentDate, assetId, upd
const NewCount = (props) => {
const newComments = props.commentCount - props.countCache;
return <div className='coral-new-comments'>
return <div className='coral-new-comments coral-load-more'>
{
props.countCache && newComments > 0 ?
<button onClick={onLoadMoreClick(props)} className='coral-load-more'>
<button onClick={onLoadMoreClick(props)}>
{newComments === 1
? lang.t('newCount', newComments, lang.t('comment'))
: lang.t('newCount', newComments, lang.t('comments'))}
+14 -7
View File
@@ -127,15 +127,15 @@ hr {
margin-bottom: 0px;
font-weight: bold;
font-size: 14px;
display: block;
overflow: hidden;
height: 50px;
min-height: 50px;
display: flex;
}
.coral-plugin-questionbox-icon.bubble{
position: absolute;
top: 11px;
left: 15px;
left: 10px;
color: #949393;
font-size: 20px;
z-index: 0;
@@ -144,7 +144,7 @@ hr {
.coral-plugin-questionbox-icon.person{
z-index: 2;
top: 20px;
left: 20px;
left: 15px;
position: absolute;
font-size: 24px;
color: white;
@@ -159,12 +159,19 @@ hr {
margin-left: 0px !important;
margin-right: 10px;
display: inline-block;
width: 15px;
height: 100%;
padding: 3px 20px;
width: 10px;
min-height: 100%;
padding: 5px 20px;
vertical-align: middle;
}
.coral-plugin-questionbox-content {
padding: 5px;
display: flex;
align-items: center;
justify-content: center;
}
.hidden {
visibility: hidden;
display: none;
@@ -36,6 +36,9 @@ query AssetQuery($asset_id: ID, $asset_url: String, $comment_id: ID!, $has_comme
charCount
requireEmailConfirmation
}
lastComment {
id
}
commentCount(excludeIgnored: $excludeIgnored)
totalCommentCount(excludeIgnored: $excludeIgnored)
comments(limit: 10, excludeIgnored: $excludeIgnored) {
@@ -7,7 +7,9 @@ const QuestionBox = ({enable, content}) =>
<i className={`${packagename}-icon material-icons bubble`}>chat_bubble</i>
<i className={`${packagename}-icon material-icons person`}>person</i>
</div>
{content}
<div className={`${packagename}-content`}>
{content}
</div>
</div>;
export default QuestionBox;
@@ -28,10 +28,10 @@ class FakeComment extends React.Component {
author={{'name': username}}/>
<PubDate created_at={created_at} />
<Content body={body} />
<div className="commentActionsLeft">
<div className="commentActionsLeft comment__action-container">
<div className={`${'coral-plugin-likes'}-container`}>
<button className={`${'coral-plugin-likes'}-button`}>
<span className={`${'coral-plugin-likes'}-button-text`}>{lang.t('like')}</span>
<button className={'coral-plugin-likes-button'}>
<span className={'coral-plugin-likes-button-text'}>{lang.t('like')}</span>
<i className={`${'coral-plugin-likes'}-icon material-icons`}
aria-hidden={true}>thumb_up</i>
</button>
@@ -43,16 +43,16 @@ class FakeComment extends React.Component {
banned={false}
/>
</div>
<div className="commentActionsRight">
<div className="commentActionsRight comment__action-container">
<div className="coral-plugin-permalinks-container">
<button className="coral-plugin-permalinks-button">
<span className={`comment__action-button comment__action-button--nowrap ${'coral-plugin-flags'}-button-text`}>{lang.t('permalink.permalink')}</span>
<i className="coral-plugin-permalinks-icon material-icons" aria-hidden={true}>link</i>
{lang.t('permalink.permalink')}
</button>
</div>
<div className={`${'coral-plugin-flags'}-container`}>
<button className={`${'coral-plugin-flags'}-button`}>
<span className={`${'coral-plugin-flags'}-button-text`}>{lang.t('report')}</span>
<span className={`comment__action-button comment__action-button--nowrap ${'coral-plugin-flags'}-button-text`}>{lang.t('report')}</span>
<i className={`${'coral-plugin-flags'}-icon material-icons`}
aria-hidden={true}>flag</i>
</button>
+2 -2
View File
@@ -25,14 +25,14 @@
"emailInUse": "Email address already in use",
"emailORusernameInUse": "Email address or Username already in use",
"requiredField": "This field is required",
"passwordsDontMatch": "Passwords don\"t match.",
"passwordsDontMatch": "Passwords don't match.",
"specialCharacters": "Usernames can contain letters, numbers and _ only",
"checkTheForm": "Invalid Form. Please, check the fields"
},
"createdisplay": {
"writeyourusername": "Edit your username",
"yourusername": "Your username appears on every comment you post.",
"ifyoudontchangeyourname": "If you don\"t change your username at this step, your Facebook display name will appear alongside of all your comments.",
"ifyoudontchangeyourname": "If you don't change your username at this step, your Facebook display name will appear alongside of all your comments.",
"username": "Username",
"continue": "Continue with the same Facebook username",
"save": "Save",
+8
View File
@@ -3,6 +3,7 @@ const {
GraphQLInterfaceType
} = require('graphql');
const debug = require('debug')('talk:graph:schema');
const Joi = require('joi');
/**
* XXX taken from graphql-js: src/execution/execute.js, because that function
@@ -82,6 +83,8 @@ const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeNa
Object.keys(hooks).forEach((hook) => {
switch (hook) {
case 'pre':
Joi.assert(hooks.pre, Joi.func().maxArity(4));
debug(`adding pre hook to resolver ${typeName}.${fieldName} from plugin '${plugin.name}'`);
if (typeof hooks.pre !== 'function') {
@@ -91,6 +94,8 @@ const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeNa
acc.pre.push(hooks.pre);
break;
case 'post':
Joi.assert(hooks.pre, Joi.func().maxArity(5));
debug(`adding post hook to resolver ${typeName}.${fieldName} from plugin '${plugin.name}'`);
if (typeof hooks.post !== 'function') {
@@ -129,6 +134,9 @@ const decorateWithHooks = (schema, hooks) => forEachField(schema, (field, typeNa
return;
}
// Ensure it matches the format we expect.
Joi.assert(post, Joi.array().items(Joi.func().maxArity(3)), `invalid post hooks were found for ${typeName}.${fieldName}`);
// Cache the original resolverType function.
let resolveType = field.resolveType;
+7
View File
@@ -1,4 +1,11 @@
const Asset = {
lastComment({id}, _, {loaders: {Comments}}) {
return Comments.getByQuery({
asset_id: id,
limit: 1,
parent_id: null
}).then(data => data[0]);
},
recentComments({id}, _, {loaders: {Comments}}) {
return Comments.genRecentComments.load(id);
},
+3
View File
@@ -416,6 +416,9 @@ type Asset {
# The URL that the asset is located on.
url: String
# Returns last comment
lastComment: Comment
# Returns recent comments
recentComments: [Comment]
+6
View File
@@ -46,6 +46,12 @@ const AssetSchema = new Schema({
type: Schema.Types.Mixed,
default: null
},
// Additional metadata stored on the field.
metadata: {
default: {},
type: Object
}
}, {
versionKey: false,
timestamps: {
+7 -1
View File
@@ -75,7 +75,13 @@ const CommentSchema = new Schema({
default: 'NONE'
},
tags: [TagSchema],
parent_id: String
parent_id: String,
// Additional metadata stored on the field.
metadata: {
default: {},
type: Object
}
}, {
timestamps: {
createdAt: 'created_at',
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "talk",
"version": "1.4.0",
"description": "A commenting platform from The Coral Project. https://coralproject.net",
"version": "1.5.0",
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
"main": "app.js",
"scripts": {
"postinstall": "./bin/cli plugins reconcile --skip-remote",
@@ -17,7 +17,7 @@
"e2e": "NODE_ENV=test nightwatch",
"poste2e": "NODE_ENV=test scripts/poste2e.sh",
"embed-start": "NODE_ENV=development yarn build && ./bin/cli serve --jobs",
"heroku-postbuild": "yarn build"
"heroku-postbuild": "./bin/cli plugins reconcile && yarn build"
},
"config": {
"pre-git": {
+8 -3
View File
@@ -47,8 +47,8 @@ const hookSchemas = {
pre: Joi.func(),
post: Joi.func()
}))),
loaders: Joi.object().pattern(/\w/, Joi.object().pattern(/\w/, Joi.func())),
mutators: Joi.object().pattern(/\w/, Joi.object().pattern(/\w/, Joi.func())),
loaders: Joi.func().maxArity(1),
mutators: Joi.func().maxArity(1),
resolvers: Joi.object().pattern(/\w/, Joi.object().pattern(/(?:__resolveType|\w+)/, Joi.func())),
typeDefs: Joi.string()
};
@@ -124,7 +124,12 @@ function itteratePlugins(plugins) {
// Add each plugin folder to the allowed import path so that they can import our
// internal dependancies.
Object.keys(plugins).forEach((type) => itteratePlugins(plugins[type]).forEach((plugin) => {
amp.enableForDir(path.dirname(plugin.path));
// The plugin may be remote, and therefore not installed. We check here if the
// plugin path is available before trying to monkeypatch it's require path.
if (plugin.path) {
amp.enableForDir(path.dirname(plugin.path));
}
}));
/**
+1
View File
@@ -0,0 +1 @@
<%= body %>
+80
View File
@@ -0,0 +1,80 @@
/**
* The key must be composed of alpha characters with periods seperating them.
*/
const KEY_REGEX = /^(?:[A-Za-z][A-Za-z\.]*[A-Za-z])?(?:[A-Za-z]*)$/;
/**
* Allows metadata properties to be set/unset from specific models. It is the
* expecatation of this API that the metadata field is either accessed later
* directly, or accessed as a result of another database load rather than
* this service providing an interface to do so.
*
* @class MetadataService
*/
class MetadataService {
/**
* Parses a key by ensuring that if it is either a string, or an array with
* only characters defined in the `KEY_REGEX`
*
* @static
* @param {String|Array} key
* @returns {String} string form of the key
*
* @memberOf Metadata
*/
static parseKey(key) {
if (Array.isArray(key)) {
key = key.join('.');
}
if ((typeof key !== 'string') || !KEY_REGEX.test(key) || key.length === 0) {
throw new Error(`${key} is not valid, only a-zA-Z. allowed`);
}
return ['metadata', key].join('.');
}
/**
* Sets an object on the metadata field of an object.
*
* @static
* @param {mongoose.Model} model the mongoose model for the object
* @param {String} id the value for the field `id` of the model
* @param {String|Array} key key for the metadata field
* @param {any} value javascript object to set the value of the metadata to
* @returns {Promise} resolves when the update is complete
*
* @memberOf Metadata
*/
static async set(model, id, key, value) {
key = MetadataService.parseKey(key);
return model.update({id}, {
$set: {
[key]: value
}
});
}
/**
* Removes the value for the metadata field as the specific key.
*
* @static
* @param {mongoose.Model} model the mongoose model for the object
* @param {String} id the value for the field `id` of the model
* @param {String|Array} key key for the metadata field
* @returns
*
* @memberOf Metadata
*/
static async unset(model, id, key) {
key = MetadataService.parseKey(key);
return model.update({id}, {
$unset: {[key]: ''}
});
}
}
module.exports = MetadataService;