mirror of
https://github.com/wassname/talk.git
synced 2026-08-09 12:30:42 +08:00
Support live updates
This commit is contained in:
@@ -1 +1 @@
|
||||
export {withReaction} from 'coral-framework/hocs';
|
||||
export {default as withReaction} from './withReaction';
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
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, 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';
|
||||
import * as PropTypes from 'prop-types';
|
||||
|
||||
export default (reaction) => (WrappedComponent) => {
|
||||
if (typeof reaction !== 'string') {
|
||||
console.error('Reaction must be a valid string');
|
||||
return null;
|
||||
}
|
||||
|
||||
let instances = 0;
|
||||
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,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
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 = ({[`${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 = ({[`${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--;
|
||||
if (instances === 0) {
|
||||
try {
|
||||
createdSubscription.unsubscribe();
|
||||
deletedSubscription.unsubscribe();
|
||||
}
|
||||
catch(e) {
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {comment} = this.props;
|
||||
|
||||
const reactionSummary = getMyActionSummary(
|
||||
`${Reaction}ActionSummary`,
|
||||
comment
|
||||
);
|
||||
|
||||
const count = getTotalActionCount(
|
||||
`${Reaction}ActionSummary`,
|
||||
comment
|
||||
);
|
||||
|
||||
const alreadyReacted = !!reactionSummary;
|
||||
|
||||
const withReactionProps = {reactionSummary, count, alreadyReacted};
|
||||
|
||||
return <WrappedComponent {...this.props} {...withReactionProps} />;
|
||||
}
|
||||
}
|
||||
|
||||
const withDeleteReaction = graphql(
|
||||
gql`
|
||||
mutation Delete${Reaction}Action($input: Delete${Reaction}ActionInput!) {
|
||||
delete${Reaction}Action(input: $input) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
props: ({mutate, ownProps}) => ({
|
||||
deleteReaction: () => {
|
||||
|
||||
const reactionSummary = getMyActionSummary(
|
||||
`${Reaction}ActionSummary`,
|
||||
ownProps.comment
|
||||
);
|
||||
|
||||
const id = reactionSummary.current_user.id;
|
||||
const item_id = ownProps.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 = graphql(
|
||||
gql`
|
||||
mutation Create${Reaction}Action($input: Create${Reaction}ActionInput!) {
|
||||
create${Reaction}Action(input: $input) {
|
||||
${reaction} {
|
||||
id
|
||||
}
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
props: ({mutate, ownProps}) => ({
|
||||
postReaction: () => {
|
||||
|
||||
const input = {
|
||||
item_id: ownProps.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);
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
const wrapResponse = require('../../../graph/helpers/response');
|
||||
const {SEARCH_OTHER_USERS} = require('../../../perms/constants');
|
||||
|
||||
function getReactionConfig(reaction) {
|
||||
const Reaction = reaction.charAt(0).toUpperCase() + reaction.slice(1);
|
||||
@@ -6,82 +7,142 @@ function getReactionConfig(reaction) {
|
||||
const typeDefs = `
|
||||
enum ACTION_TYPE {
|
||||
|
||||
# Represents a ${Reaction}.
|
||||
${REACTION}
|
||||
# Represents a ${Reaction}.
|
||||
${REACTION}
|
||||
}
|
||||
|
||||
enum ASSET_METRICS_SORT {
|
||||
|
||||
# Represents a ${Reaction}Action.
|
||||
${REACTION}
|
||||
# Represents a ${Reaction}Action.
|
||||
${REACTION}
|
||||
}
|
||||
|
||||
input Create${Reaction}Input {
|
||||
input Create${Reaction}ActionInput {
|
||||
|
||||
# The item's id for which we are to create a ${reaction}.
|
||||
item_id: ID!
|
||||
# The item's id for which we are to create a ${reaction}.
|
||||
item_id: ID!
|
||||
}
|
||||
|
||||
# The type of the item for which we are to create the ${reaction}.
|
||||
item_type: ACTION_ITEM_TYPE!
|
||||
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 ID of the action.
|
||||
id: ID!
|
||||
|
||||
# The author of the action.
|
||||
user: User
|
||||
# The author of the action.
|
||||
user: User
|
||||
|
||||
# The time when the Action was updated.
|
||||
updated_at: Date
|
||||
# The time when the Action was updated.
|
||||
updated_at: Date
|
||||
|
||||
# The time when the Action was created.
|
||||
created_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 count of actions with this group.
|
||||
count: Int
|
||||
|
||||
# The current user's action.
|
||||
current_user: ${Reaction}Action
|
||||
# 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 ${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
|
||||
# Number of unique actionable types that are referenced by the ${reaction}s.
|
||||
actionableItemCount: Int
|
||||
}
|
||||
|
||||
type Create${Reaction}Response implements Response {
|
||||
type Create${Reaction}ActionResponse implements Response {
|
||||
|
||||
# The ${reaction} that was created.
|
||||
${reaction}: ${Reaction}Action
|
||||
# The ${reaction} that was created.
|
||||
${reaction}: ${Reaction}Action
|
||||
|
||||
# An array of errors relating to the mutation that occurred.
|
||||
errors: [UserError!]
|
||||
# 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}(${reaction}: Create${Reaction}Input!): Create${Reaction}Response
|
||||
# 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}`]: (_, {[reaction]: {item_id, item_type}}, {mutators: {Action}}) => {
|
||||
return wrapResponse(reaction)(Action.create({item_id, item_type, action_type: REACTION}));
|
||||
[`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) => {
|
||||
pubsub.publish(`${reaction}ActionCreated`, {action, comment});
|
||||
return Promise.resolve(action);
|
||||
});
|
||||
});
|
||||
return wrapResponse(reaction)(response);
|
||||
},
|
||||
[`delete${Reaction}Action`]: (_, {input: {id}}, {mutators: {Action}, pubsub, loaders: {Comments}}) => {
|
||||
const response = Action.delete({id})
|
||||
.then((action) => {
|
||||
return Comments.get.load(action.item_id).then((comment) => {
|
||||
pubsub.publish(`${reaction}ActionDeleted`, {action, comment});
|
||||
return Promise.resolve(action);
|
||||
});
|
||||
});
|
||||
return wrapResponse(reaction)(response);
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -106,7 +167,19 @@ function getReactionConfig(reaction) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
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,
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user