mirror of
https://github.com/wassname/talk.git
synced 2026-08-12 12:30:39 +08:00
Merge branch 'master' of github.com:coralproject/talk into ignore_rb2
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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'))}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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));
|
||||
}
|
||||
}));
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<%= body %>
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user