mirror of
https://github.com/wassname/talk.git
synced 2026-07-03 09:12:07 +08:00
First implementation of ignore user plugin
This commit is contained in:
@@ -21,5 +21,6 @@ plugins/*
|
||||
!plugins/talk-plugin-sort-most-respected
|
||||
!plugins/talk-plugin-author-menu
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-ignore-user
|
||||
|
||||
node_modules
|
||||
|
||||
@@ -37,5 +37,6 @@ plugins/*
|
||||
!plugins/talk-plugin-sort-most-respected
|
||||
!plugins/talk-plugin-author-menu
|
||||
!plugins/talk-plugin-member-since
|
||||
!plugins/talk-plugin-ignore-user
|
||||
|
||||
**/node_modules/*
|
||||
|
||||
@@ -126,7 +126,6 @@ class AllCommentsPane extends React.Component {
|
||||
root,
|
||||
comments,
|
||||
commentClassNames,
|
||||
ignoreUser,
|
||||
setActiveReplyBox,
|
||||
activeReplyBox,
|
||||
notify,
|
||||
@@ -173,7 +172,6 @@ class AllCommentsPane extends React.Component {
|
||||
currentUser={currentUser}
|
||||
postFlag={postFlag}
|
||||
postDontAgree={postDontAgree}
|
||||
ignoreUser={ignoreUser}
|
||||
commentIsIgnored={commentIsIgnored}
|
||||
loadMore={loadNewReplies}
|
||||
deleteAction={deleteAction}
|
||||
|
||||
@@ -66,17 +66,6 @@
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.topRight .popover {
|
||||
margin-top: 1em;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.topRight .link.active,
|
||||
.topRight .active .link {
|
||||
padding-bottom: 0.125em;
|
||||
border-bottom: 2px solid currentColor;
|
||||
}
|
||||
|
||||
.editCommentForm {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@@ -175,4 +164,4 @@
|
||||
|
||||
.username {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import mapValues from 'lodash/mapValues';
|
||||
import LoadMore from './LoadMore';
|
||||
import {getEditableUntilDate} from './util';
|
||||
import {findCommentWithId} from '../graphql/utils';
|
||||
import {TopRightMenu} from './TopRightMenu';
|
||||
import CommentContent from './CommentContent';
|
||||
import Slot from 'coral-framework/components/Slot';
|
||||
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
|
||||
@@ -187,9 +186,6 @@ export default class Comment extends React.Component {
|
||||
// given a comment, return whether it should be rendered as ignored
|
||||
commentIsIgnored: PropTypes.func,
|
||||
|
||||
// dispatch action to ignore another user
|
||||
ignoreUser: PropTypes.func,
|
||||
|
||||
// edit a comment, passed (id, asset_id, { body })
|
||||
editComment: PropTypes.func,
|
||||
|
||||
@@ -317,7 +313,6 @@ export default class Comment extends React.Component {
|
||||
comment,
|
||||
postFlag,
|
||||
parentId,
|
||||
ignoreUser,
|
||||
highlighted,
|
||||
postComment,
|
||||
currentUser,
|
||||
@@ -480,16 +475,6 @@ export default class Comment extends React.Component {
|
||||
}
|
||||
</span>
|
||||
}
|
||||
{ isActive && (currentUser && (comment.user.id !== currentUser.id)) &&
|
||||
|
||||
/* TopRightMenu allows currentUser to ignore other users' comments */
|
||||
<span className={cn(styles.topRight, styles.topRightMenu)}>
|
||||
<TopRightMenu
|
||||
comment={comment}
|
||||
ignoreUser={ignoreUser}
|
||||
notify={notify} />
|
||||
</span>
|
||||
}
|
||||
{ !isActive &&
|
||||
<InactiveCommentLabel status={comment.status}/>
|
||||
}
|
||||
@@ -600,7 +585,6 @@ export default class Comment extends React.Component {
|
||||
postFlag={postFlag}
|
||||
deleteAction={deleteAction}
|
||||
loadMore={loadMore}
|
||||
ignoreUser={ignoreUser}
|
||||
charCountEnable={charCountEnable}
|
||||
maxCharCount={maxCharCount}
|
||||
showSignInDialog={showSignInDialog}
|
||||
|
||||
@@ -64,7 +64,6 @@ class Stream extends React.Component {
|
||||
postDontAgree,
|
||||
deleteAction,
|
||||
showSignInDialog,
|
||||
ignoreUser,
|
||||
loadNewReplies,
|
||||
auth: {user},
|
||||
emit,
|
||||
@@ -90,7 +89,6 @@ class Stream extends React.Component {
|
||||
data={data}
|
||||
root={root}
|
||||
commentClassNames={commentClassNames}
|
||||
ignoreUser={ignoreUser}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
activeReplyBox={activeReplyBox}
|
||||
notify={notify}
|
||||
@@ -133,7 +131,6 @@ class Stream extends React.Component {
|
||||
postDontAgree,
|
||||
deleteAction,
|
||||
showSignInDialog,
|
||||
ignoreUser,
|
||||
activeStreamTab,
|
||||
setActiveStreamTab,
|
||||
loadNewReplies,
|
||||
@@ -183,7 +180,6 @@ class Stream extends React.Component {
|
||||
root={root}
|
||||
comments={comments}
|
||||
commentClassNames={commentClassNames}
|
||||
ignoreUser={ignoreUser}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
activeReplyBox={activeReplyBox}
|
||||
notify={notify}
|
||||
@@ -322,9 +318,6 @@ Stream.propTypes = {
|
||||
notify: PropTypes.func.isRequired,
|
||||
postComment: PropTypes.func.isRequired,
|
||||
|
||||
// dispatch action to ignore another user
|
||||
ignoreUser: React.PropTypes.func,
|
||||
|
||||
// edit a comment, passed (id, asset_id, { body })
|
||||
editComment: React.PropTypes.func
|
||||
};
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import {IgnoreUserWizard} from './IgnoreUserWizard';
|
||||
import Toggleable from './Toggleable';
|
||||
|
||||
// TopRightMenu appears as a dropdown in the top right of the comment.
|
||||
// when you click the down cehvron, it expands and shows IgnoreUserWizard
|
||||
// when you click 'cancel' in the wizard, it closes the menu
|
||||
export class TopRightMenu extends React.Component {
|
||||
static propTypes = {
|
||||
|
||||
// comment on which this menu appears
|
||||
comment: PropTypes.shape({
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired
|
||||
}).isRequired
|
||||
}).isRequired,
|
||||
ignoreUser: PropTypes.func,
|
||||
|
||||
// show notification to the user (e.g. for errors)
|
||||
notify: PropTypes.func.isRequired,
|
||||
}
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
timesReset: 0
|
||||
};
|
||||
}
|
||||
render() {
|
||||
const {comment, ignoreUser, notify} = this.props;
|
||||
|
||||
// timesReset is used as Toggleable key so it re-renders on reset (closing the toggleable)
|
||||
const reset = () => this.setState({timesReset: this.state.timesReset + 1});
|
||||
const ignoreUserAndCloseMenuAndNotifyOnError = async ({id}) => {
|
||||
|
||||
// close menu
|
||||
reset();
|
||||
|
||||
// ignore user
|
||||
try {
|
||||
await ignoreUser({id});
|
||||
} catch (error) {
|
||||
notify('error', 'Failed to ignore user');
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Toggleable key={this.state.timesReset} className="talk-stream-comment-chevron">
|
||||
<div style={{position: 'absolute', right: 0, zIndex: 1}}>
|
||||
<IgnoreUserWizard
|
||||
user={comment.user}
|
||||
cancel={reset}
|
||||
ignoreUser={ignoreUserAndCloseMenuAndNotifyOnError}
|
||||
/>
|
||||
</div>
|
||||
</Toggleable>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {bindActionCreators} from 'redux';
|
||||
import {ADDTL_COMMENTS_ON_LOAD_MORE, THREADING_LEVEL} from '../constants/stream';
|
||||
import {
|
||||
withPostComment, withPostFlag, withPostDontAgree,
|
||||
withDeleteAction, withIgnoreUser, withEditComment
|
||||
withDeleteAction, withEditComment
|
||||
} from 'coral-framework/graphql/mutations';
|
||||
|
||||
import * as authActions from 'coral-embed-stream/src/actions/auth';
|
||||
@@ -374,7 +374,6 @@ export default compose(
|
||||
withPostComment,
|
||||
withPostFlag,
|
||||
withPostDontAgree,
|
||||
withIgnoreUser,
|
||||
withDeleteAction,
|
||||
withEditComment,
|
||||
)(StreamContainer);
|
||||
|
||||
@@ -108,18 +108,6 @@ export default {
|
||||
`,
|
||||
},
|
||||
mutations: {
|
||||
IgnoreUser: ({variables}) => ({
|
||||
updateQueries: {
|
||||
CoralEmbedStream_Embed: (previousData) => {
|
||||
const ignoredUserId = variables.id;
|
||||
const updated = update(previousData, {me: {ignoredUsers: {$push: [{
|
||||
id: ignoredUserId,
|
||||
__typename: 'User',
|
||||
}]}}});
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
}),
|
||||
StopIgnoringUser: ({variables}) => ({
|
||||
updateQueries: {
|
||||
CoralEmbedStream_Profile: (previousData) => {
|
||||
|
||||
@@ -293,7 +293,7 @@ export const withIgnoreUser = withMutation(
|
||||
}
|
||||
`, {
|
||||
props: ({mutate}) => ({
|
||||
ignoreUser: ({id}) => {
|
||||
ignoreUser: (id) => {
|
||||
return mutate({
|
||||
variables: {
|
||||
id,
|
||||
|
||||
@@ -5,3 +5,4 @@ export {default as withFragments} from 'coral-framework/hocs/withFragments';
|
||||
export {default as excludeIf} from 'coral-framework/hocs/excludeIf';
|
||||
export {default as connect} from 'coral-framework/hocs/connect';
|
||||
export {default as withEmit} from 'coral-framework/hocs/withEmit';
|
||||
export {withIgnoreUser} from 'coral-framework/graphql/mutations';
|
||||
|
||||
@@ -35,3 +35,7 @@
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@ import {Slot} from 'plugin-api/beta/client/components';
|
||||
export default ({data, root, asset, comment, contentSlot}) => {
|
||||
if (contentSlot) {
|
||||
return (
|
||||
<Slot
|
||||
fill={contentSlot}
|
||||
data={data}
|
||||
queryData={{asset, root, comment}}
|
||||
/>
|
||||
<div className={styles.menu}>
|
||||
<Slot
|
||||
fill={contentSlot}
|
||||
data={data}
|
||||
queryData={{asset, root, comment}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,6 +23,7 @@ export default ({data, root, asset, comment, contentSlot}) => {
|
||||
queryData={{asset, root, comment}}
|
||||
/>
|
||||
<Slot
|
||||
className={styles.actions}
|
||||
fill={'authorMenuActions'}
|
||||
data={data}
|
||||
queryData={{asset, root, comment}}
|
||||
|
||||
@@ -22,6 +22,9 @@ class AuthorNameContainer extends React.Component {
|
||||
this.setState({
|
||||
menuVisible: false
|
||||
});
|
||||
if (this.props.contentSlot) {
|
||||
this.props.resetContentSlot();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +34,7 @@ class AuthorNameContainer extends React.Component {
|
||||
root={this.props.root}
|
||||
asset={this.props.asset}
|
||||
comment={this.props.comment}
|
||||
contentSlot={this.props.contentSlot}
|
||||
menuVisible={this.state.menuVisible}
|
||||
toggleMenu={this.toggleMenu}
|
||||
hideMenu={this.hideMenu}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"presets": [
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
"add-module-exports",
|
||||
"transform-class-properties",
|
||||
"transform-decorators-legacy",
|
||||
"transform-object-assign",
|
||||
"transform-object-rest-spread",
|
||||
"transform-async-to-generator",
|
||||
"transform-react-jsx"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error",
|
||||
"no-console": ["warn", { "allow": ["warn", "error"] }]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
.root {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.memberSince {
|
||||
color: #484747;
|
||||
margin-left: 4px;
|
||||
letter-spacing: 0.2px;
|
||||
font-size: 0.9em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 1.4em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import styles from './IgnoreUserAction.css';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
|
||||
export default ({ignoreUser}) => (
|
||||
<button className={styles.button} onClick={ignoreUser}>
|
||||
{t('talk-plugin-ignore-user.ignore')}
|
||||
</button>
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
.root {
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import styles from './IgnoreUserConfirmation.css';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
|
||||
export default ({ignoreUser}) => (
|
||||
<div className={styles.root}>
|
||||
Do you really want to ignore this user?
|
||||
<div className={styles.actions}>
|
||||
<button className={styles.button} onClick={ignoreUser}>
|
||||
{t('talk-plugin-ignore-user.ignore')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import IgnoreUserAction from '../components/IgnoreUserAction';
|
||||
import {compose} from 'react-apollo';
|
||||
import {connect, withFragments} from 'plugin-api/beta/client/hocs';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {setContentSlot} from 'plugins/talk-plugin-author-menu/client/actions';
|
||||
import IgnoreUserConfirmation from './IgnoreUserConfirmation';
|
||||
|
||||
class IgnoreUserActionContainer extends React.Component {
|
||||
|
||||
ignoreUser = () => {
|
||||
this.props.setContentSlot('ignoreUserConfirmation');
|
||||
};
|
||||
|
||||
render() {
|
||||
return <IgnoreUserAction
|
||||
ignoreUser={this.ignoreUser}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
setContentSlot,
|
||||
}, dispatch);
|
||||
|
||||
const withIgnoreUserActionFragments = withFragments(IgnoreUserConfirmation.fragments);
|
||||
|
||||
const enhance = compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
withIgnoreUserActionFragments,
|
||||
);
|
||||
|
||||
export default enhance(IgnoreUserActionContainer);
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import IgnoreUserConfirmation from '../components/IgnoreUserConfirmation';
|
||||
import {compose, gql} from 'react-apollo';
|
||||
import {connect, withFragments, withIgnoreUser} from 'plugin-api/beta/client/hocs';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {setContentSlot} from 'plugins/talk-plugin-author-menu/client/actions';
|
||||
|
||||
class IgnoreUserConfirmationContainer extends React.Component {
|
||||
|
||||
ignoreUser = () => {
|
||||
this.props.ignoreUser(this.props.comment.user.id);
|
||||
this.props.setContentSlot(null);
|
||||
};
|
||||
|
||||
render() {
|
||||
return <IgnoreUserConfirmation
|
||||
username={this.props.comment.username}
|
||||
ignoreUser={this.ignoreUser}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators({
|
||||
setContentSlot,
|
||||
}, dispatch);
|
||||
|
||||
const withIgnoreUserConfirmationFragments = withFragments({
|
||||
comment: gql`
|
||||
fragment TalkIgnoreUser_IgnoreUserConfirmation_comment on Comment {
|
||||
user {
|
||||
id
|
||||
username
|
||||
}
|
||||
}`,
|
||||
});
|
||||
|
||||
const enhance = compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
withIgnoreUserConfirmationFragments,
|
||||
withIgnoreUser,
|
||||
);
|
||||
|
||||
export default enhance(IgnoreUserConfirmationContainer);
|
||||
@@ -0,0 +1,26 @@
|
||||
import IgnoreUserAction from './containers/IgnoreUserAction';
|
||||
import IgnoreUserConfirmation from './containers/IgnoreUserConfirmation';
|
||||
import translations from './translations.yml';
|
||||
import update from 'immutability-helper';
|
||||
|
||||
export default {
|
||||
mutations: {
|
||||
IgnoreUser: ({variables}) => ({
|
||||
updateQueries: {
|
||||
CoralEmbedStream_Embed: (previousData) => {
|
||||
const ignoredUserId = variables.id;
|
||||
const updated = update(previousData, {me: {ignoredUsers: {$push: [{
|
||||
id: ignoredUserId,
|
||||
__typename: 'User',
|
||||
}]}}});
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
slots: {
|
||||
authorMenuActions: [IgnoreUserAction],
|
||||
ignoreUserConfirmation: [IgnoreUserConfirmation]
|
||||
},
|
||||
translations
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
en:
|
||||
talk-plugin-ignore-user:
|
||||
ignore: "ignore"
|
||||
es:
|
||||
talk-plugin-ignore-user:
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = {};
|
||||
Reference in New Issue
Block a user