mirror of
https://github.com/wassname/talk.git
synced 2026-07-17 11:33:39 +08:00
Merge branch 'master' into bump-version-2-0-0
This commit is contained in:
+108
-48
@@ -8,9 +8,12 @@ const program = require('./commander');
|
||||
const parseDuration = require('ms');
|
||||
const Table = require('cli-table');
|
||||
const AssetModel = require('../models/asset');
|
||||
const CommentModel = require('../models/comment');
|
||||
const AssetsService = require('../services/assets');
|
||||
const mongoose = require('../services/mongoose');
|
||||
const scraper = require('../services/scraper');
|
||||
const util = require('./util');
|
||||
const inquirer = require('inquirer');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([
|
||||
@@ -20,65 +23,112 @@ util.onshutdown([
|
||||
/**
|
||||
* Lists all the assets registered in the database.
|
||||
*/
|
||||
function listAssets() {
|
||||
AssetModel
|
||||
.find({})
|
||||
.sort({'created_at': 1})
|
||||
.then((asset) => {
|
||||
let table = new Table({
|
||||
head: [
|
||||
'ID',
|
||||
'Title',
|
||||
'URL'
|
||||
]
|
||||
});
|
||||
async function listAssets() {
|
||||
try {
|
||||
let assets = await AssetModel.find({}).sort({'created_at': 1});
|
||||
|
||||
asset.forEach((asset) => {
|
||||
table.push([
|
||||
asset.id,
|
||||
asset.title ? asset.title : '',
|
||||
asset.url ? asset.url : ''
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown(1);
|
||||
let table = new Table({
|
||||
head: [
|
||||
'ID',
|
||||
'Title',
|
||||
'URL'
|
||||
]
|
||||
});
|
||||
|
||||
assets.forEach((asset) => {
|
||||
table.push([
|
||||
asset.id,
|
||||
asset.title ? asset.title : '',
|
||||
asset.url ? asset.url : ''
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(table.toString());
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshAssets(ageString) {
|
||||
const now = new Date().getTime();
|
||||
const ageMs = parseDuration(ageString);
|
||||
const age = new Date(now - ageMs);
|
||||
async function refreshAssets(ageString) {
|
||||
try {
|
||||
const now = new Date().getTime();
|
||||
const ageMs = parseDuration(ageString);
|
||||
const age = new Date(now - ageMs);
|
||||
|
||||
AssetModel.find({
|
||||
$or: [
|
||||
{
|
||||
scraped: {
|
||||
$lte: age
|
||||
let assets = await AssetModel.find({
|
||||
$or: [
|
||||
{
|
||||
scraped: {
|
||||
$lte: age
|
||||
}
|
||||
},
|
||||
{
|
||||
scraped: null
|
||||
}
|
||||
},
|
||||
{
|
||||
scraped: null
|
||||
}
|
||||
]
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
// Queue all the assets for scraping.
|
||||
.then((assets) => Promise.all(assets.map(scraper.create)))
|
||||
// Queue all the assets for scraping.
|
||||
await Promise.all(assets.map(scraper.create));
|
||||
|
||||
.then(() => {
|
||||
console.log('Assets were queued to be scraped');
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function updateURL(assetID, assetURL) {
|
||||
try {
|
||||
await AssetsService.updateURL(assetID, assetURL);
|
||||
|
||||
console.log(`Asset ${assetID} was updated to have url ${assetURL}.`);
|
||||
util.shutdown();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function merge(srcID, dstID) {
|
||||
try {
|
||||
|
||||
// Grab the assets...
|
||||
let [srcAsset, dstAsset] = await AssetsService.findByIDs([srcID, dstID]);
|
||||
if (!srcAsset || !dstAsset) {
|
||||
throw new Error('Not all assets indicated by id exist, cannot merge');
|
||||
}
|
||||
|
||||
// Count the affected resources...
|
||||
let srcCommentCount = await CommentModel.find({asset_id: srcID}).count();
|
||||
|
||||
console.log(`Now going to update ${srcCommentCount} comments and delete the source Asset[${srcID}].`);
|
||||
|
||||
let {confirm} = await inquirer.prompt([
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'confirm',
|
||||
message: 'Proceed with merge',
|
||||
default: false
|
||||
}
|
||||
]);
|
||||
|
||||
if (confirm) {
|
||||
|
||||
// Perform the merge!
|
||||
await AssetsService.merge(srcID, dstID);
|
||||
} else {
|
||||
console.warn('Aborting merge');
|
||||
}
|
||||
|
||||
util.shutdown(0);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
util.shutdown(1);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
@@ -95,6 +145,16 @@ program
|
||||
.description('queues the assets that exceed the age requested')
|
||||
.action(refreshAssets);
|
||||
|
||||
program
|
||||
.command('update-url <assetID> <url>')
|
||||
.description('update the URL of an asset')
|
||||
.action(updateURL);
|
||||
|
||||
program
|
||||
.command('merge <srcID> <dstID>')
|
||||
.description('merges two assets together by moving comments from src to dst and deleting the src asset')
|
||||
.action(merge);
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
// If there is no command listed, output help.
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
letter-spacing: .8;
|
||||
|
||||
&:hover {
|
||||
background-color: #232323;
|
||||
background-color: #404040;
|
||||
}
|
||||
|
||||
&.active {
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
right: 0;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
padding: 40px 18px;
|
||||
padding: 50px 18px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -310,24 +310,33 @@
|
||||
.flaggedByCount {
|
||||
display: block;
|
||||
text-align: left;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.flaggedByCount i {
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.flaggedBy {
|
||||
display: inline;
|
||||
padding: 3px;
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.flaggedByLabel {
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.flaggedReasons {
|
||||
padding-top: 15px;
|
||||
margin-left: 24px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.flaggedByReason {
|
||||
p.flaggedByReason {
|
||||
font-size: 1tpx;
|
||||
margin: 0px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,10 @@ span {
|
||||
}
|
||||
}
|
||||
|
||||
.approve {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.notFound {
|
||||
position: relative;
|
||||
margin: 20px auto;
|
||||
@@ -193,7 +197,7 @@ span {
|
||||
top: 0;
|
||||
padding: 40px 18px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.itemHeader {
|
||||
display: flex;
|
||||
@@ -255,6 +259,8 @@ span {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.empty {
|
||||
color: #444;
|
||||
margin-top: 50px;
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
.heading {
|
||||
margin: 0;
|
||||
padding-left: 10px;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
color: #2c2c2c;
|
||||
}
|
||||
|
||||
.widgetTable {
|
||||
@@ -32,7 +33,8 @@
|
||||
}
|
||||
|
||||
.widgetHead p {
|
||||
color: rgb(35, 102, 223);
|
||||
color: #2c2c2c;
|
||||
font-weight: 500;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
text-transform: capitalize;
|
||||
@@ -47,11 +49,11 @@
|
||||
}
|
||||
|
||||
.rowLinkify {
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid lightgrey;
|
||||
color: #555;
|
||||
height: var(--row-height);
|
||||
padding: 10px;
|
||||
transition: background-color 200ms;
|
||||
}
|
||||
|
||||
.rowLinkify:last-child {
|
||||
@@ -60,6 +62,7 @@
|
||||
|
||||
.rowLinkify:hover {
|
||||
background-color: #f8f8f8;
|
||||
pointer: default;
|
||||
}
|
||||
|
||||
.linkToAsset {
|
||||
@@ -68,16 +71,17 @@
|
||||
}
|
||||
|
||||
.linkToModerate {
|
||||
background-color: #e0e0e0;
|
||||
background-color: #BDBDBD;
|
||||
padding: 10px 14px;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
float: right;
|
||||
margin-left: 15px;
|
||||
transition: background-color 200ms;
|
||||
}
|
||||
|
||||
.linkToModerate:hover {
|
||||
background-color: #ccc;
|
||||
background-color: #9E9E9E;
|
||||
}
|
||||
|
||||
.lede {
|
||||
@@ -89,14 +93,10 @@
|
||||
color: #555;
|
||||
text-decoration: none;
|
||||
font-size: 1.2em;
|
||||
font-weight: normal;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.assetTitle:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.widgetCount {
|
||||
color: #555;
|
||||
font-size: 1.3em;
|
||||
|
||||
@@ -12,4 +12,5 @@
|
||||
right: 0;
|
||||
margin-top: -2px;
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
display: flex;
|
||||
|
||||
.stat {
|
||||
margin: 0 4px 12px;
|
||||
margin: 0 4px 10px 0px;
|
||||
}
|
||||
|
||||
.stat:last-child {
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
@@ -18,14 +18,20 @@
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
color: #EEEEEE;
|
||||
color: #C0C0C0;
|
||||
text-transform: capitalize;
|
||||
font-weight: 100;
|
||||
font-size: 15px;
|
||||
font-size: 14px;
|
||||
letter-spacing: 1px;
|
||||
transition: border-bottom 200ms;
|
||||
padding: 0px 5px;
|
||||
margin-right: 30px;
|
||||
transition: color 200ms;
|
||||
padding: 0px 10px;
|
||||
margin-right: 20px;
|
||||
&:hover {
|
||||
color: white;
|
||||
border-bottom: solid 2px #F36451;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
@@ -33,6 +39,10 @@
|
||||
box-sizing: border-box;
|
||||
border-bottom: solid 4px #F36451;
|
||||
font-weight: 400;
|
||||
&:hover {
|
||||
border-bottom: solid 4px #F36451;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.active > span {
|
||||
@@ -103,19 +113,18 @@ span {
|
||||
font-weight: 400;
|
||||
font-size: 15px;
|
||||
letter-spacing: 1px;
|
||||
transition: opacity 200ms;
|
||||
transition: background-color 200ms;
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
opacity: .8;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: #212121;
|
||||
}
|
||||
span {
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
@@ -245,7 +254,6 @@ span {
|
||||
padding: 5px;
|
||||
color: #262626;
|
||||
font-size: 14px;
|
||||
margin-left: 15px;
|
||||
line-height: 1px;
|
||||
font-weight: 300;
|
||||
}
|
||||
@@ -421,17 +429,21 @@ span {
|
||||
|
||||
.tabIcon {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
top: 3px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.username {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
color: #393B44;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
|
||||
font-weight: 600;
|
||||
padding: 2px 5px;
|
||||
border-radius: 2px;
|
||||
margin-left: -5px;
|
||||
transition: background-color 200ms ease;
|
||||
&:hover {
|
||||
background-color: rgba(255, 0, 0, .1);
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -311,11 +311,11 @@ export const checkLogin = () => (dispatch) => {
|
||||
throw new Error('Not logged in');
|
||||
}
|
||||
|
||||
dispatch(checkLoginSuccess(result.user));
|
||||
|
||||
// Reset the websocket.
|
||||
resetWebsocket();
|
||||
|
||||
dispatch(checkLoginSuccess(result.user));
|
||||
|
||||
// Display create username dialog if necessary.
|
||||
if (result.user.canEditName && result.user.status !== 'BANNED') {
|
||||
dispatch(showCreateUsernameDialog());
|
||||
|
||||
@@ -7,7 +7,7 @@ export default {
|
||||
'SuspendUserResponse',
|
||||
'RejectUsernameResponse',
|
||||
'SetUserStatusResponse',
|
||||
'PostCommentResponse',
|
||||
'CreateFlagResponse',
|
||||
'EditCommentResponse',
|
||||
'PostFlagResponse',
|
||||
'CreateDontAgreeResponse',
|
||||
@@ -17,3 +17,4 @@ export default {
|
||||
'StopIgnoringUserResponse',
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export {default as withFragments} from './withFragments';
|
||||
export {default as withMutation} from './withMutation';
|
||||
export {default as withQuery} from './withQuery';
|
||||
export {default as withReaction} from './withReaction';
|
||||
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import uuid from 'uuid/v4';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {getDisplayName} from '../helpers/hoc';
|
||||
import {compose, gql, graphql} from 'react-apollo';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
import {showSignInDialog} from 'coral-framework/actions/auth';
|
||||
import {capitalize} from 'coral-framework/helpers/strings';
|
||||
import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils';
|
||||
|
||||
export default (reaction) => (WrappedComponent) => {
|
||||
if (typeof reaction !== 'string') {
|
||||
console.error('Reaction must be a valid string');
|
||||
return null;
|
||||
}
|
||||
|
||||
reaction = reaction.toLowerCase();
|
||||
|
||||
class WithReactions extends React.Component {
|
||||
render() {
|
||||
const {comment} = this.props;
|
||||
|
||||
const reactionSummary = getMyActionSummary(
|
||||
`${capitalize(reaction)}ActionSummary`,
|
||||
comment
|
||||
);
|
||||
|
||||
const count = getTotalActionCount(
|
||||
`${capitalize(reaction)}ActionSummary`,
|
||||
comment
|
||||
);
|
||||
|
||||
const alreadyReacted = () => !!reactionSummary;
|
||||
|
||||
const withReactionProps = {reactionSummary, count, alreadyReacted};
|
||||
|
||||
return <WrappedComponent {...this.props} {...withReactionProps} />;
|
||||
}
|
||||
}
|
||||
|
||||
const isReaction = (a) =>
|
||||
a.__typename === `${capitalize(reaction)}ActionSummary`;
|
||||
|
||||
const COMMENT_FRAGMENT = gql`
|
||||
fragment ${capitalize(reaction)}Button_updateFragment on Comment {
|
||||
action_summaries {
|
||||
... on ${capitalize(reaction)}ActionSummary {
|
||||
count
|
||||
current_user {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const withDeleteReaction = graphql(
|
||||
gql`
|
||||
mutation deleteReaction($id: ID!) {
|
||||
deleteAction(id:$id) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
props: ({mutate, ownProps}) => ({
|
||||
deleteReaction: () => {
|
||||
|
||||
const reactionSummary = getMyActionSummary(
|
||||
`${capitalize(reaction)}ActionSummary`,
|
||||
ownProps.comment
|
||||
);
|
||||
|
||||
const reactionData = {
|
||||
id: reactionSummary.current_user.id,
|
||||
commentId: ownProps.comment.id
|
||||
};
|
||||
|
||||
return mutate({
|
||||
variables: {id: reactionData.id},
|
||||
optimisticResponse: {
|
||||
deleteAction: {
|
||||
__typename: 'DeleteActionResponse',
|
||||
errors: null
|
||||
}
|
||||
},
|
||||
update: (proxy) => {
|
||||
const fragmentId = `Comment_${reactionData.commentId}`;
|
||||
|
||||
// Read the data from our cache for this query.
|
||||
const data = proxy.readFragment({
|
||||
fragment: COMMENT_FRAGMENT,
|
||||
id: fragmentId
|
||||
});
|
||||
|
||||
// Check whether we liked this comment.
|
||||
const idx = data.action_summaries.findIndex(isReaction);
|
||||
if (
|
||||
idx < 0 ||
|
||||
get(data.action_summaries[idx], 'current_user.id') !== reactionData.id
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
data.action_summaries[idx] = {
|
||||
...data.action_summaries[idx],
|
||||
count: data.action_summaries[idx].count - 1,
|
||||
current_user: null
|
||||
};
|
||||
|
||||
// Write our data back to the cache.
|
||||
proxy.writeFragment({
|
||||
fragment: COMMENT_FRAGMENT,
|
||||
id: fragmentId,
|
||||
data
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
const withPostReaction = graphql(
|
||||
gql`
|
||||
mutation create${capitalize(reaction)}($${reaction}: Create${capitalize(reaction)}Input!) {
|
||||
create${capitalize(reaction)}(${reaction}: $${reaction}) {
|
||||
${reaction} {
|
||||
id
|
||||
}
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
props: ({mutate, ownProps}) => ({
|
||||
postReaction: () => {
|
||||
|
||||
const reactionData = {
|
||||
item_id: ownProps.comment.id,
|
||||
item_type: 'COMMENTS'
|
||||
};
|
||||
|
||||
return mutate({
|
||||
variables: {[reaction]: reactionData},
|
||||
optimisticResponse: {
|
||||
[`create${capitalize(reaction)}`]: {
|
||||
__typename: `Create${capitalize(reaction)}Response`,
|
||||
errors: null,
|
||||
[reaction]: {
|
||||
__typename: `${capitalize(reaction)}Action`,
|
||||
id: uuid()
|
||||
}
|
||||
}
|
||||
},
|
||||
update: (proxy, mutationResult) => {
|
||||
const fragmentId = `Comment_${reactionData.item_id}`;
|
||||
|
||||
// Read the data from our cache for this query.
|
||||
const data = proxy.readFragment({
|
||||
fragment: COMMENT_FRAGMENT,
|
||||
id: fragmentId
|
||||
});
|
||||
|
||||
// Add our comment from the mutation to the end.
|
||||
let idx = data.action_summaries.findIndex(isReaction);
|
||||
|
||||
// Check whether we already reactioned this comment.
|
||||
if (idx >= 0 && data.action_summaries[idx].current_user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (idx < 0) {
|
||||
|
||||
// Add initial action when it doesn't exist.
|
||||
data.action_summaries.push({
|
||||
__typename: `${capitalize(reaction)}ActionSummary`,
|
||||
count: 0,
|
||||
current_user: null
|
||||
});
|
||||
idx = data.action_summaries.length - 1;
|
||||
}
|
||||
|
||||
data.action_summaries[idx] = {
|
||||
...data.action_summaries[idx],
|
||||
count: data.action_summaries[idx].count + 1,
|
||||
current_user: mutationResult.data[
|
||||
`create${capitalize(reaction)}`
|
||||
][reaction]
|
||||
};
|
||||
|
||||
// Write our data back to the cache.
|
||||
proxy.writeFragment({
|
||||
fragment: COMMENT_FRAGMENT,
|
||||
id: fragmentId,
|
||||
data
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
user: state.auth.toJS().user,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({showSignInDialog}, dispatch);
|
||||
|
||||
const enhance = compose(
|
||||
withFragments({
|
||||
comment: gql`
|
||||
fragment ${capitalize(reaction)}Button_comment on Comment {
|
||||
action_summaries {
|
||||
... on ${capitalize(reaction)}ActionSummary {
|
||||
count
|
||||
current_user {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withDeleteReaction,
|
||||
withPostReaction
|
||||
);
|
||||
|
||||
WithReactions.displayName = `WithReactions(${getDisplayName(WrappedComponent)})`;
|
||||
|
||||
return enhance(WithReactions);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import ApolloClient, {addTypename} from 'apollo-client';
|
||||
import {networkInterface} from './transport';
|
||||
import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws';
|
||||
import {SUBSCRIPTION_END} from 'subscriptions-transport-ws/dist/messageTypes';
|
||||
import MessageTypes from 'subscriptions-transport-ws/dist/message-types';
|
||||
import {getAuthToken} from '../helpers/request';
|
||||
|
||||
let client, wsClient = null, wsClientToken = null;
|
||||
@@ -13,18 +13,17 @@ export function resetWebsocket() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unsubscribe from all the active subscriptions.
|
||||
Object.keys(wsClient.subscriptions).forEach((id) => {
|
||||
|
||||
// Create the message.
|
||||
let message = {id: parseInt(id), type: SUBSCRIPTION_END};
|
||||
|
||||
// Send the unsubscribe message.
|
||||
wsClient.client.send(JSON.stringify(message));
|
||||
});
|
||||
|
||||
// Close the client, this will trigger a reconnect.
|
||||
// Close socket connection which will also unregister subscriptions on the server-side.
|
||||
wsClient.close();
|
||||
|
||||
// Reconnect to the server.
|
||||
wsClient.connect();
|
||||
|
||||
// Reregister all subscriptions (uses non public api).
|
||||
// See: https://github.com/apollographql/subscriptions-transport-ws/issues/171
|
||||
Object.keys(wsClient.operations).forEach((id) => {
|
||||
wsClient.sendMessage(id, MessageTypes.GQL_START, wsClient.operations[id].options);
|
||||
});
|
||||
}
|
||||
|
||||
export function getClient() {
|
||||
@@ -35,6 +34,7 @@ export function getClient() {
|
||||
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
wsClient = new SubscriptionClient(`${protocol}://${location.host}/api/v1/live`, {
|
||||
reconnect: true,
|
||||
lazy: true,
|
||||
connectionParams: {
|
||||
get token() {
|
||||
|
||||
|
||||
@@ -104,6 +104,20 @@ class ErrAuthentication extends APIError {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ErrAlreadyExists is returned when an attempt to create a resource failed due to an existing one.
|
||||
*/
|
||||
class ErrAlreadyExists extends APIError {
|
||||
constructor(existing = null) {
|
||||
super('resource already exists', {
|
||||
translation_key: 'ALREADY_EXISTS',
|
||||
status: 409
|
||||
}, {
|
||||
existing
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ErrContainsProfanity is returned in the event that the middleware detects
|
||||
// profanity/wordlisted words in the payload.
|
||||
const ErrContainsProfanity = new APIError('This username contains elements which are not permitted in our community. If you think this is in error, please contact us or try again.', {
|
||||
@@ -168,9 +182,17 @@ const ErrCommentTooShort = new APIError('Comment was too short', {
|
||||
status: 400
|
||||
});
|
||||
|
||||
// ErrAssetURLAlreadyExists is returned when a rename operation is requested
|
||||
// but an asset already exists with the new url.
|
||||
const ErrAssetURLAlreadyExists = new APIError('Asset URL already exists, cannot rename', {
|
||||
translation_key: 'ASSET_URL_ALREADY_EXISTS',
|
||||
status: 409
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
ExtendableError,
|
||||
APIError,
|
||||
ErrAlreadyExists,
|
||||
ErrPasswordTooShort,
|
||||
ErrSettingsNotInit,
|
||||
ErrMissingEmail,
|
||||
@@ -191,5 +213,6 @@ module.exports = {
|
||||
ErrInstallLock,
|
||||
ErrLoginAttemptMaximumExceeded,
|
||||
ErrEditWindowHasEnded,
|
||||
ErrCommentTooShort
|
||||
ErrCommentTooShort,
|
||||
ErrAssetURLAlreadyExists
|
||||
};
|
||||
|
||||
@@ -36,10 +36,10 @@ const createAction = async ({user = {}}, {item_id, item_type, action_type, group
|
||||
* Deletes an action based on the user id if the user owns that action.
|
||||
* @param {Object} user the user performing the request
|
||||
* @param {String} id the id of the action to delete
|
||||
* @return {Promise} resolves when the action is deleted
|
||||
* @return {Promise} resolves to the deleted action, or null if not found.
|
||||
*/
|
||||
const deleteAction = ({user}, {id}) => {
|
||||
return ActionModel.remove({
|
||||
return ActionModel.findOneAndRemove({
|
||||
id,
|
||||
user_id: user.id
|
||||
});
|
||||
|
||||
@@ -50,7 +50,7 @@ const createSubscriptionManager = (server) => new SubscriptionServer({
|
||||
connection.upgradeReq.headers['authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
},
|
||||
onSubscribe: (parsedMessage, baseParams, connection) => {
|
||||
onOperation: (parsedMessage, baseParams, connection) => {
|
||||
|
||||
// Cache the upgrade request.
|
||||
let upgradeReq = connection.upgradeReq;
|
||||
@@ -58,7 +58,7 @@ const createSubscriptionManager = (server) => new SubscriptionServer({
|
||||
// Attach the context per request.
|
||||
baseParams.context = async () => {
|
||||
let req;
|
||||
|
||||
|
||||
try {
|
||||
req = await deserializeUser(upgradeReq);
|
||||
} catch (e) {
|
||||
@@ -66,7 +66,7 @@ const createSubscriptionManager = (server) => new SubscriptionServer({
|
||||
|
||||
return new Context({}, pubsub);
|
||||
}
|
||||
|
||||
|
||||
return new Context(req, pubsub);
|
||||
};
|
||||
|
||||
|
||||
+7
-6
@@ -42,17 +42,17 @@ en:
|
||||
banned: Banned
|
||||
banned_user: "Banned User"
|
||||
cancel: Cancel
|
||||
dont_like_username: "I don't like this username"
|
||||
dont_like_username: "Dislike username"
|
||||
flaggedaccounts: "Flagged Usernames"
|
||||
flags: Flags
|
||||
impersonating: "This user is impersonating"
|
||||
impersonating: "Impersonation"
|
||||
loading: "Loading results"
|
||||
moderator: Moderator
|
||||
newsroom_role: "Newsroom Role"
|
||||
no_flagged_accounts: "The Account Flags queue is currently empty."
|
||||
no_flagged_accounts: "The Flagged Usernames queue is currently empty."
|
||||
no_results: "No users found with that user name or email address. They're hiding!"
|
||||
note: "Note: Banning this user will not let them edit comment or remove anything."
|
||||
offensive: "This comment is offensive"
|
||||
offensive: "Offensive"
|
||||
other: Other
|
||||
people: People
|
||||
role: "Select role..."
|
||||
@@ -144,8 +144,8 @@ en:
|
||||
auto_update: "Data automatically updates every five minutes or when you Reload."
|
||||
comment_count: comments
|
||||
flags: Flags
|
||||
most_flags: "Articles with the most flags"
|
||||
most_conversations: "Articles with the most conversations"
|
||||
most_flags: "Stories with the most flags"
|
||||
most_conversations: "Stories with the most conversations"
|
||||
next_update: "{0} minutes until next update."
|
||||
no_activity: "There haven't been any comments anywhere in the last five minutes."
|
||||
no_flags: "There have been no flags in the last 5 minutes! Hooray!"
|
||||
@@ -189,6 +189,7 @@ en:
|
||||
PASSWORD_REQUIRED: "Must input a password"
|
||||
COMMENTING_CLOSED: "Commenting is already closed"
|
||||
NOT_FOUND: "Resource not found"
|
||||
ALREADY_EXISTS: "Resource already exists"
|
||||
INVALID_ASSET_URL: "Assert URL is invalid"
|
||||
email: "Not a valid E-Mail"
|
||||
confirm_password: "Passwords don't match. Please check again"
|
||||
|
||||
+3
-3
@@ -44,14 +44,14 @@ es:
|
||||
dont_like_username: "No me gusta este nombre de usuario"
|
||||
flaggedaccounts: "Nombres de Usuario Reportados"
|
||||
flags: Reportes
|
||||
impersonating: "El usuario esta suplantando identidad"
|
||||
impersonating: Impersonando
|
||||
loading: "Cargando resultados"
|
||||
moderator: Moderator
|
||||
newsroom_role: "Rol en la redacción"
|
||||
no_flagged_accounts: "No hay ninguna cuenta reportada en este momento."
|
||||
no_flagged_accounts: "No hay ningún nombre de usario reportado en este momento."
|
||||
no_results: "No se encontraron usuarios con ese nombre o correo."
|
||||
note: "Nota: Suspender a este usuario no le va a permitir (al usuario) borrar ni editar ni comentar."
|
||||
offensive: "Este comentario es ofensivo"
|
||||
offensive: Ofensivo
|
||||
other: Otro
|
||||
people: Gente
|
||||
role: "Seleccionar rol..."
|
||||
|
||||
+2
-2
@@ -81,7 +81,7 @@
|
||||
"graphql-errors": "^2.1.0",
|
||||
"graphql-redis-subscriptions": "^1.1.5",
|
||||
"graphql-server-express": "^0.6.0",
|
||||
"graphql-subscriptions": "^0.3.1",
|
||||
"graphql-subscriptions": "^0.4.3",
|
||||
"graphql-tools": "^0.10.1",
|
||||
"helmet": "^3.5.0",
|
||||
"immutability-helper": "^2.2.0",
|
||||
@@ -116,7 +116,7 @@
|
||||
"semver": "^5.3.0",
|
||||
"simplemde": "^1.11.2",
|
||||
"snake-case": "^2.1.0",
|
||||
"subscriptions-transport-ws": "^0.5.5-alpha.0",
|
||||
"subscriptions-transport-ws": "^0.7.2",
|
||||
"timekeeper": "^1.0.0",
|
||||
"uuid": "^3.0.1",
|
||||
"yaml-loader": "^0.4.0",
|
||||
|
||||
@@ -1 +1 @@
|
||||
export {withReaction} from 'coral-framework/hocs';
|
||||
export {default as withReaction} from './withReaction';
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import uuid from 'uuid/v4';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {getDisplayName} from 'coral-framework/helpers/hoc';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
import withMutation from 'coral-framework/hocs/withMutation';
|
||||
import {showSignInDialog} from 'coral-framework/actions/auth';
|
||||
import {capitalize} from 'coral-framework/helpers/strings';
|
||||
import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils';
|
||||
import * as PropTypes from 'prop-types';
|
||||
|
||||
export default (reaction) => (WrappedComponent) => {
|
||||
if (typeof reaction !== 'string') {
|
||||
console.error('Reaction must be a valid string');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Global instance counter for each `reaction` type.
|
||||
let instances = 0;
|
||||
|
||||
// Track current subscriptions.
|
||||
let createdSubscription = null;
|
||||
let deletedSubscription = null;
|
||||
|
||||
reaction = reaction.toLowerCase();
|
||||
const Reaction = capitalize(reaction);
|
||||
|
||||
const COMMENT_FRAGMENT = gql`
|
||||
fragment ${Reaction}Button_updateFragment on Comment {
|
||||
action_summaries {
|
||||
... on ${Reaction}ActionSummary {
|
||||
count
|
||||
current_user {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const isReaction = (a) =>
|
||||
a.__typename === `${Reaction}ActionSummary`;
|
||||
|
||||
const addReactionToStore = (proxy, {action, self}) => {
|
||||
const fragmentId = `Comment_${action.item_id}`;
|
||||
|
||||
// Read the data from our cache for this query.
|
||||
const data = proxy.readFragment({
|
||||
fragment: COMMENT_FRAGMENT,
|
||||
id: fragmentId
|
||||
});
|
||||
|
||||
// Add our comment from the mutation to the end.
|
||||
let idx = data.action_summaries.findIndex(isReaction);
|
||||
|
||||
// Check whether we already reactioned this comment.
|
||||
if (self && idx >= 0 && data.action_summaries[idx].current_user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (idx < 0) {
|
||||
|
||||
// Add initial action when it doesn't exist.
|
||||
data.action_summaries.push({
|
||||
__typename: `${Reaction}ActionSummary`,
|
||||
count: 0,
|
||||
current_user: null
|
||||
});
|
||||
idx = data.action_summaries.length - 1;
|
||||
}
|
||||
|
||||
data.action_summaries[idx] = {
|
||||
...data.action_summaries[idx],
|
||||
count: data.action_summaries[idx].count + 1,
|
||||
current_user: self ? action : data.action_summaries[idx].current_user
|
||||
};
|
||||
|
||||
// Write our data back to the cache.
|
||||
proxy.writeFragment({
|
||||
fragment: COMMENT_FRAGMENT,
|
||||
id: fragmentId,
|
||||
data
|
||||
});
|
||||
};
|
||||
|
||||
const deleteReactionFromStore = (proxy, {action, self}) => {
|
||||
const fragmentId = `Comment_${action.item_id}`;
|
||||
|
||||
// Read the data from our cache for this query.
|
||||
const data = proxy.readFragment({
|
||||
fragment: COMMENT_FRAGMENT,
|
||||
id: fragmentId
|
||||
});
|
||||
|
||||
// Check whether we liked this comment.
|
||||
const idx = data.action_summaries.findIndex(isReaction);
|
||||
|
||||
if (
|
||||
self &&
|
||||
(idx < 0 || get(data.action_summaries[idx], 'current_user.id') !== action.id)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
data.action_summaries[idx] = {
|
||||
...data.action_summaries[idx],
|
||||
count: data.action_summaries[idx].count - 1,
|
||||
current_user: self ? null : data.action_summaries[idx].current_user,
|
||||
};
|
||||
|
||||
// Write our data back to the cache.
|
||||
proxy.writeFragment({
|
||||
fragment: COMMENT_FRAGMENT,
|
||||
id: fragmentId,
|
||||
data
|
||||
});
|
||||
};
|
||||
|
||||
const REACTION_CREATED_SUBSCRIPTION = gql`
|
||||
subscription ${Reaction}ActionCreated($assetId: ID!) {
|
||||
${reaction}ActionCreated(asset_id: $assetId) {
|
||||
id
|
||||
user {
|
||||
id
|
||||
}
|
||||
item_id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const REACTION_DELETED_SUBSCRIPTION = gql`
|
||||
subscription ${Reaction}ActionDeleted($assetId: ID!) {
|
||||
${reaction}ActionDeleted(asset_id: $assetId) {
|
||||
id
|
||||
user {
|
||||
id
|
||||
}
|
||||
item_id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
class WithReactions extends React.Component {
|
||||
|
||||
static contextTypes = {
|
||||
client: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
// Whether or not a mutation is currently active.
|
||||
duringMutation = false;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
// Start subscriptions when it is first needed.
|
||||
if (instances === 0) {
|
||||
createdSubscription = context.client.subscribe({
|
||||
query: REACTION_CREATED_SUBSCRIPTION,
|
||||
variables: {
|
||||
assetId: this.props.root.asset.id,
|
||||
},
|
||||
}).subscribe({
|
||||
next: this.onReactionCreated,
|
||||
error(err) { console.error('err', err); },
|
||||
});
|
||||
|
||||
deletedSubscription = context.client.subscribe({
|
||||
query: REACTION_DELETED_SUBSCRIPTION,
|
||||
variables: {
|
||||
assetId: this.props.root.asset.id,
|
||||
},
|
||||
}).subscribe({
|
||||
next: this.onReactionDeleted,
|
||||
error(err) { console.error('err', err); },
|
||||
});
|
||||
}
|
||||
instances++;
|
||||
}
|
||||
|
||||
// onReactionCreated handles live updates through the subscriptions.
|
||||
onReactionCreated = ({[`${reaction}ActionCreated`]: action}) => {
|
||||
if (this.props.user && action.user && this.props.user.id === action.user.id) {
|
||||
return;
|
||||
}
|
||||
addReactionToStore(this.context.client, {action, self: false});
|
||||
};
|
||||
|
||||
// onReactionDeleted handles live updates through the subscriptions.
|
||||
onReactionDeleted = ({[`${reaction}ActionDeleted`]: action}) => {
|
||||
if (this.props.user && action.user && this.props.user.id === action.user.id) {
|
||||
return;
|
||||
}
|
||||
deleteReactionFromStore(this.context.client, {action, self: false});
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
instances--;
|
||||
|
||||
// End subscriptions when last component will be unmounted.
|
||||
if (instances === 0) {
|
||||
try {
|
||||
createdSubscription.unsubscribe();
|
||||
deletedSubscription.unsubscribe();
|
||||
}
|
||||
catch(e) {
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
postReaction = () => {
|
||||
if (this.duringMutation) {
|
||||
return;
|
||||
}
|
||||
this.duringMutation = true;
|
||||
return this.props.postReaction(this.props.comment)
|
||||
.then((result) => {this.duringMutation = false; return Promise.resolve(result); })
|
||||
.catch((err) => {this.duringMutation = false; throw err; });
|
||||
}
|
||||
|
||||
deleteReaction = () => {
|
||||
if (this.duringMutation) {
|
||||
return;
|
||||
}
|
||||
this.duringMutation = true;
|
||||
return this.props.deleteReaction(this.props.comment)
|
||||
.then((result) => {this.duringMutation = false; return Promise.resolve(result); })
|
||||
.catch((err) => {this.duringMutation = false; throw err; });
|
||||
}
|
||||
|
||||
render() {
|
||||
const {comment} = this.props;
|
||||
|
||||
const reactionSummary = getMyActionSummary(
|
||||
`${Reaction}ActionSummary`,
|
||||
comment
|
||||
);
|
||||
|
||||
const count = getTotalActionCount(
|
||||
`${Reaction}ActionSummary`,
|
||||
comment
|
||||
);
|
||||
|
||||
const alreadyReacted = !!reactionSummary;
|
||||
|
||||
return <WrappedComponent
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
user={this.props.user}
|
||||
comment={comment}
|
||||
reactionSummary={reactionSummary}
|
||||
count={count}
|
||||
alreadyReacted={alreadyReacted}
|
||||
postReaction={this.postReaction}
|
||||
deleteReaction={this.deleteReaction}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
const withDeleteReaction = withMutation(
|
||||
gql`
|
||||
mutation Delete${Reaction}Action($input: Delete${Reaction}ActionInput!) {
|
||||
delete${Reaction}Action(input: $input) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
props: ({mutate}) => ({
|
||||
deleteReaction: (comment) => {
|
||||
|
||||
const reactionSummary = getMyActionSummary(
|
||||
`${Reaction}ActionSummary`,
|
||||
comment
|
||||
);
|
||||
|
||||
const id = reactionSummary.current_user.id;
|
||||
const item_id = comment.id;
|
||||
|
||||
const input = {id};
|
||||
return mutate({
|
||||
variables: {input},
|
||||
optimisticResponse: {
|
||||
[`delete${Reaction}Action`]: {
|
||||
__typename: `Delete${Reaction}ActionResponse`,
|
||||
errors: null
|
||||
}
|
||||
},
|
||||
update: (proxy) => {
|
||||
deleteReactionFromStore(proxy, {action: {item_id, id}, self: true});
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
const withPostReaction = withMutation(
|
||||
gql`
|
||||
mutation Create${Reaction}Action($input: Create${Reaction}ActionInput!) {
|
||||
create${Reaction}Action(input: $input) {
|
||||
${reaction} {
|
||||
id
|
||||
}
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
props: ({mutate}) => ({
|
||||
postReaction: (comment) => {
|
||||
|
||||
const input = {
|
||||
item_id: comment.id,
|
||||
};
|
||||
|
||||
return mutate({
|
||||
variables: {input},
|
||||
optimisticResponse: {
|
||||
[`create${Reaction}Action`]: {
|
||||
__typename: `Create${Reaction}ActionResponse`,
|
||||
errors: null,
|
||||
[reaction]: {
|
||||
__typename: `${Reaction}Action`,
|
||||
id: uuid()
|
||||
}
|
||||
}
|
||||
},
|
||||
update: (proxy, {data: {[`create${Reaction}Action`]: {[reaction]: action}}}) => {
|
||||
const a = {
|
||||
...action,
|
||||
item_id: input.item_id,
|
||||
};
|
||||
addReactionToStore(proxy, {action: a, self: true});
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
user: state.auth.toJS().user,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({showSignInDialog}, dispatch);
|
||||
|
||||
const enhance = compose(
|
||||
withFragments({
|
||||
comment: gql`
|
||||
fragment ${Reaction}Button_comment on Comment {
|
||||
action_summaries {
|
||||
... on ${Reaction}ActionSummary {
|
||||
count
|
||||
current_user {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
}),
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withDeleteReaction,
|
||||
withPostReaction
|
||||
);
|
||||
|
||||
WithReactions.displayName = `WithReactions(${getDisplayName(WrappedComponent)})`;
|
||||
|
||||
return enhance(WithReactions);
|
||||
};
|
||||
@@ -0,0 +1,205 @@
|
||||
const wrapResponse = require('../../../graph/helpers/response');
|
||||
const {SEARCH_OTHER_USERS} = require('../../../perms/constants');
|
||||
const errors = require('../../../errors');
|
||||
|
||||
function getReactionConfig(reaction) {
|
||||
reaction = reaction.toLowerCase();
|
||||
|
||||
const Reaction = reaction.charAt(0).toUpperCase() + reaction.slice(1);
|
||||
const REACTION = reaction.toUpperCase();
|
||||
const typeDefs = `
|
||||
enum ACTION_TYPE {
|
||||
|
||||
# Represents a ${Reaction}.
|
||||
${REACTION}
|
||||
}
|
||||
|
||||
enum ASSET_METRICS_SORT {
|
||||
|
||||
# Represents a ${Reaction}Action.
|
||||
${REACTION}
|
||||
}
|
||||
|
||||
input Create${Reaction}ActionInput {
|
||||
|
||||
# The item's id for which we are to create a ${reaction}.
|
||||
item_id: ID!
|
||||
}
|
||||
|
||||
input Delete${Reaction}ActionInput {
|
||||
|
||||
# The item's id for which we are deleting a ${reaction}.
|
||||
id: ID!
|
||||
}
|
||||
|
||||
# ${Reaction}Action is used by users who "${reaction}" a specific entity.
|
||||
type ${Reaction}Action implements Action {
|
||||
|
||||
# The ID of the action.
|
||||
id: ID!
|
||||
|
||||
# The author of the action.
|
||||
user: User
|
||||
|
||||
# The time when the Action was updated.
|
||||
updated_at: Date
|
||||
|
||||
# The time when the Action was created.
|
||||
created_at: Date
|
||||
|
||||
# The item's id for which the Action was created.
|
||||
item_id: ID!
|
||||
}
|
||||
|
||||
type ${Reaction}ActionSummary implements ActionSummary {
|
||||
|
||||
# The count of actions with this group.
|
||||
count: Int
|
||||
|
||||
# The current user's action.
|
||||
current_user: ${Reaction}Action
|
||||
}
|
||||
|
||||
# A summary of counts related to all the ${Reaction}s on an Asset.
|
||||
type ${Reaction}AssetActionSummary implements AssetActionSummary {
|
||||
|
||||
# Number of ${reaction}s associated with actionable types on this this Asset.
|
||||
actionCount: Int
|
||||
|
||||
# Number of unique actionable types that are referenced by the ${reaction}s.
|
||||
actionableItemCount: Int
|
||||
}
|
||||
|
||||
type Create${Reaction}ActionResponse implements Response {
|
||||
|
||||
# The ${reaction} that was created.
|
||||
${reaction}: ${Reaction}Action
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
type Delete${Reaction}ActionResponse implements Response {
|
||||
|
||||
# The ${reaction} that was created.
|
||||
${reaction}: ${Reaction}Action
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
type RootMutation {
|
||||
|
||||
# Creates a ${reaction} on an entity.
|
||||
create${Reaction}Action(input: Create${Reaction}ActionInput!): Create${Reaction}ActionResponse
|
||||
delete${Reaction}Action(input: Delete${Reaction}ActionInput!): Delete${Reaction}ActionResponse
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
|
||||
# Subscribe to ${reaction}s.
|
||||
${reaction}ActionCreated(asset_id: ID!): ${Reaction}Action
|
||||
|
||||
# Subscribe to ${reaction} removals.
|
||||
${reaction}ActionDeleted(asset_id: ID!): ${Reaction}Action
|
||||
}
|
||||
`;
|
||||
|
||||
return {
|
||||
typeDefs,
|
||||
resolvers: {
|
||||
Subscription: {
|
||||
[`${reaction}ActionCreated`]: ({action}) => {
|
||||
return action;
|
||||
},
|
||||
[`${reaction}ActionDeleted`]: ({action}) => {
|
||||
return action;
|
||||
},
|
||||
},
|
||||
[`${Reaction}Action`]: {
|
||||
|
||||
// This will load the user for the specific action. We'll limit this to the
|
||||
// admin users only or the current logged in user.
|
||||
user({user_id}, _, {loaders: {Users}, user}) {
|
||||
if (user && (user.can(SEARCH_OTHER_USERS) || user_id === user.id)) {
|
||||
return Users.getByID.load(user_id);
|
||||
}
|
||||
}
|
||||
},
|
||||
RootMutation: {
|
||||
[`create${Reaction}Action`]: (_, {input: {item_id}}, {mutators: {Action}, pubsub, loaders: {Comments}}) => {
|
||||
const response = Comments.get.load(item_id).then((comment) => {
|
||||
return Action.create({item_id, item_type: 'COMMENTS', action_type: REACTION})
|
||||
.then((action) => {
|
||||
|
||||
// The comment is needed to allow better filtering e.g. by asset_id.
|
||||
pubsub.publish(`${reaction}ActionCreated`, {action, comment});
|
||||
return Promise.resolve(action);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err instanceof errors.ErrAlreadyExists) {
|
||||
return Promise.resolve(err.metadata.existing);
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
return wrapResponse(reaction)(response);
|
||||
},
|
||||
[`delete${Reaction}Action`]: (_, {input: {id}}, {mutators: {Action}, pubsub, loaders: {Comments}}) => {
|
||||
const response = Action.delete({id})
|
||||
.then((action) => {
|
||||
|
||||
// Action doesn't exist or was already deleted.
|
||||
if (!action) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return Comments.get.load(action.item_id).then((comment) => {
|
||||
|
||||
// The comment is needed to allow better filtering e.g. by asset_id.
|
||||
pubsub.publish(`${reaction}ActionDeleted`, {action, comment});
|
||||
return Promise.resolve(action);
|
||||
});
|
||||
});
|
||||
|
||||
return wrapResponse(reaction)(response);
|
||||
}
|
||||
},
|
||||
},
|
||||
hooks: {
|
||||
Action: {
|
||||
__resolveType: {
|
||||
post({action_type}) {
|
||||
switch (action_type) {
|
||||
case REACTION:
|
||||
return `${Reaction}Action`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ActionSummary: {
|
||||
__resolveType: {
|
||||
post({action_type}) {
|
||||
switch (action_type) {
|
||||
case REACTION:
|
||||
return `${Reaction}ActionSummary`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
setupFunctions: {
|
||||
[`${reaction}ActionCreated`]: (options, args) => ({
|
||||
[`${reaction}ActionCreated`]: {
|
||||
filter: ({comment}) => comment.asset_id === args.asset_id,
|
||||
},
|
||||
}),
|
||||
[`${reaction}ActionDeleted`]: (options, args) => ({
|
||||
[`${reaction}ActionDeleted`]: {
|
||||
filter: ({comment}) => comment.asset_id === args.asset_id,
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = getReactionConfig;
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
getReactionConfig: require('./getReactionConfig'),
|
||||
};
|
||||
+65
-37
@@ -10,6 +10,7 @@ const PLUGINS_JSON = process.env.TALK_PLUGINS_JSON;
|
||||
// Add the current path to the module root.
|
||||
amp.addPath(__dirname);
|
||||
|
||||
let pluginsPath;
|
||||
let plugins = {};
|
||||
|
||||
// Try to parse the plugins.json file, logging out an error if the plugins.json
|
||||
@@ -22,14 +23,16 @@ try {
|
||||
|
||||
if (PLUGINS_JSON && PLUGINS_JSON.length > 0) {
|
||||
debug('Now using TALK_PLUGINS_JSON environment variable for plugins');
|
||||
plugins = require(envPlugins);
|
||||
pluginsPath = envPlugins;
|
||||
} else if (fs.existsSync(customPlugins)) {
|
||||
debug(`Now using ${customPlugins} for plugins`);
|
||||
plugins = JSON.parse(fs.readFileSync(customPlugins, 'utf8'));
|
||||
pluginsPath = customPlugins;
|
||||
} else {
|
||||
debug(`Now using ${defaultPlugins} for plugins`);
|
||||
plugins = JSON.parse(fs.readFileSync(defaultPlugins, 'utf8'));
|
||||
pluginsPath = defaultPlugins;
|
||||
}
|
||||
|
||||
plugins = require(pluginsPath);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
console.error('plugins.json and plugins.default.json not found, plugins will not be active');
|
||||
@@ -78,7 +81,12 @@ function isInternal(name) {
|
||||
*/
|
||||
function pluginPath(name) {
|
||||
if (isInternal(name)) {
|
||||
return path.join(__dirname, 'plugins', name);
|
||||
try {
|
||||
return resolve.sync(name, {moduleDirectory: 'plugins', basedir: process.cwd()});
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -88,15 +96,8 @@ function pluginPath(name) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Itterates over the plugins and gets the plugin path's, version, and name.
|
||||
*
|
||||
* @param {Array<Object|String>} plugins
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
function itteratePlugins(plugins) {
|
||||
return plugins.map((p) => {
|
||||
let plugin = {};
|
||||
class Plugin {
|
||||
constructor(entry) {
|
||||
|
||||
// This checks to see if the structure for this entry is an object:
|
||||
//
|
||||
@@ -106,23 +107,47 @@ function itteratePlugins(plugins) {
|
||||
//
|
||||
// "people"
|
||||
//
|
||||
if (typeof p === 'object') {
|
||||
plugin.name = Object.keys(p).find((name) => name !== null);
|
||||
plugin.version = p[plugin.name];
|
||||
} else if (typeof p === 'string') {
|
||||
plugin.name = p;
|
||||
plugin.version = `file:./plugins/${plugin.name}`;
|
||||
if (typeof entry === 'object') {
|
||||
this.name = Object.keys(entry).find((name) => name !== null);
|
||||
this.version = entry[this.name];
|
||||
} else if (typeof entry === 'string') {
|
||||
this.name = entry;
|
||||
this.version = `file:./plugins/${this.name}`;
|
||||
} else {
|
||||
throw new Error(`plugins.json is malformed, refer to PLUGINS.md for formatting, expected a string or an object for a plugin entry, found a ${typeof p}`);
|
||||
throw new Error(`plugins.json is malformed, refer to PLUGINS.md for formatting, expected a string or an object for a plugin entry, found a ${typeof entry}`);
|
||||
}
|
||||
|
||||
// Get the path for the plugin.
|
||||
plugin.path = pluginPath(plugin.name);
|
||||
this.path = pluginPath(this.name);
|
||||
}
|
||||
|
||||
return plugin;
|
||||
});
|
||||
require() {
|
||||
if (typeof this.path === 'undefined') {
|
||||
throw new Error(`plugin '${this.name}' is not local and is not resolvable, plugin reconsiliation may be required`);
|
||||
}
|
||||
|
||||
try {
|
||||
this.module = require(this.path);
|
||||
} catch (e) {
|
||||
if (e && e.code && e.code === 'MODULE_NOT_FOUND' && isInternal(this.name)) {
|
||||
console.error(new Error(`plugin '${this.name}' could not be loaded due to missing dependencies, plugin reconsiliation may be required`));
|
||||
throw e;
|
||||
}
|
||||
|
||||
console.error(new Error(`plugin '${this.name}' could not be required from '${this.path}': ${e.message}`));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Itterates over the plugins and gets the plugin path's, version, and name.
|
||||
*
|
||||
* @param {Array<Object|String>} plugins
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
const itteratePlugins = (plugins) => plugins.map((p) => new Plugin(p));
|
||||
|
||||
// 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) => {
|
||||
@@ -139,22 +164,20 @@ Object.keys(plugins).forEach((type) => itteratePlugins(plugins[type]).forEach((p
|
||||
*/
|
||||
class PluginSection {
|
||||
constructor(plugins) {
|
||||
this.plugins = itteratePlugins(plugins).map((plugin) => {
|
||||
if (typeof plugin.path === 'undefined') {
|
||||
throw new Error(`plugin '${plugin.name}' is not local and is not resolvable, plugin reconsiliation may be required`);
|
||||
}
|
||||
this.required = false;
|
||||
this.plugins = itteratePlugins(plugins);
|
||||
}
|
||||
|
||||
try {
|
||||
plugin.module = require(plugin.path);
|
||||
} catch (e) {
|
||||
if (e && e.code && e.code === 'MODULE_NOT_FOUND' && isInternal(plugin.name)) {
|
||||
console.error(new Error(`plugin '${plugin.name}' could not be loaded due to missing dependencies, plugin reconsiliation may be required`));
|
||||
throw e;
|
||||
}
|
||||
require() {
|
||||
if (this.required) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.required = true;
|
||||
this.plugins.forEach((plugin) => {
|
||||
|
||||
console.error(new Error(`plugin '${plugin.name}' could not be required from '${plugin.path}': ${e.message}`));
|
||||
throw e;
|
||||
}
|
||||
// Load the plugin.
|
||||
plugin.require();
|
||||
|
||||
if (isInternal(plugin.name)) {
|
||||
debug(`loading internal plugin '${plugin.name}' from '${plugin.path}'`);
|
||||
@@ -171,6 +194,10 @@ class PluginSection {
|
||||
* available.
|
||||
*/
|
||||
hook(hook) {
|
||||
|
||||
// Load the plugin source if we haven't already.
|
||||
this.require();
|
||||
|
||||
return this.plugins
|
||||
.filter(({module}) => hook in module)
|
||||
.filter((plugin) => {
|
||||
@@ -226,6 +253,7 @@ class PluginManager {
|
||||
|
||||
module.exports = {
|
||||
plugins,
|
||||
pluginsPath,
|
||||
PluginManager,
|
||||
isInternal,
|
||||
pluginPath,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import styles from './styles.css';
|
||||
import {withReaction} from 'plugin-api/beta/client/hocs';
|
||||
import {t, can} from 'plugin-api/beta/client/services';
|
||||
import {Icon} from 'plugin-api/beta/client/components';
|
||||
import cn from 'classnames';
|
||||
|
||||
const plugin = 'coral-plugin-like';
|
||||
|
||||
class LikeButton extends React.Component {
|
||||
handleClick = () => {
|
||||
const {
|
||||
postReaction,
|
||||
deleteReaction,
|
||||
showSignInDialog,
|
||||
alreadyReacted,
|
||||
user,
|
||||
} = this.props;
|
||||
|
||||
// If the current user does not exist, trigger sign in dialog.
|
||||
if (!user) {
|
||||
showSignInDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
// If the current user is suspended, do nothing.
|
||||
if (!can(user, 'INTERACT_WITH_COMMUNITY')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (alreadyReacted) {
|
||||
deleteReaction();
|
||||
} else {
|
||||
postReaction();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {count, alreadyReacted} = this.props;
|
||||
return (
|
||||
<div className={cn(styles.container, `${plugin}-container`)}>
|
||||
<button
|
||||
className={cn(styles.button, {[styles.liked]: alreadyReacted}, `${plugin}-button`)}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<span>{t(alreadyReacted ? 'coral-plugin-like.liked' : 'coral-plugin-like.like')}</span>
|
||||
<Icon name="thumb_up" className={`${plugin}-icon`} />
|
||||
<span className={`${plugin}-count`}>{count > 0 && count}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withReaction('like')(LikeButton);
|
||||
@@ -1,86 +0,0 @@
|
||||
import React, {Component} from 'react';
|
||||
import styles from './style.css';
|
||||
|
||||
import cn from 'classnames';
|
||||
import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const name = 'coral-plugin-like';
|
||||
|
||||
class LikeButton extends Component {
|
||||
handleClick = () => {
|
||||
const {postLike, showSignInDialog, deleteAction} = this.props;
|
||||
const {root: {me}, comment} = this.props;
|
||||
|
||||
const myLikeActionSummary = getMyActionSummary(
|
||||
'LikeActionSummary',
|
||||
comment
|
||||
);
|
||||
|
||||
// If the current user does not exist, trigger sign in dialog.
|
||||
if (!me) {
|
||||
showSignInDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
// If the current user is banned, do nothing.
|
||||
if (me.status === 'BANNED') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (myLikeActionSummary) {
|
||||
deleteAction(myLikeActionSummary.current_user.id, comment.id);
|
||||
} else {
|
||||
postLike({
|
||||
item_id: comment.id,
|
||||
item_type: 'COMMENTS'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {comment} = this.props;
|
||||
|
||||
if (!comment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const myLike = getMyActionSummary('LikeActionSummary', comment);
|
||||
let count = getTotalActionCount('LikeActionSummary', comment);
|
||||
|
||||
return (
|
||||
<div className={cn(styles.like, `${name}-container`)}>
|
||||
<button
|
||||
className={cn(
|
||||
styles.button,
|
||||
{[styles.liked]: myLike},
|
||||
`${name}-button`
|
||||
)}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<span className={`${name}-button-text`}>
|
||||
{t(myLike ? 'liked' : 'like')}
|
||||
</span>
|
||||
<i
|
||||
className={cn(
|
||||
styles.icon,
|
||||
'material-icons',
|
||||
{[styles.liked]: myLike},
|
||||
`${name}-icon`
|
||||
)}
|
||||
aria-hidden={true}
|
||||
>
|
||||
thumb_up
|
||||
</i>
|
||||
<span className={`${name}-count`}>{count > 0 && count}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LikeButton.propTypes = {
|
||||
data: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default LikeButton;
|
||||
@@ -1,186 +0,0 @@
|
||||
import get from 'lodash/get';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {compose, gql, graphql} from 'react-apollo';
|
||||
import LikeButton from '../components/LikeButton';
|
||||
import withFragments from 'coral-framework/hocs/withFragments';
|
||||
import{showSignInDialog} from 'coral-framework/actions/auth';
|
||||
|
||||
const isLikeAction = (a) => a.__typename === 'LikeActionSummary';
|
||||
|
||||
const COMMENT_FRAGMENT = gql`
|
||||
fragment LikeButton_updateFragment on Comment {
|
||||
action_summaries {
|
||||
... on LikeActionSummary {
|
||||
count
|
||||
current_user {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const withDeleteAction = graphql(
|
||||
gql`
|
||||
mutation deleteAction($id: ID!) {
|
||||
deleteAction(id:$id) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
props: ({mutate}) => ({
|
||||
deleteAction: (id, commentId) => {
|
||||
return mutate({
|
||||
variables: {id},
|
||||
optimisticResponse: {
|
||||
deleteAction: {
|
||||
__typename: 'DeleteActionResponse',
|
||||
errors: null
|
||||
}
|
||||
},
|
||||
update: (proxy) => {
|
||||
const fragmentId = `Comment_${commentId}`;
|
||||
|
||||
// Read the data from our cache for this query.
|
||||
const data = proxy.readFragment({
|
||||
fragment: COMMENT_FRAGMENT,
|
||||
id: fragmentId
|
||||
});
|
||||
|
||||
// Check whether we liked this comment.
|
||||
const idx = data.action_summaries.findIndex(isLikeAction);
|
||||
if (
|
||||
idx < 0 ||
|
||||
get(data.action_summaries[idx], 'current_user.id') !== id
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
data.action_summaries[idx] = {
|
||||
...data.action_summaries[idx],
|
||||
count: data.action_summaries[idx].count - 1,
|
||||
current_user: null
|
||||
};
|
||||
|
||||
// Write our data back to the cache.
|
||||
proxy.writeFragment({
|
||||
fragment: COMMENT_FRAGMENT,
|
||||
id: fragmentId,
|
||||
data
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
const withPostLike = graphql(
|
||||
gql`
|
||||
mutation createLike($like: CreateLikeInput!) {
|
||||
createLike(like: $like) {
|
||||
like {
|
||||
id
|
||||
}
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
props: ({mutate}) => ({
|
||||
postLike: (like) => {
|
||||
return mutate({
|
||||
variables: {like},
|
||||
optimisticResponse: {
|
||||
createLike: {
|
||||
__typename: 'CreateLikeResponse',
|
||||
errors: null,
|
||||
like: {
|
||||
__typename: 'LikeAction',
|
||||
id: 'pending'
|
||||
}
|
||||
}
|
||||
},
|
||||
update: (proxy, mutationResult) => {
|
||||
const fragmentId = `Comment_${like.item_id}`;
|
||||
|
||||
// Read the data from our cache for this query.
|
||||
const data = proxy.readFragment({
|
||||
fragment: COMMENT_FRAGMENT,
|
||||
id: fragmentId
|
||||
});
|
||||
|
||||
// Add our comment from the mutation to the end.
|
||||
let idx = data.action_summaries.findIndex(isLikeAction);
|
||||
|
||||
// Check whether we already liked this comment.
|
||||
if (idx >= 0 && data.action_summaries[idx].current_user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (idx < 0) {
|
||||
|
||||
// Add initial action when it doesn't exist.
|
||||
data.action_summaries.push({
|
||||
__typename: 'LikeActionSummary',
|
||||
count: 0,
|
||||
current_user: null
|
||||
});
|
||||
idx = data.action_summaries.length - 1;
|
||||
}
|
||||
|
||||
data.action_summaries[idx] = {
|
||||
...data.action_summaries[idx],
|
||||
count: data.action_summaries[idx].count + 1,
|
||||
current_user: mutationResult.data.createLike.like
|
||||
};
|
||||
|
||||
// Write our data back to the cache.
|
||||
proxy.writeFragment({
|
||||
fragment: COMMENT_FRAGMENT,
|
||||
id: fragmentId,
|
||||
data
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({showSignInDialog}, dispatch);
|
||||
|
||||
const enhance = compose(
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment LikeButton_root on RootQuery {
|
||||
me {
|
||||
status
|
||||
}
|
||||
}
|
||||
`,
|
||||
comment: gql`
|
||||
fragment LikeButton_comment on Comment {
|
||||
action_summaries {
|
||||
... on LikeActionSummary {
|
||||
count
|
||||
current_user {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
}),
|
||||
connect(null, mapDispatchToProps),
|
||||
withDeleteAction,
|
||||
withPostLike
|
||||
);
|
||||
|
||||
export default enhance(LikeButton);
|
||||
@@ -1,5 +1,5 @@
|
||||
import LikeButton from './containers/LikeButton';
|
||||
import translations from './translations.json';
|
||||
import LikeButton from './LikeButton';
|
||||
import translations from './translations.yml';
|
||||
|
||||
export default {
|
||||
translations,
|
||||
|
||||
+2
-7
@@ -1,6 +1,6 @@
|
||||
.like {
|
||||
.container {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
color: #2a2a2a;
|
||||
@@ -17,14 +17,9 @@
|
||||
|
||||
&.liked {
|
||||
color: rgb(0,134,227);
|
||||
|
||||
&:hover {
|
||||
color: rgb(0,134,227);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding: 0 5px;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"like": "Like",
|
||||
"liked": "Liked"
|
||||
},
|
||||
"es": {
|
||||
"like": "Me Gusta",
|
||||
"liked": "Me Gustó"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
en:
|
||||
coral-plugin-like:
|
||||
like: Like
|
||||
liked: Liked
|
||||
es:
|
||||
coral-plugin-like:
|
||||
like: Me Gusta
|
||||
liked: Me Gustó
|
||||
|
||||
@@ -1,36 +1,2 @@
|
||||
const {readFileSync} = require('fs');
|
||||
const path = require('path');
|
||||
const wrapResponse = require('../../graph/helpers/response');
|
||||
|
||||
module.exports = {
|
||||
typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8'),
|
||||
resolvers: {
|
||||
RootMutation: {
|
||||
createLike(_, {like: {item_id, item_type}}, {mutators: {Action}}) {
|
||||
return wrapResponse('like')(Action.create({item_id, item_type, action_type: 'LIKE'}));
|
||||
}
|
||||
}
|
||||
},
|
||||
hooks: {
|
||||
Action: {
|
||||
__resolveType: {
|
||||
post({action_type}) {
|
||||
switch (action_type) {
|
||||
case 'LIKE':
|
||||
return 'LikeAction';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ActionSummary: {
|
||||
__resolveType: {
|
||||
post({action_type}) {
|
||||
switch (action_type) {
|
||||
case 'LIKE':
|
||||
return 'LikeActionSummary';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const {getReactionConfig} = require('../../plugin-api/beta/server');
|
||||
module.exports = getReactionConfig('like');
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
enum ACTION_TYPE {
|
||||
|
||||
# Represents a Like.
|
||||
LIKE
|
||||
}
|
||||
|
||||
enum ASSET_METRICS_SORT {
|
||||
|
||||
# Represents a LikeAction.
|
||||
LIKE
|
||||
}
|
||||
|
||||
input CreateLikeInput {
|
||||
|
||||
# The item's id for which we are to create a like.
|
||||
item_id: ID!
|
||||
|
||||
# The type of the item for which we are to create the like.
|
||||
item_type: ACTION_ITEM_TYPE!
|
||||
}
|
||||
|
||||
# LikeAction is used by users who "like" a specific entity.
|
||||
type LikeAction implements Action {
|
||||
|
||||
# The ID of the action.
|
||||
id: ID!
|
||||
|
||||
# The author of the action.
|
||||
user: User
|
||||
|
||||
# The time when the Action was updated.
|
||||
updated_at: Date
|
||||
|
||||
# The time when the Action was created.
|
||||
created_at: Date
|
||||
}
|
||||
|
||||
type LikeActionSummary implements ActionSummary {
|
||||
|
||||
# The count of actions with this group.
|
||||
count: Int
|
||||
|
||||
# The current user's action.
|
||||
current_user: LikeAction
|
||||
}
|
||||
|
||||
# A summary of counts related to all the Likes on an Asset.
|
||||
type LikeAssetActionSummary implements AssetActionSummary {
|
||||
|
||||
# Number of likes associated with actionable types on this this Asset.
|
||||
actionCount: Int
|
||||
|
||||
# Number of unique actionable types that are referenced by the likes.
|
||||
actionableItemCount: Int
|
||||
}
|
||||
|
||||
type CreateLikeResponse implements Response {
|
||||
|
||||
# The like that was created.
|
||||
like: LikeAction
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
type RootMutation {
|
||||
|
||||
# Creates a like on an entity.
|
||||
createLike(like: CreateLikeInput!): CreateLikeResponse
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import React from 'react';
|
||||
import {Icon} from 'coral-ui';
|
||||
import styles from './styles.css';
|
||||
import {withReaction} from 'plugin-api/beta/client/hocs';
|
||||
import {t, can} from 'plugin-api/beta/client/services';
|
||||
import {Icon} from 'plugin-api/beta/client/components';
|
||||
import cn from 'classnames';
|
||||
|
||||
const plugin = 'coral-plugin-love';
|
||||
|
||||
class LoveButton extends React.Component {
|
||||
handleClick = () => {
|
||||
@@ -25,7 +28,7 @@ class LoveButton extends React.Component {
|
||||
return;
|
||||
}
|
||||
|
||||
if (alreadyReacted()) {
|
||||
if (alreadyReacted) {
|
||||
deleteReaction();
|
||||
} else {
|
||||
postReaction();
|
||||
@@ -35,14 +38,16 @@ class LoveButton extends React.Component {
|
||||
render() {
|
||||
const {count, alreadyReacted} = this.props;
|
||||
return (
|
||||
<button
|
||||
className={`${styles.button} ${alreadyReacted() ? styles.loved : ''}`}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<span>{t(alreadyReacted() ? 'loved' : 'love')}</span>
|
||||
<Icon name="favorite" />
|
||||
<span>{count > 0 && count}</span>
|
||||
</button>
|
||||
<div className={cn(styles.container, `${plugin}-container`)}>
|
||||
<button
|
||||
className={cn(styles.button, {[styles.loved]: alreadyReacted}, `${plugin}-button`)}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<span>{t(alreadyReacted ? 'coral-plugin-love.loved' : 'coral-plugin-love.love')}</span>
|
||||
<Icon name="favorite" className={`${plugin}-icon`} />
|
||||
<span className={`${plugin}-count`}>{count > 0 && count}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import LoveButton from './LoveButton';
|
||||
import translations from './translations.json';
|
||||
import translations from './translations.yml';
|
||||
|
||||
export default {
|
||||
translations,
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
.respect {
|
||||
display: inline-block;
|
||||
.container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.button {
|
||||
color: #2a2a2a;
|
||||
margin: 5px 10px 5px 0px;
|
||||
background: none;
|
||||
padding: 0px;
|
||||
border: none;
|
||||
font-size: inherit;
|
||||
color: #2a2a2a;
|
||||
margin: 5px 10px 5px 0px;
|
||||
background: none;
|
||||
padding: 0px;
|
||||
border: none;
|
||||
font-size: inherit;
|
||||
|
||||
&:hover {
|
||||
color: #767676;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.loved {
|
||||
color: #e52338;
|
||||
&:hover {
|
||||
color: #767676;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.loved {
|
||||
color: #e52338;
|
||||
|
||||
&:hover {
|
||||
color: #e52839;
|
||||
cursor: pointer;
|
||||
}
|
||||
color: #e52839;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"love": "Love",
|
||||
"loved": "Loved"
|
||||
},
|
||||
"es": {
|
||||
"love": "Amo",
|
||||
"loved": "Amé"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
en:
|
||||
coral-plugin-love:
|
||||
love: Love
|
||||
loved: Loved
|
||||
es:
|
||||
coral-plugin-love:
|
||||
love: Amo
|
||||
loved: Amé
|
||||
|
||||
@@ -1,36 +1,2 @@
|
||||
const {readFileSync} = require('fs');
|
||||
const path = require('path');
|
||||
const wrapResponse = require('../../graph/helpers/response');
|
||||
|
||||
module.exports = {
|
||||
typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8'),
|
||||
resolvers: {
|
||||
RootMutation: {
|
||||
createLove(_, {love: {item_id, item_type}}, {mutators: {Action}}) {
|
||||
return wrapResponse('love')(Action.create({item_id, item_type, action_type: 'LOVE'}));
|
||||
}
|
||||
}
|
||||
},
|
||||
hooks: {
|
||||
Action: {
|
||||
__resolveType: {
|
||||
post({action_type}) {
|
||||
switch (action_type) {
|
||||
case 'LOVE':
|
||||
return 'LoveAction';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ActionSummary: {
|
||||
__resolveType: {
|
||||
post({action_type}) {
|
||||
switch (action_type) {
|
||||
case 'LOVE':
|
||||
return 'LoveActionSummary';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const {getReactionConfig} = require('../../plugin-api/beta/server');
|
||||
module.exports = getReactionConfig('love');
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
enum ACTION_TYPE {
|
||||
|
||||
# Represents a Love.
|
||||
LOVE
|
||||
}
|
||||
|
||||
enum ASSET_METRICS_SORT {
|
||||
|
||||
# Represents a LoveAction.
|
||||
LOVE
|
||||
}
|
||||
|
||||
input CreateLoveInput {
|
||||
|
||||
# The item's id for which we are to create a love.
|
||||
item_id: ID!
|
||||
|
||||
# The type of the item for which we are to create the love.
|
||||
item_type: ACTION_ITEM_TYPE!
|
||||
}
|
||||
|
||||
# LoveAction is used by users who "love" a specific entity.
|
||||
type LoveAction implements Action {
|
||||
|
||||
# The ID of the action.
|
||||
id: ID!
|
||||
|
||||
# The author of the action.
|
||||
user: User
|
||||
|
||||
# The time when the Action was updated.
|
||||
updated_at: Date
|
||||
|
||||
# The time when the Action was created.
|
||||
created_at: Date
|
||||
}
|
||||
|
||||
type LoveActionSummary implements ActionSummary {
|
||||
|
||||
# The count of actions with this group.
|
||||
count: Int
|
||||
|
||||
# The current user's action.
|
||||
current_user: LoveAction
|
||||
}
|
||||
|
||||
# A summary of counts related to all the Loves on an Asset.
|
||||
type LoveAssetActionSummary implements AssetActionSummary {
|
||||
|
||||
# Number of loves associated with actionable types on this this Asset.
|
||||
actionCount: Int
|
||||
|
||||
# Number of unique actionable types that are referenced by the loves.
|
||||
actionableItemCount: Int
|
||||
}
|
||||
|
||||
type CreateLoveResponse implements Response {
|
||||
|
||||
# The love that was created.
|
||||
love: LoveAction
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
type RootMutation {
|
||||
|
||||
# Creates a love on an entity.
|
||||
createLove(love: CreateLoveInput!): CreateLoveResponse
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import Icon from './Icon';
|
||||
import styles from './styles.css';
|
||||
import {withReaction} from 'plugin-api/beta/client/hocs';
|
||||
import {t, can} from 'plugin-api/beta/client/services';
|
||||
import cn from 'classnames';
|
||||
|
||||
const plugin = 'coral-plugin-respect';
|
||||
|
||||
class RespectButton extends React.Component {
|
||||
handleClick = () => {
|
||||
const {
|
||||
postReaction,
|
||||
deleteReaction,
|
||||
showSignInDialog,
|
||||
alreadyReacted,
|
||||
user,
|
||||
} = this.props;
|
||||
|
||||
// If the current user does not exist, trigger sign in dialog.
|
||||
if (!user) {
|
||||
showSignInDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
// If the current user is suspended, do nothing.
|
||||
if (!can(user, 'INTERACT_WITH_COMMUNITY')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (alreadyReacted) {
|
||||
deleteReaction();
|
||||
} else {
|
||||
postReaction();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {count, alreadyReacted} = this.props;
|
||||
return (
|
||||
<div className={cn(styles.container, `${plugin}-container`)}>
|
||||
<button
|
||||
className={cn(styles.button, {[styles.respected]: alreadyReacted}, `${plugin}-button`)}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<span className={`${plugin}-button-text`}>
|
||||
{t(alreadyReacted ? 'coral-plugin-respect.respected' : 'coral-plugin-respect.respect')}
|
||||
</span>
|
||||
<Icon className={cn(styles.icon, `${plugin}-icon`)} />
|
||||
<span className={cn(styles.icon, `${plugin}-count`)}>{count > 0 && count}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withReaction('respect')(RespectButton);
|
||||
@@ -1,6 +0,0 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
|
||||
export default ({className}) => (
|
||||
<i className={cn('fa', 'fa-handshake-o', className)} aria-hidden="true"/>
|
||||
);
|
||||
@@ -1,69 +0,0 @@
|
||||
import React, {Component} from 'react';
|
||||
import styles from './style.css';
|
||||
import Icon from './Icon';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import cn from 'classnames';
|
||||
|
||||
import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils';
|
||||
|
||||
const name = 'coral-plugin-respect';
|
||||
|
||||
class RespectButton extends Component {
|
||||
|
||||
handleClick = () => {
|
||||
const {postRespect, showSignInDialog, deleteAction} = this.props;
|
||||
const {root: {me}, comment} = this.props;
|
||||
|
||||
const myRespectActionSummary = getMyActionSummary('RespectActionSummary', comment);
|
||||
|
||||
// If the current user does not exist, trigger sign in dialog.
|
||||
if (!me) {
|
||||
showSignInDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
// If the current user is banned, do nothing.
|
||||
if (me.status === 'BANNED') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (myRespectActionSummary) {
|
||||
deleteAction(myRespectActionSummary.current_user.id, comment.id);
|
||||
} else {
|
||||
postRespect({
|
||||
item_id: comment.id,
|
||||
item_type: 'COMMENTS'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {comment} = this.props;
|
||||
|
||||
if (!comment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const myRespect = getMyActionSummary('RespectActionSummary', comment);
|
||||
let count = getTotalActionCount('RespectActionSummary', comment);
|
||||
|
||||
return (
|
||||
<div className={cn(styles.respect, `${name}-container`)}>
|
||||
<button
|
||||
className={cn(styles.button, {[styles.respected]: myRespect}, `${name}-button`)}
|
||||
onClick={this.handleClick} >
|
||||
<span className={`${name}-button-text`}>{t(myRespect ? 'respected' : 'respect')}</span>
|
||||
<Icon className={cn(styles.icon, {[styles.respected]: myRespect}, `${name}-icon`)} />
|
||||
<span className={`${name}-count`}>{count > 0 && count}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RespectButton.propTypes = {
|
||||
data: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default RespectButton;
|
||||
@@ -1,163 +0,0 @@
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import get from 'lodash/get';
|
||||
import {withFragments, withMutation} from 'coral-framework/hocs';
|
||||
import {showSignInDialog} from 'coral-framework/actions/auth';
|
||||
import RespectButton from '../components/RespectButton';
|
||||
|
||||
const isRespectAction = (a) => a.__typename === 'RespectActionSummary';
|
||||
|
||||
const COMMENT_FRAGMENT = gql`
|
||||
fragment CoralRespect_UpdateFragment on Comment {
|
||||
action_summaries {
|
||||
... on RespectActionSummary {
|
||||
count
|
||||
current_user {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const withDeleteAction = withMutation(gql`
|
||||
mutation CoralRespect_DeleteAction($id: ID!) {
|
||||
deleteAction(id:$id) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`, {
|
||||
props: ({mutate}) => ({
|
||||
deleteAction: (id, commentId) => {
|
||||
return mutate({
|
||||
variables: {id},
|
||||
optimisticResponse: {
|
||||
deleteAction: {
|
||||
__typename: 'DeleteActionResponse',
|
||||
errors: null,
|
||||
}
|
||||
},
|
||||
update: (proxy) => {
|
||||
const fragmentId = `Comment_${commentId}`;
|
||||
|
||||
// Read the data from our cache for this query.
|
||||
const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId});
|
||||
|
||||
// Check whether we respected this comment.
|
||||
const idx = data.action_summaries.findIndex(isRespectAction);
|
||||
if (idx < 0 || get(data.action_summaries[idx], 'current_user.id') !== id) {
|
||||
return;
|
||||
}
|
||||
|
||||
data.action_summaries[idx] = {
|
||||
...data.action_summaries[idx],
|
||||
count: data.action_summaries[idx].count - 1,
|
||||
current_user: null,
|
||||
};
|
||||
|
||||
// Write our data back to the cache.
|
||||
proxy.writeFragment({fragment: COMMENT_FRAGMENT, id: fragmentId, data});
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const withPostRespect = withMutation(gql`
|
||||
mutation CoralRespect_CreateRespect($respect: CreateRespectInput!) {
|
||||
createRespect(respect: $respect) {
|
||||
respect {
|
||||
id
|
||||
}
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`, {
|
||||
props: ({mutate}) => ({
|
||||
postRespect: (respect) => {
|
||||
return mutate({
|
||||
variables: {respect},
|
||||
optimisticResponse: {
|
||||
createRespect: {
|
||||
__typename: 'CreateRespectResponse',
|
||||
errors: null,
|
||||
respect: {
|
||||
__typename: 'RespectAction',
|
||||
id: 'pending',
|
||||
},
|
||||
}
|
||||
},
|
||||
update: (proxy, mutationResult) => {
|
||||
const fragmentId = `Comment_${respect.item_id}`;
|
||||
|
||||
// Read the data from our cache for this query.
|
||||
const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId});
|
||||
|
||||
// Add our comment from the mutation to the end.
|
||||
let idx = data.action_summaries.findIndex(isRespectAction);
|
||||
|
||||
// Check whether we already respected this comment.
|
||||
if (idx >= 0 && data.action_summaries[idx].current_user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (idx < 0) {
|
||||
|
||||
// Add initial action when it doesn't exist.
|
||||
data.action_summaries.push({
|
||||
__typename: 'RespectActionSummary',
|
||||
count: 0,
|
||||
current_user: null,
|
||||
});
|
||||
idx = data.action_summaries.length - 1;
|
||||
}
|
||||
|
||||
data.action_summaries[idx] = {
|
||||
...data.action_summaries[idx],
|
||||
count: data.action_summaries[idx].count + 1,
|
||||
current_user: mutationResult.data.createRespect.respect,
|
||||
};
|
||||
|
||||
// Write our data back to the cache.
|
||||
proxy.writeFragment({fragment: COMMENT_FRAGMENT, id: fragmentId, data});
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({showSignInDialog}, dispatch);
|
||||
|
||||
const enhance = compose(
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment CoralRespect_RespectButton_root on RootQuery {
|
||||
me {
|
||||
status
|
||||
}
|
||||
}
|
||||
`,
|
||||
comment: gql`
|
||||
fragment CoralRespect_RespectButton_comment on Comment {
|
||||
action_summaries {
|
||||
... on RespectActionSummary {
|
||||
count
|
||||
current_user {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
}),
|
||||
connect(null, mapDispatchToProps),
|
||||
withDeleteAction,
|
||||
withPostRespect,
|
||||
);
|
||||
|
||||
export default enhance(RespectButton);
|
||||
@@ -1,9 +1,9 @@
|
||||
import RespectButton from './containers/RespectButton';
|
||||
import translations from './translations.json';
|
||||
import RespectButton from './RespectButton';
|
||||
import translations from './translations.yml';
|
||||
|
||||
export default {
|
||||
translations,
|
||||
slots: {
|
||||
commentActions: [RespectButton],
|
||||
commentReactions: [RespectButton]
|
||||
}
|
||||
};
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
.respect {
|
||||
.container {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
color: #2a2a2a;
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"respect": "Respect",
|
||||
"respected": "Respected"
|
||||
},
|
||||
"es": {
|
||||
"respect": "Respetar",
|
||||
"respected": "Respetado"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
en:
|
||||
coral-plugin-respect:
|
||||
respect: Respect
|
||||
respected: Respected
|
||||
es:
|
||||
coral-plugin-respect:
|
||||
respect: Respetar
|
||||
respected: Respetado
|
||||
|
||||
@@ -1,41 +1,2 @@
|
||||
const {readFileSync} = require('fs');
|
||||
const path = require('path');
|
||||
const wrapResponse = require('../../graph/helpers/response');
|
||||
|
||||
module.exports = {
|
||||
typeDefs: readFileSync(
|
||||
path.join(__dirname, 'server/typeDefs.graphql'),
|
||||
'utf8'
|
||||
),
|
||||
resolvers: {
|
||||
RootMutation: {
|
||||
createRespect(_, {respect: {item_id, item_type}}, {mutators: {Action}}) {
|
||||
return wrapResponse('respect')(
|
||||
Action.create({item_id, item_type, action_type: 'RESPECT'})
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
hooks: {
|
||||
Action: {
|
||||
__resolveType: {
|
||||
post({action_type}) {
|
||||
switch (action_type) {
|
||||
case 'RESPECT':
|
||||
return 'RespectAction';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ActionSummary: {
|
||||
__resolveType: {
|
||||
post({action_type}) {
|
||||
switch (action_type) {
|
||||
case 'RESPECT':
|
||||
return 'RespectActionSummary';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const {getReactionConfig} = require('../../plugin-api/beta/server');
|
||||
module.exports = getReactionConfig('respect');
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
enum ACTION_TYPE {
|
||||
|
||||
# Represents a Respect.
|
||||
RESPECT
|
||||
}
|
||||
|
||||
input CreateRespectInput {
|
||||
|
||||
# The item's id for which we are to create a respect.
|
||||
item_id: ID!
|
||||
|
||||
# The type of the item for which we are to create the respect.
|
||||
item_type: ACTION_ITEM_TYPE!
|
||||
}
|
||||
|
||||
# RespectAction is used by users who "respect" a specific entity.
|
||||
type RespectAction implements Action {
|
||||
|
||||
# The ID of the action.
|
||||
id: ID!
|
||||
|
||||
# The author of the action.
|
||||
user: User
|
||||
|
||||
# The time when the Action was updated.
|
||||
updated_at: Date
|
||||
|
||||
# The time when the Action was created.
|
||||
created_at: Date
|
||||
}
|
||||
|
||||
type RespectActionSummary implements ActionSummary {
|
||||
|
||||
# The count of actions with this group.
|
||||
count: Int
|
||||
|
||||
# The current user's action.
|
||||
current_user: RespectAction
|
||||
}
|
||||
|
||||
type CreateRespectResponse implements Response {
|
||||
|
||||
# The respect that was created.
|
||||
respect: RespectAction
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
}
|
||||
|
||||
type RootMutation {
|
||||
|
||||
# Creates a respect on an entity.
|
||||
createRespect(respect: CreateRespectInput!): CreateRespectResponse
|
||||
}
|
||||
+27
-10
@@ -1,5 +1,6 @@
|
||||
const ActionModel = require('../models/action');
|
||||
const _ = require('lodash');
|
||||
const errors = require('../errors');
|
||||
|
||||
module.exports = class ActionsService {
|
||||
|
||||
@@ -12,10 +13,10 @@ module.exports = class ActionsService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an action.
|
||||
* @param {String} item_id identifier of the comment (uuid)
|
||||
* Inserts an action.
|
||||
* @param {String} item_id identifier of the item (uuid)
|
||||
* @param {String} user_id user id of the action (uuid)
|
||||
* @param {String} action the new action to the comment
|
||||
* @param {String} action the new action to the item
|
||||
* @return {Promise}
|
||||
*/
|
||||
static insertUserAction(action) {
|
||||
@@ -31,16 +32,32 @@ module.exports = class ActionsService {
|
||||
};
|
||||
|
||||
// Create/Update the action.
|
||||
return ActionModel.findOneAndUpdate(query, action, {
|
||||
return new Promise((resolve, reject) => {
|
||||
ActionModel.findOneAndUpdate(
|
||||
query, {
|
||||
|
||||
// Ensure that if it's new, we return the new object created.
|
||||
new: true,
|
||||
// Only set when not existing.
|
||||
$setOnInsert: action,
|
||||
}, {
|
||||
|
||||
// Perform an upsert in the event that this doesn't exist.
|
||||
upsert: true,
|
||||
// Ensure that if it's new, we return the new object created.
|
||||
new: true,
|
||||
|
||||
// Set the default values if not provided based on the mongoose models.
|
||||
setDefaultsOnInsert: true
|
||||
// Use raw result to get `updatedExisting`.
|
||||
passRawResult: true,
|
||||
|
||||
// Perform an upsert in the event that this doesn't exist.
|
||||
upsert: true,
|
||||
|
||||
// Set the default values if not provided based on the mongoose models.
|
||||
setDefaultsOnInsert: true
|
||||
}, (err, doc, raw) => {
|
||||
if (err) { return reject(err); }
|
||||
if (raw.lastErrorObject.updatedExisting) {
|
||||
return reject(new errors.ErrAlreadyExists(raw.value));
|
||||
}
|
||||
return resolve(raw.value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+56
-3
@@ -1,3 +1,4 @@
|
||||
const CommentModel = require('../models/comment');
|
||||
const AssetModel = require('../models/asset');
|
||||
const SettingsService = require('./settings');
|
||||
const domainlist = require('./domainlist');
|
||||
@@ -128,9 +129,61 @@ module.exports = class AssetsService {
|
||||
* @param {Array} ids an array of Strings of asset_id
|
||||
* @return {Promise} resolves to list of Assets
|
||||
*/
|
||||
static findMultipleById(ids) {
|
||||
const query = ids.map((id) => ({id}));
|
||||
return AssetModel.find(query);
|
||||
static async findByIDs(ids) {
|
||||
|
||||
// Find the assets.
|
||||
let assets = await AssetModel.find({
|
||||
id: {
|
||||
$in: ids
|
||||
}
|
||||
});
|
||||
|
||||
// Return them in the right order.
|
||||
return ids.map((id) => assets.find((asset) => asset.id === id));
|
||||
}
|
||||
|
||||
static async updateURL(id, url) {
|
||||
|
||||
// Try to see if an asset already exists with the given url.
|
||||
let asset = await AssetsService.findByUrl(url);
|
||||
if (asset !== null) {
|
||||
throw errors.ErrAssetURLAlreadyExists;
|
||||
}
|
||||
|
||||
// Seems that there was no other asset with the same url, try and perform
|
||||
// the rename operation! An error may be thrown from this if the operation
|
||||
// fails. This is ok.
|
||||
await AssetModel.update({id}, {$set: {url}});
|
||||
}
|
||||
|
||||
static async merge(srcAssetID, dstAssetID) {
|
||||
|
||||
// Fetch both assets.
|
||||
let [srcAsset, dstAsset] = await AssetsService.findByIDs([srcAssetID, dstAssetID]);
|
||||
if (!srcAsset || !dstAsset) {
|
||||
throw errors.ErrNotFound;
|
||||
}
|
||||
|
||||
// Resolve the merge operation, this invloves moving all resources attached
|
||||
// to the src asset to the dst asset, and then removing the src asset.
|
||||
|
||||
// First, update all the old comments to the new asset.
|
||||
await CommentModel.update({
|
||||
asset_id: srcAssetID
|
||||
}, {
|
||||
$set: {
|
||||
asset_id: dstAssetID
|
||||
}
|
||||
}, {
|
||||
multi: true
|
||||
});
|
||||
|
||||
// Second remove the old asset.
|
||||
await AssetModel.remove({
|
||||
id: srcAssetID
|
||||
});
|
||||
|
||||
// That's it!
|
||||
}
|
||||
|
||||
static all(skip = null, limit = null) {
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
const AssetModel = require('../../../models/asset');
|
||||
const CommentModel = require('../../../models/comment');
|
||||
const AssetsService = require('../../../services/assets');
|
||||
const CommentsService = require('../../../services/comments');
|
||||
const SettingsService = require('../../../services/settings');
|
||||
|
||||
const url = require('url');
|
||||
const chai = require('chai');
|
||||
const expect = chai.expect;
|
||||
const chaiAsPromised = require('chai-as-promised');
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
|
||||
// Use the chai should.
|
||||
chai.should();
|
||||
|
||||
const settings = {id: '1', moderation: 'PRE', domains: {whitelist: ['new.test.com', 'test.com', 'override.test.com']}};
|
||||
const defaults = {url:'http://test.com'};
|
||||
|
||||
describe('services.AssetsService', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
const settings = {id: '1', moderation: 'PRE', domains: {whitelist: ['new.test.com', 'test.com', 'override.test.com']}};
|
||||
const defaults = {url:'http://test.com'};
|
||||
let asset;
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init(settings);
|
||||
|
||||
return SettingsService.init(settings).then(() => {
|
||||
return AssetModel.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true});
|
||||
});
|
||||
asset = await AssetModel.findOneAndUpdate({id: '1'}, {$setOnInsert: defaults}, {upsert: true, new: true});
|
||||
});
|
||||
|
||||
describe('#findById', ()=> {
|
||||
@@ -120,4 +127,79 @@ describe('services.AssetsService', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateURL', () => {
|
||||
|
||||
it('should change the url if the asset was found, and there was no conflict', async () => {
|
||||
let newURL = url.resolve(asset.url, '/new-url');
|
||||
|
||||
// Update the asset.
|
||||
await AssetsService.updateURL(asset.id, newURL);
|
||||
|
||||
// Check that the url was updated.
|
||||
let {url: databaseURL} = await AssetsService.findById(asset.id);
|
||||
|
||||
expect(databaseURL).to.equal(newURL);
|
||||
});
|
||||
|
||||
it('should error if the new url already exists', async () => {
|
||||
let newURL = url.resolve(asset.url, '/new-url');
|
||||
|
||||
// Create a new asset with our new URL.
|
||||
await AssetModel.findOneAndUpdate({id: '2'}, {$setOnInsert: {url: newURL}}, {upsert: true, new: true});
|
||||
|
||||
return AssetsService.updateURL(asset.id, newURL).should.eventually.be.rejected;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#merge', () => {
|
||||
|
||||
it('should error if either the src or the dst is missing', () => {
|
||||
return AssetsService.merge('not-found', asset.id).should.eventually.be.rejected;
|
||||
});
|
||||
|
||||
it('should merge the assets', async () => {
|
||||
let newURL = url.resolve(asset.url, '/new-url');
|
||||
|
||||
// Create a new asset with our new URL.
|
||||
await AssetModel.findOneAndUpdate({id: '2'}, {$setOnInsert: {url: newURL}}, {upsert: true, new: true});
|
||||
|
||||
// Create some comments on both assets.
|
||||
await CommentsService.publicCreate([
|
||||
{
|
||||
asset_id: '1',
|
||||
body: 'This is a comment!',
|
||||
status: 'ACCEPTED'
|
||||
},
|
||||
{
|
||||
asset_id: '1',
|
||||
body: 'This is a comment!',
|
||||
status: 'ACCEPTED'
|
||||
},
|
||||
{
|
||||
asset_id: '2',
|
||||
body: 'This is a comment!',
|
||||
status: 'ACCEPTED'
|
||||
},
|
||||
{
|
||||
asset_id: '2',
|
||||
body: 'This is a comment!',
|
||||
status: 'ACCEPTED'
|
||||
}
|
||||
]);
|
||||
|
||||
// Merge all the comments from asset 1 into asset 2, followed by deleting
|
||||
// asset 1.
|
||||
await AssetsService.merge('1', '2');
|
||||
|
||||
// Check to see if the comments are moved.
|
||||
expect(await CommentModel.find({asset_id: '1'}).count()).to.equal(0);
|
||||
expect(await CommentModel.find({asset_id: '2'}).count()).to.equal(4);
|
||||
|
||||
// Check to see if the asset was removed.
|
||||
expect(await AssetModel.findOne({id: '1'})).to.equal(null);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
+102
-49
@@ -3,6 +3,7 @@ const fs = require('fs');
|
||||
const CompressionPlugin = require('compression-webpack-plugin');
|
||||
const autoprefixer = require('autoprefixer');
|
||||
const precss = require('precss');
|
||||
const _ = require('lodash');
|
||||
const Copy = require('copy-webpack-plugin');
|
||||
const LicenseWebpackPlugin = require('license-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
@@ -10,23 +11,11 @@ const webpack = require('webpack');
|
||||
// Possibly load the config from the .env file (if there is one).
|
||||
require('dotenv').config();
|
||||
|
||||
let pluginsConfigPath;
|
||||
const {plugins, pluginsPath, PluginManager} = require('./plugins');
|
||||
const manager = new PluginManager(plugins);
|
||||
const targetPlugins = manager.section('targets').plugins;
|
||||
|
||||
let envPlugins = path.join(__dirname, 'plugins.env.js');
|
||||
let customPlugins = path.join(__dirname, 'plugins.json');
|
||||
let defaultPlugins = path.join(__dirname, 'plugins.default.json');
|
||||
|
||||
if (process.env.TALK_PLUGINS_JSON && process.env.TALK_PLUGINS_JSON.length > 0) {
|
||||
pluginsConfigPath = envPlugins;
|
||||
} else if (fs.existsSync(customPlugins)) {
|
||||
pluginsConfigPath = customPlugins;
|
||||
} else {
|
||||
pluginsConfigPath = defaultPlugins;
|
||||
}
|
||||
|
||||
console.log(`Using ${pluginsConfigPath} as the plugin configuration path`);
|
||||
|
||||
// Edit the build targets and embeds below.
|
||||
console.log(`Using ${pluginsPath} as the plugin configuration path`);
|
||||
|
||||
const buildTargets = [
|
||||
'coral-admin',
|
||||
@@ -37,47 +26,23 @@ const buildEmbeds = [
|
||||
'stream'
|
||||
];
|
||||
|
||||
//==============================================================================
|
||||
// Base Webpack Config
|
||||
//==============================================================================
|
||||
|
||||
const config = {
|
||||
devtool: 'cheap-module-source-map',
|
||||
entry: Object.assign({}, {
|
||||
'embed': [
|
||||
'babel-polyfill',
|
||||
path.join(__dirname, 'client/coral-embed/src/index')
|
||||
]
|
||||
}, buildTargets.reduce((entry, target) => {
|
||||
|
||||
// Add the entry for the bundle.
|
||||
entry[`${target}/bundle`] = [
|
||||
'babel-polyfill',
|
||||
path.join(__dirname, 'client/', target, '/src/index')
|
||||
];
|
||||
|
||||
return entry;
|
||||
}, {}), buildEmbeds.reduce((entry, embed) => {
|
||||
|
||||
// Add the entry for the bundle.
|
||||
entry[`embed/${embed}/bundle`] = [
|
||||
'babel-polyfill',
|
||||
path.join(__dirname, 'client/', `coral-embed-${embed}`, '/src/index')
|
||||
];
|
||||
|
||||
return entry;
|
||||
}, {})),
|
||||
output: {
|
||||
path: path.join(__dirname, 'dist'),
|
||||
publicPath: '/client/',
|
||||
filename: '[name].js',
|
||||
|
||||
// NOTE: this causes all exports to override the global.Coral, so no more
|
||||
// than one bundle.js can be included on a page.
|
||||
library: 'Coral'
|
||||
filename: '[name].js'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
loader: 'plugins-loader',
|
||||
test: /\.(json|js)$/,
|
||||
include: pluginsConfigPath
|
||||
include: pluginsPath
|
||||
},
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
@@ -127,7 +92,8 @@ const config = {
|
||||
plugins: [
|
||||
new LicenseWebpackPlugin({
|
||||
pattern: /^(MIT|ISC|BSD.*)$/,
|
||||
addUrl: true
|
||||
addUrl: true,
|
||||
suppressErrors: true
|
||||
}),
|
||||
new Copy([
|
||||
...buildEmbeds.map((embed) => ({
|
||||
@@ -156,7 +122,7 @@ const config = {
|
||||
alias: {
|
||||
'plugin-api': path.resolve(__dirname, 'plugin-api/'),
|
||||
plugins: path.resolve(__dirname, 'plugins/'),
|
||||
pluginsConfig: pluginsConfigPath
|
||||
pluginsConfig: pluginsPath
|
||||
},
|
||||
modules: [
|
||||
path.resolve(__dirname, 'plugins'),
|
||||
@@ -168,6 +134,10 @@ const config = {
|
||||
}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// Production configuration overrides
|
||||
//==============================================================================
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
config.plugins.push(new CompressionPlugin({
|
||||
asset: '[path].gz[query]',
|
||||
@@ -178,4 +148,87 @@ if (process.env.NODE_ENV === 'production') {
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = config;
|
||||
//==============================================================================
|
||||
// Entries
|
||||
//==============================================================================
|
||||
|
||||
// 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
|
||||
];
|
||||
|
||||
return entry;
|
||||
}, {})
|
||||
}, root);
|
||||
|
||||
module.exports = [
|
||||
|
||||
// Coral Embed
|
||||
applyConfig([
|
||||
|
||||
// Load in the root embed.
|
||||
{
|
||||
name: 'embed',
|
||||
path: path.join(__dirname, 'client/coral-embed/src/index')
|
||||
}
|
||||
|
||||
], {
|
||||
output: {
|
||||
library: 'Coral'
|
||||
}
|
||||
}),
|
||||
|
||||
// All framework targets/embeds/plugins.
|
||||
applyConfig([
|
||||
|
||||
// // Load in all the targets.
|
||||
...buildTargets.map((target) => ({
|
||||
name: `${target}/bundle`,
|
||||
path: path.join(__dirname, 'client/', target, '/src/index')
|
||||
})),
|
||||
|
||||
// Load in all the embeds.
|
||||
...buildEmbeds.map((embed) => ({
|
||||
name: `embed/${embed}/bundle`,
|
||||
path: path.join(__dirname, 'client/', `coral-embed-${embed}`, '/src/index')
|
||||
})),
|
||||
|
||||
// Load in all the plugin entries.
|
||||
...targetPlugins.reduce((entries, plugin) => {
|
||||
|
||||
// Introspect the path to find a targets folder.
|
||||
let folder = path.dirname(plugin.path);
|
||||
let files = fs.readdirSync(folder);
|
||||
|
||||
// While the folder does not contain the targets folder...
|
||||
while (!files.includes('targets')) {
|
||||
|
||||
// Try to go up a folder.
|
||||
folder = path.normalize(path.join(folder, '..'));
|
||||
|
||||
// And as long as we haven't gone too high
|
||||
if (!(folder.includes(path.join(__dirname, 'node_modules')) || !folder.includes(path.join(__dirname, 'plugins')))) {
|
||||
throw new Error(`target plugin ${plugin.name} does not have a 'targets' folder`);
|
||||
}
|
||||
|
||||
files = fs.readdirSync(folder);
|
||||
}
|
||||
|
||||
// List all targets available in that folder.
|
||||
folder = path.join(folder, 'targets');
|
||||
|
||||
let targets = fs.readdirSync(folder);
|
||||
if (targets.length === 0) {
|
||||
throw new Error(`target plugin ${plugin.name} has no targets in it's target folder ${folder}`);
|
||||
}
|
||||
|
||||
return entries.concat(targets.map((target) => ({
|
||||
name: `plugin/${plugin.name}/${target}/bundle`,
|
||||
path: path.join(folder, target, 'index')
|
||||
})));
|
||||
}, [])
|
||||
])
|
||||
];
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
version "0.8.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.8.6.tgz#b34fb880493ba835b0c067024ee70130d6f9bb68"
|
||||
|
||||
"@types/graphql@^0.9.0":
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.9.0.tgz#fccf859f0d2817687f210737dc3be48a18b1d754"
|
||||
"@types/graphql@^0.9.0", "@types/graphql@^0.9.1":
|
||||
version "0.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-0.9.1.tgz#b04ebe84bc997cc60dbea2ed4d0d4342c737f99d"
|
||||
|
||||
"@types/isomorphic-fetch@0.0.33":
|
||||
version "0.0.33"
|
||||
@@ -53,9 +53,9 @@
|
||||
"@types/express-serve-static-core" "*"
|
||||
"@types/mime" "*"
|
||||
|
||||
"@types/ws@0.0.38":
|
||||
version "0.0.38"
|
||||
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-0.0.38.tgz#42106fff4b422ca956734e29f0d73a6d893194d3"
|
||||
"@types/ws@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-3.0.0.tgz#4682a04d385484e73f7d8275cabc8b672d66143c"
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
@@ -2704,6 +2704,10 @@ es6-promise@^3.0.2, es6-promise@^3.2.1:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
|
||||
|
||||
es6-promise@^4.0.5:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.1.0.tgz#dda03ca8f9f89bc597e689842929de7ba8cebdf0"
|
||||
|
||||
es6-set@~0.1.5:
|
||||
version "0.1.5"
|
||||
resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1"
|
||||
@@ -2941,7 +2945,7 @@ event-stream@~3.3.0:
|
||||
stream-combiner "~0.0.4"
|
||||
through "~2.3.1"
|
||||
|
||||
eventemitter3@^2.0.2:
|
||||
eventemitter3@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-2.0.3.tgz#b5e1079b59fb5e1ba2771c0a993be060a58c99ba"
|
||||
|
||||
@@ -3631,13 +3635,15 @@ graphql-subscriptions@^0.2.0:
|
||||
dependencies:
|
||||
es6-promise "^3.2.1"
|
||||
|
||||
graphql-subscriptions@^0.3.0, graphql-subscriptions@^0.3.1:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-0.3.1.tgz#0cedc2d507420cf26cf414080b079f05402f0303"
|
||||
graphql-subscriptions@^0.4.3:
|
||||
version "0.4.3"
|
||||
resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-0.4.3.tgz#2aed6ba87551cc747742b793497ff24b22991867"
|
||||
dependencies:
|
||||
es6-promise "^3.2.1"
|
||||
"@types/graphql" "^0.9.1"
|
||||
es6-promise "^4.0.5"
|
||||
iterall "^1.1.1"
|
||||
|
||||
graphql-tag@^1.2.3, graphql-tag@^1.2.4:
|
||||
graphql-tag@^1.2.3:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-1.3.2.tgz#7abb3a8fd9f3415d07163314ed237061c785b759"
|
||||
|
||||
@@ -4456,6 +4462,10 @@ iterall@1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.0.3.tgz#e0b31958f835013c323ff0b10943829ac69aa4b7"
|
||||
|
||||
iterall@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.1.1.tgz#f7f0af11e9a04ec6426260f5019d9fcca4d50214"
|
||||
|
||||
jju@^1.1.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/jju/-/jju-1.3.0.tgz#dadd9ef01924bc728b03f2f7979bdbd62f7a2aaa"
|
||||
@@ -5777,10 +5787,6 @@ optionator@^0.8.1, optionator@^0.8.2:
|
||||
type-check "~0.3.2"
|
||||
wordwrap "~1.0.0"
|
||||
|
||||
options@>=0.0.5:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f"
|
||||
|
||||
os-browserify@^0.2.0:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f"
|
||||
@@ -7386,7 +7392,7 @@ rx@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782"
|
||||
|
||||
safe-buffer@^5.0.1:
|
||||
safe-buffer@^5.0.1, safe-buffer@~5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
|
||||
|
||||
@@ -7834,19 +7840,20 @@ stylus@0.54.5, stylus@~0.54.5:
|
||||
sax "0.5.x"
|
||||
source-map "0.1.x"
|
||||
|
||||
subscriptions-transport-ws@^0.5.5-alpha.0:
|
||||
version "0.5.5"
|
||||
resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.5.5.tgz#7715ed4306f532a9dd53c02cbd789d2ee8871bb9"
|
||||
subscriptions-transport-ws@^0.7.2:
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.7.2.tgz#26917fd4ba23fa37fcd28accb6fe36a040b3bbe9"
|
||||
dependencies:
|
||||
"@types/ws" "0.0.38"
|
||||
"@types/ws" "^3.0.0"
|
||||
backo2 "^1.0.2"
|
||||
eventemitter3 "^2.0.2"
|
||||
graphql "^0.9.1"
|
||||
graphql-subscriptions "^0.3.0"
|
||||
graphql-tag "^1.2.4"
|
||||
eventemitter3 "^2.0.3"
|
||||
graphql-subscriptions "^0.4.3"
|
||||
graphql-tag "^2.0.0"
|
||||
iterall "^1.1.1"
|
||||
lodash.assign "^4.2.0"
|
||||
lodash.isobject "^3.0.2"
|
||||
lodash.isstring "^4.0.1"
|
||||
ws "^1.1.0"
|
||||
ws "^3.0.0"
|
||||
|
||||
sugarss@^0.2.0:
|
||||
version "0.2.0"
|
||||
@@ -8190,9 +8197,9 @@ uid-safe@2.1.4, uid-safe@~2.1.4:
|
||||
dependencies:
|
||||
random-bytes "~1.0.0"
|
||||
|
||||
ultron@1.0.x:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa"
|
||||
ultron@~1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.0.tgz#b07a2e6a541a815fc6a34ccd4533baec307ca864"
|
||||
|
||||
undefsafe@0.0.3:
|
||||
version "0.0.3"
|
||||
@@ -8510,12 +8517,12 @@ write@^0.2.1:
|
||||
dependencies:
|
||||
mkdirp "^0.5.1"
|
||||
|
||||
ws@^1.1.0:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.4.tgz#57f40d036832e5f5055662a397c4de76ed66bf61"
|
||||
ws@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-3.0.0.tgz#98ddb00056c8390cb751e7788788497f99103b6c"
|
||||
dependencies:
|
||||
options ">=0.0.5"
|
||||
ultron "1.0.x"
|
||||
safe-buffer "~5.0.1"
|
||||
ultron "~1.1.0"
|
||||
|
||||
x-xss-protection@1.0.0:
|
||||
version "1.0.0"
|
||||
|
||||
Reference in New Issue
Block a user