diff --git a/components/ActionMenu/ReportAbuseMenuItem.js b/components/ActionMenu/ReportAbuseMenuItem.js new file mode 100644 index 00000000..eee37de0 --- /dev/null +++ b/components/ActionMenu/ReportAbuseMenuItem.js @@ -0,0 +1,50 @@ +import { t } from 'ttag'; +import MenuItem from '@material-ui/core/MenuItem'; +import { makeStyles } from '@material-ui/core/styles'; + +import { getSpamReportUrl } from 'constants/urls'; +import useCurrentUser from 'lib/useCurrentUser'; + +const useStyles = makeStyles({ + link: { + color: 'inherit', + textDecoration: 'none', + }, +}); + +/** + * + * @param {string} itemUserId - the author's user ID of the item + */ +export function useCanReportAbuse(itemUserId) { + const currentUser = useCurrentUser(); + return currentUser && currentUser.id !== itemUserId; +} + +/** + * @param {object} props + * @param {string} props.userId - spammer's user ID + * @param {'replyRequest' | 'articleReplyFeedback' | 'reply'} props.itemType - reported spam item type + * @param {string} props.itemId - reply ID for reply; article ID for replyRequest; article ID,reply ID (separated in comma) for article reply feedback. + * + * @returns {string} Pre-filled URL to the google form that reports spam. + */ +function ReportAbuseMenuItem(props) { + const classes = useStyles(); + const canReportAbuse = useCanReportAbuse(props.userId); + + if (!canReportAbuse) return null; + + return ( + + {t`Report abuse`} + + ); +} + +export default ReportAbuseMenuItem; diff --git a/components/ActionMenu/index.js b/components/ActionMenu/index.js index 7d7ff1f6..a4eb9bff 100644 --- a/components/ActionMenu/index.js +++ b/components/ActionMenu/index.js @@ -1,2 +1,5 @@ import ActionMenu from './ActionMenu'; export default ActionMenu; + +import ReportAbuseMenuItem, { useCanReportAbuse } from './ReportAbuseMenuItem'; +export { ReportAbuseMenuItem, useCanReportAbuse }; diff --git a/components/ArticleReply/ArticleReply.js b/components/ArticleReply/ArticleReply.js index abd86eae..20cad5b7 100644 --- a/components/ArticleReply/ArticleReply.js +++ b/components/ArticleReply/ArticleReply.js @@ -56,12 +56,14 @@ const ArticleReplyData = gql` } ...ArticleReplySummaryData ...ArticleReplyFeedbackControlData + ...ReplyActionsData } ${Hyperlinks.fragments.HyperlinkData} ${ArticleReplyFeedbackControl.fragments.ArticleReplyFeedbackControlData} ${ReplyInfo.fragments.replyInfo} ${Avatar.fragments.AvatarData} ${ArticleReplySummary.fragments.ArticleReplySummaryData} + ${ReplyActions.fragments.ReplyActionsData} `; const ArticleReplyForUser = gql` @@ -71,9 +73,11 @@ const ArticleReplyForUser = gql` replyId canUpdateStatus ...ArticleReplyFeedbackControlDataForUser + ...ReplyActionsDataForUser } ${ArticleReplyFeedbackControl.fragments .ArticleReplyFeedbackControlDataForUser} + ${ReplyActions.fragments.ReplyActionsDataForUser} `; const ArticleReply = React.memo(({ articleReply }) => { diff --git a/components/ArticleReply/ReplyActions.js b/components/ArticleReply/ReplyActions.js index e3a1cb81..d0b2a9cc 100644 --- a/components/ArticleReply/ReplyActions.js +++ b/components/ArticleReply/ReplyActions.js @@ -3,9 +3,27 @@ import gql from 'graphql-tag'; import { useMutation } from '@apollo/react-hooks'; import { t } from 'ttag'; -import ActionMenu from 'components/ActionMenu'; +import ActionMenu, { + ReportAbuseMenuItem, + useCanReportAbuse, +} from 'components/ActionMenu'; import { MenuItem } from '@material-ui/core'; +const ReplyActionsData = gql` + fragment ReplyActionsData on ArticleReply { + articleId + replyId + userId + status + } +`; + +const ReplyActionsDataForUser = gql` + fragment ReplyActionsDataForUser on ArticleReply { + canUpdateStatus + } +`; + const UPDATE_ARTICLE_REPLY_STATUS = gql` mutation UpdateArticleReplyStatus( $articleId: String! @@ -17,14 +35,17 @@ const UPDATE_ARTICLE_REPLY_STATUS = gql` replyId: $replyId status: $status ) { - articleId - replyId - status + ...ReplyActionsData + ...ReplyActionsDataForUser } + ${ReplyActionsData} + ${ReplyActionsDataForUser} } `; const ReplyActions = ({ articleReply }) => { + const canReportAbuse = useCanReportAbuse(articleReply.userId); + const [ updateArticleReplyStatus, { loading: updatingArticleReplyStatus }, @@ -50,20 +71,34 @@ const ReplyActions = ({ articleReply }) => { }); }, [updateArticleReplyStatus]); - if (!articleReply.canUpdateStatus) return null; + if (!articleReply.canUpdateStatus && !canReportAbuse) return null; return ( - - {articleReply.status === 'NORMAL' ? t`Delete` : t`Restore`} - + {articleReply.canUpdateStatus && ( + + {articleReply.status === 'NORMAL' ? t`Delete` : t`Restore`} + + )} + {canReportAbuse && ( + + )} ); }; +ReplyActions.fragments = { + ReplyActionsData, + ReplyActionsDataForUser, +}; + export default ReplyActions; diff --git a/components/ArticleReplyFeedbackControl/Feedback.js b/components/ArticleReplyFeedbackControl/Feedback.js index 4dbab608..08d2128f 100644 --- a/components/ArticleReplyFeedbackControl/Feedback.js +++ b/components/ArticleReplyFeedbackControl/Feedback.js @@ -2,6 +2,10 @@ import { makeStyles } from '@material-ui/core/styles'; import { Box } from '@material-ui/core'; import gql from 'graphql-tag'; import Avatar from 'components/AppLayout/Widgets/Avatar'; +import ActionMenu, { + ReportAbuseMenuItem, + useCanReportAbuse, +} from 'components/ActionMenu'; const useStyles = makeStyles(theme => ({ root: { @@ -17,16 +21,27 @@ const useStyles = makeStyles(theme => ({ }, })); -function Feedback({ feedback }) { - const classes = useStyles(); +function Feedback({ feedback, articleId, replyId }) { + const canReportAbuse = useCanReportAbuse(feedback.userId); + const comment = (feedback.comment || '').trim(); + const classes = useStyles({ comment }); return (
{feedback.user?.name}
-
{feedback.comment}
+
{comment}
+ {comment && canReportAbuse && ( + + + + )}
); } @@ -35,6 +50,7 @@ Feedback.fragments = { ReasonDisplayFeedbackData: gql` fragment ReasonDisplayFeedbackData on ArticleReplyFeedback { id + userId user { name ...AvatarData diff --git a/components/ArticleReplyFeedbackControl/ReasonsDisplay.js b/components/ArticleReplyFeedbackControl/ReasonsDisplay.js index 30aafaa8..60dcafd7 100644 --- a/components/ArticleReplyFeedbackControl/ReasonsDisplay.js +++ b/components/ArticleReplyFeedbackControl/ReasonsDisplay.js @@ -126,14 +126,24 @@ function ReasonsDisplay({ articleReply, onSizeChange = () => {} }) { {feedbacks .filter(({ vote }) => vote === 'UPVOTE') .map(feedback => ( - + ))} {feedbacks .filter(({ vote }) => vote === 'DOWNVOTE') .map(feedback => ( - + ))} diff --git a/components/Feedback.js b/components/Feedback.js deleted file mode 100644 index b062c026..00000000 --- a/components/Feedback.js +++ /dev/null @@ -1,47 +0,0 @@ -import gql from 'graphql-tag'; -import { makeStyles } from '@material-ui/core/styles'; -import Avatar from 'components/AppLayout/Widgets/Avatar'; -import { Box } from '@material-ui/core'; - -const useStyles = makeStyles(theme => ({ - root: { - marginTop: 16, - display: 'flex', - borderBottom: `1px solid ${theme.palette.secondary[100]}`, - alignItems: ({ comment }) => (comment ? 'flex-start' : 'center'), - paddingBottom: 10, - }, - name: { - color: ({ comment }) => - comment ? theme.palette.primary[500] : theme.palette.secondary[300], - }, -})); - -function Feedback({ comment, user }) { - const classes = useStyles({ comment }); - return ( -
- - -
{user.name}
-
{comment}
-
-
- ); -} - -Feedback.fragments = { - Feedback: gql` - fragment Feedback on ArticleReplyFeedback { - id - user { - id - name - avatarUrl - } - comment - } - `, -}; - -export default Feedback; diff --git a/components/ReplyRequestReason/ReplyRequestReason.js b/components/ReplyRequestReason/ReplyRequestReason.js index 02e0ff06..9cffedd1 100644 --- a/components/ReplyRequestReason/ReplyRequestReason.js +++ b/components/ReplyRequestReason/ReplyRequestReason.js @@ -1,11 +1,16 @@ import React from 'react'; +import gql from 'graphql-tag'; +import PropTypes from 'prop-types'; import { useMutation } from '@apollo/react-hooks'; import { makeStyles } from '@material-ui/core/styles'; import { Box, Button } from '@material-ui/core'; + import { ThumbUpIcon, ThumbDownIcon } from 'components/icons'; import Avatar from 'components/AppLayout/Widgets/Avatar'; -import gql from 'graphql-tag'; -import PropTypes from 'prop-types'; +import ActionMenu, { + ReportAbuseMenuItem, + useCanReportAbuse, +} from 'components/ActionMenu'; const useStyles = makeStyles(theme => ({ root: { @@ -77,7 +82,7 @@ const UPDATE_VOTE = gql` ${ReplyRequestInfo} `; -function ReplyRequestReason({ replyRequest }) { +function ReplyRequestReason({ replyRequest, articleId }) { const { id: replyRequestId, reason: replyRequestReason, @@ -87,6 +92,7 @@ function ReplyRequestReason({ replyRequest }) { user, } = replyRequest; + const canReportAbuse = useCanReportAbuse(user.id); const [voteReason, { loading }] = useMutation(UPDATE_VOTE); const handleVote = vote => { voteReason({ variables: { vote, replyRequestId } }); @@ -132,6 +138,17 @@ function ReplyRequestReason({ replyRequest }) { + {canReportAbuse && ( + + + + + + )} ); } diff --git a/constants/urls.js b/constants/urls.js index 47ca4dd7..d9a0c8c5 100644 --- a/constants/urls.js +++ b/constants/urls.js @@ -21,3 +21,16 @@ export const FACEBOOK_SHARE_URL_PREFIX = export const DONATION_URL = 'https://ocf.neticrm.tw/civicrm/contribute/transact?id=48'; + +/** + * @param {object} params + * @param {string} params.userId - spammer's user ID + * @param {'replyRequest' | 'articleReplyFeedback' | 'reply'} params.itemType - reported spam item type + * @param {string} params.itemId - reply ID for reply; article ID for replyRequest; article ID,reply ID (separated in comma) for article reply feedback. + * + * @returns {string} Pre-filled URL to the google form that reports spam. + */ +export const getSpamReportUrl = ({ userId, itemType, itemId }) => { + // Prefilled URL as constant, manually edited to become template string + return `https://docs.google.com/forms/d/e/1FAIpQLSf7d8xCAz682vR3WLRVTxqqbWiFXLd6ShZpOnsXXTmAbPFcUA/viewform?usp=pp_url&entry.1302713624=${userId}&entry.192715150=${itemId}&entry.511781180=${itemType}&entry.1691230719=${location.href}`; +};