From 5a17ffe90959c8abab70ac2aedced734414ad6fd Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Wed, 8 Jan 2025 00:18:08 +0100 Subject: [PATCH 01/19] feat: ReportUserModal --- .../shared/src/components/modals/common.tsx | 28 +++-- .../src/components/modals/common/types.ts | 1 + .../modals/report/ReportUserModal.tsx | 105 ++++++++++++++++++ packages/shared/src/report.ts | 1 + 4 files changed, 125 insertions(+), 10 deletions(-) create mode 100644 packages/shared/src/components/modals/report/ReportUserModal.tsx diff --git a/packages/shared/src/components/modals/common.tsx b/packages/shared/src/components/modals/common.tsx index f0f6af6364..35140833d9 100644 --- a/packages/shared/src/components/modals/common.tsx +++ b/packages/shared/src/components/modals/common.tsx @@ -214,6 +214,13 @@ const AddToCustomFeedModal = dynamic( ), ); +const ReportUserModal = dynamic( + () => + import( + /* webpackChunkName: "reportUserModal" */ './report/ReportUserModal' + ), +); + export const modals = { [LazyModal.SquadMember]: SquadMemberModal, [LazyModal.UpvotedPopup]: UpvotedPopupModal, @@ -250,6 +257,7 @@ export const modals = { [LazyModal.ClickbaitShield]: ClickbaitShieldModal, [LazyModal.MoveBookmark]: MoveBookmarkModal, [LazyModal.AddToCustomFeed]: AddToCustomFeedModal, + [LazyModal.ReportUser]: ReportUserModal, }; type GetComponentProps = T extends @@ -281,14 +289,14 @@ export type LazyModalType = { [K in keyof ModalsType]: NonOptional< LazyModalComponentType > extends Record - ? { - type: K; - persistOnRouteChange?: boolean; - props?: LazyModalComponentType; - } - : { - type: K; - persistOnRouteChange?: boolean; - props: LazyModalComponentType; - }; + ? { + type: K; + persistOnRouteChange?: boolean; + props?: LazyModalComponentType; + } + : { + type: K; + persistOnRouteChange?: boolean; + props: LazyModalComponentType; + }; }[T]; diff --git a/packages/shared/src/components/modals/common/types.ts b/packages/shared/src/components/modals/common/types.ts index 00832f1e0b..64a3b9ff5e 100644 --- a/packages/shared/src/components/modals/common/types.ts +++ b/packages/shared/src/components/modals/common/types.ts @@ -60,6 +60,7 @@ export enum LazyModal { ClickbaitShield = 'clickbaitShield', MoveBookmark = 'moveBookmark', AddToCustomFeed = 'addToCustomFeed', + ReportUser = 'reportUser', } export type ModalTabItem = { diff --git a/packages/shared/src/components/modals/report/ReportUserModal.tsx b/packages/shared/src/components/modals/report/ReportUserModal.tsx new file mode 100644 index 0000000000..3469b0aa9d --- /dev/null +++ b/packages/shared/src/components/modals/report/ReportUserModal.tsx @@ -0,0 +1,105 @@ +import React, { useState } from 'react'; +import { ReasonSelectionModal } from './ReasonSelectionModal'; +import { ReportEntity, SEND_REPORT_MUTATION, type ReportReason } from '../../../report'; +import { Checkbox } from '../../fields/Checkbox'; +import type { UserShortProfile } from '../../../lib/user'; +import { useMutation } from '@tanstack/react-query'; +import { gqlClient } from '../../../graphql/common'; +import { CONTENT_PREFERENCE_BLOCK_MUTATION, ContentPreferenceType } from '../../../graphql/contentPreference'; +import { useAuthContext } from '../../../contexts/AuthContext'; +import { useToastNotification } from '../../../hooks'; + + +const reportReasons: { value: string; label: string }[] = [ + { value: 'inappropriate', label: 'Inappropriate or NSFW Content' }, + { value: 'trolling', label: 'Trolling or Disruptive Behavior' }, + { value: 'harassment', label: 'Harassment or Bullying' }, + { value: 'impersonation', label: 'Impersonation or False Identity' }, + { value: 'spam', label: 'Spam or Unsolicited Advertising' }, + { value: 'misinformation', label: 'Misinformation or False Claims' }, + { value: 'hateSpeech', label: 'Hate Speech or Discrimination' }, + { value: 'privacy', label: 'Privacy or Copyright Violation' }, + { value: 'plagiarism', label: 'Plagiarism or Content Theft' }, + { value: 'other', label: 'Other' }, +]; + + +type ReportUserModalProps = { + offendingUser: UserShortProfile + defaultBlockUser?: boolean + onClose: () => void +} + +export const ReportUserModal = ({ offendingUser, defaultBlockUser, onClose }: ReportUserModalProps) => { + const { displayToast } = useToastNotification(); + const { user } = useAuthContext(); + const [blockUser, setBlockUser] = useState(defaultBlockUser); + const { isPending: isBlockPending, mutateAsync: blockUserMutation } = useMutation({ + mutationFn: () => gqlClient.request(CONTENT_PREFERENCE_BLOCK_MUTATION, { + id: offendingUser.id, + entity: ContentPreferenceType.User, + feedId: user.id + }), + onSuccess: () => { + if (!defaultBlockUser) { + displayToast(`🚫 ${offendingUser.username} has been blocked`); + onClose(); + } + }, + onError: () => { + displayToast(`❌ Failed to block ${offendingUser.username}`); + } + }) + const { isPending: isReportPending, mutateAsync: reportUserMutation } = useMutation({ + mutationFn: ({ reason, text }: { reason: ReportReason, text: string }) => gqlClient.request(SEND_REPORT_MUTATION, { + id: offendingUser.id, + type: ReportEntity.User, + reason: reason, + comment: text, + }), + onSuccess: () => { + if (!defaultBlockUser) { + displayToast(`🗒️ ${offendingUser.username} has been reported`); + onClose(); + } + }, + onError: () => { + displayToast(`❌ Failed to report ${offendingUser.username}`); + }, + }) + + const onReportUser = ( + e: React.MouseEvent, + reason: ReportReason, + text: string, + ) => { + e.preventDefault(); + reportUserMutation({ reason, text }) + if (defaultBlockUser) { + blockUserMutation() + } + }; + + const isPending = isBlockPending || isReportPending + const checkboxDisabled = defaultBlockUser || isPending + return ( + setBlockUser(e.target.checked)} + checked={blockUser}> + Block {offendingUser.username} + + } + /> + ); +}; + +export default ReportUserModal; \ No newline at end of file diff --git a/packages/shared/src/report.ts b/packages/shared/src/report.ts index e9cb1c23ac..2d89c4db00 100644 --- a/packages/shared/src/report.ts +++ b/packages/shared/src/report.ts @@ -6,6 +6,7 @@ export enum ReportEntity { Post = 'post', Source = 'source', Comment = 'comment', + User = 'user', } export enum ReportReason { From 0540f796d35dd38eddb884642054e477a5aa3243 Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Wed, 8 Jan 2025 11:45:35 +0100 Subject: [PATCH 02/19] formatting --- .../modals/report/ReportUserModal.tsx | 184 ++++++++++-------- 1 file changed, 99 insertions(+), 85 deletions(-) diff --git a/packages/shared/src/components/modals/report/ReportUserModal.tsx b/packages/shared/src/components/modals/report/ReportUserModal.tsx index 3469b0aa9d..27d1c436b9 100644 --- a/packages/shared/src/components/modals/report/ReportUserModal.tsx +++ b/packages/shared/src/components/modals/report/ReportUserModal.tsx @@ -1,105 +1,119 @@ +import type { ReactElement } from 'react'; import React, { useState } from 'react'; +import { useMutation } from '@tanstack/react-query'; import { ReasonSelectionModal } from './ReasonSelectionModal'; -import { ReportEntity, SEND_REPORT_MUTATION, type ReportReason } from '../../../report'; +import type { ReportReason } from '../../../report'; +import { ReportEntity, SEND_REPORT_MUTATION } from '../../../report'; import { Checkbox } from '../../fields/Checkbox'; import type { UserShortProfile } from '../../../lib/user'; -import { useMutation } from '@tanstack/react-query'; import { gqlClient } from '../../../graphql/common'; -import { CONTENT_PREFERENCE_BLOCK_MUTATION, ContentPreferenceType } from '../../../graphql/contentPreference'; +import { + CONTENT_PREFERENCE_BLOCK_MUTATION, + ContentPreferenceType, +} from '../../../graphql/contentPreference'; import { useAuthContext } from '../../../contexts/AuthContext'; import { useToastNotification } from '../../../hooks'; - const reportReasons: { value: string; label: string }[] = [ - { value: 'inappropriate', label: 'Inappropriate or NSFW Content' }, - { value: 'trolling', label: 'Trolling or Disruptive Behavior' }, - { value: 'harassment', label: 'Harassment or Bullying' }, - { value: 'impersonation', label: 'Impersonation or False Identity' }, - { value: 'spam', label: 'Spam or Unsolicited Advertising' }, - { value: 'misinformation', label: 'Misinformation or False Claims' }, - { value: 'hateSpeech', label: 'Hate Speech or Discrimination' }, - { value: 'privacy', label: 'Privacy or Copyright Violation' }, - { value: 'plagiarism', label: 'Plagiarism or Content Theft' }, - { value: 'other', label: 'Other' }, + { value: 'inappropriate', label: 'Inappropriate or NSFW Content' }, + { value: 'trolling', label: 'Trolling or Disruptive Behavior' }, + { value: 'harassment', label: 'Harassment or Bullying' }, + { value: 'impersonation', label: 'Impersonation or False Identity' }, + { value: 'spam', label: 'Spam or Unsolicited Advertising' }, + { value: 'misinformation', label: 'Misinformation or False Claims' }, + { value: 'hateSpeech', label: 'Hate Speech or Discrimination' }, + { value: 'privacy', label: 'Privacy or Copyright Violation' }, + { value: 'plagiarism', label: 'Plagiarism or Content Theft' }, + { value: 'other', label: 'Other' }, ]; - type ReportUserModalProps = { - offendingUser: UserShortProfile - defaultBlockUser?: boolean - onClose: () => void -} + offendingUser: UserShortProfile; + defaultBlockUser?: boolean; + onClose: () => void; +}; -export const ReportUserModal = ({ offendingUser, defaultBlockUser, onClose }: ReportUserModalProps) => { - const { displayToast } = useToastNotification(); - const { user } = useAuthContext(); - const [blockUser, setBlockUser] = useState(defaultBlockUser); - const { isPending: isBlockPending, mutateAsync: blockUserMutation } = useMutation({ - mutationFn: () => gqlClient.request(CONTENT_PREFERENCE_BLOCK_MUTATION, { - id: offendingUser.id, - entity: ContentPreferenceType.User, - feedId: user.id +export const ReportUserModal = ({ + offendingUser, + defaultBlockUser, + onClose, +}: ReportUserModalProps): ReactElement => { + const { displayToast } = useToastNotification(); + const { user } = useAuthContext(); + const [blockUser, setBlockUser] = useState(defaultBlockUser); + const { isPending: isBlockPending, mutateAsync: blockUserMutation } = + useMutation({ + mutationFn: () => + gqlClient.request(CONTENT_PREFERENCE_BLOCK_MUTATION, { + id: offendingUser.id, + entity: ContentPreferenceType.User, + feedId: user.id, }), - onSuccess: () => { - if (!defaultBlockUser) { - displayToast(`🚫 ${offendingUser.username} has been blocked`); - onClose(); - } - }, - onError: () => { - displayToast(`❌ Failed to block ${offendingUser.username}`); + onSuccess: () => { + if (!defaultBlockUser) { + displayToast(`🚫 ${offendingUser.username} has been blocked`); + onClose(); } - }) - const { isPending: isReportPending, mutateAsync: reportUserMutation } = useMutation({ - mutationFn: ({ reason, text }: { reason: ReportReason, text: string }) => gqlClient.request(SEND_REPORT_MUTATION, { - id: offendingUser.id, - type: ReportEntity.User, - reason: reason, - comment: text, + }, + onError: () => { + displayToast(`❌ Failed to block ${offendingUser.username}`); + }, + }); + const { isPending: isReportPending, mutateAsync: reportUserMutation } = + useMutation({ + mutationFn: ({ reason, text }: { reason: ReportReason; text: string }) => + gqlClient.request(SEND_REPORT_MUTATION, { + id: offendingUser.id, + type: ReportEntity.User, + reason, + comment: text, }), - onSuccess: () => { - if (!defaultBlockUser) { - displayToast(`🗒️ ${offendingUser.username} has been reported`); - onClose(); - } - }, - onError: () => { - displayToast(`❌ Failed to report ${offendingUser.username}`); - }, - }) - - const onReportUser = ( - e: React.MouseEvent, - reason: ReportReason, - text: string, - ) => { - e.preventDefault(); - reportUserMutation({ reason, text }) - if (defaultBlockUser) { - blockUserMutation() + onSuccess: () => { + if (!defaultBlockUser) { + displayToast(`🗒️ ${offendingUser.username} has been reported`); + onClose(); } - }; + }, + onError: () => { + displayToast(`❌ Failed to report ${offendingUser.username}`); + }, + }); + + const onReportUser = ( + e: React.MouseEvent, + reason: ReportReason, + text: string, + ) => { + e.preventDefault(); + reportUserMutation({ reason, text }); + if (defaultBlockUser) { + blockUserMutation(); + } + }; - const isPending = isBlockPending || isReportPending - const checkboxDisabled = defaultBlockUser || isPending - return ( - setBlockUser(e.target.checked)} - checked={blockUser}> - Block {offendingUser.username} - - } - /> - ); + const isPending = isBlockPending || isReportPending; + const checkboxDisabled = defaultBlockUser || isPending; + return ( + setBlockUser(e.target.checked)} + checked={blockUser} + > + Block {offendingUser.username} + + } + /> + ); }; -export default ReportUserModal; \ No newline at end of file +export default ReportUserModal; From 8be4262c1f5bea62244318e92e37490fff8b87be Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Wed, 8 Jan 2025 11:51:59 +0100 Subject: [PATCH 03/19] lint --- .../shared/src/components/modals/common.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/shared/src/components/modals/common.tsx b/packages/shared/src/components/modals/common.tsx index 35140833d9..31bf056ac4 100644 --- a/packages/shared/src/components/modals/common.tsx +++ b/packages/shared/src/components/modals/common.tsx @@ -289,14 +289,14 @@ export type LazyModalType = { [K in keyof ModalsType]: NonOptional< LazyModalComponentType > extends Record - ? { - type: K; - persistOnRouteChange?: boolean; - props?: LazyModalComponentType; - } - : { - type: K; - persistOnRouteChange?: boolean; - props: LazyModalComponentType; - }; + ? { + type: K; + persistOnRouteChange?: boolean; + props?: LazyModalComponentType; + } + : { + type: K; + persistOnRouteChange?: boolean; + props: LazyModalComponentType; + }; }[T]; From eef302c72f923b32c257f5bff86db12b2ce5582d Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Wed, 8 Jan 2025 11:56:26 +0100 Subject: [PATCH 04/19] remove unnecessary check --- .../shared/src/components/modals/report/ReportUserModal.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/components/modals/report/ReportUserModal.tsx b/packages/shared/src/components/modals/report/ReportUserModal.tsx index 27d1c436b9..dc72e81e4f 100644 --- a/packages/shared/src/components/modals/report/ReportUserModal.tsx +++ b/packages/shared/src/components/modals/report/ReportUserModal.tsx @@ -50,10 +50,8 @@ export const ReportUserModal = ({ feedId: user.id, }), onSuccess: () => { - if (!defaultBlockUser) { - displayToast(`🚫 ${offendingUser.username} has been blocked`); - onClose(); - } + displayToast(`🚫 ${offendingUser.username} has been blocked`); + onClose(); }, onError: () => { displayToast(`❌ Failed to block ${offendingUser.username}`); From 63e5977ad59c145d98ef09bcf982e4f3e546b4e3 Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Wed, 8 Jan 2025 11:57:18 +0100 Subject: [PATCH 05/19] update conditional --- .../shared/src/components/modals/report/ReportUserModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/components/modals/report/ReportUserModal.tsx b/packages/shared/src/components/modals/report/ReportUserModal.tsx index dc72e81e4f..2622049cd6 100644 --- a/packages/shared/src/components/modals/report/ReportUserModal.tsx +++ b/packages/shared/src/components/modals/report/ReportUserModal.tsx @@ -67,7 +67,7 @@ export const ReportUserModal = ({ comment: text, }), onSuccess: () => { - if (!defaultBlockUser) { + if (!blockUser) { displayToast(`🗒️ ${offendingUser.username} has been reported`); onClose(); } From 08de8e0b4da624eb2e70879887def24cdfdeec5c Mon Sep 17 00:00:00 2001 From: Luca Pagliaro Date: Wed, 8 Jan 2025 17:44:50 +0100 Subject: [PATCH 06/19] feat: add block user option in post context menu --- .../shared/src/components/PostOptionsMenu.tsx | 68 +++++++++++++------ .../modals/report/ReportUserModal.tsx | 2 +- 2 files changed, 50 insertions(+), 20 deletions(-) diff --git a/packages/shared/src/components/PostOptionsMenu.tsx b/packages/shared/src/components/PostOptionsMenu.tsx index f3c1e845bd..9d2f540695 100644 --- a/packages/shared/src/components/PostOptionsMenu.tsx +++ b/packages/shared/src/components/PostOptionsMenu.tsx @@ -7,30 +7,30 @@ import classNames from 'classnames'; import useFeedSettings from '../hooks/useFeedSettings'; import useReportPost from '../hooks/useReportPost'; import type { Post } from '../graphql/posts'; -import { UserVote, isVideoPost } from '../graphql/posts'; +import { isVideoPost, UserVote } from '../graphql/posts'; import { - TrashIcon, - HammerIcon, - EyeIcon, + AddUserIcon, + BellAddIcon, + BellSubscribedIcon, BlockIcon, - FlagIcon, - PlusIcon, - EditIcon, - UpvoteIcon, - DownvoteIcon, - SendBackwardIcon, BringForwardIcon, - PinIcon, - BellSubscribedIcon, - ShareIcon, + DownvoteIcon, + EditIcon, + EyeIcon, + FlagIcon, + FolderIcon, + HammerIcon, MiniCloseIcon, MinusIcon, - BellAddIcon, - AddUserIcon, + PinIcon, + PlusIcon, RemoveUserIcon, - FolderIcon, + SendBackwardIcon, + ShareIcon, ShieldIcon, ShieldWarningIcon, + TrashIcon, + UpvoteIcon, } from './icons'; import type { ReportedCallback } from './modals'; import useTagAndSource from '../hooks/useTagAndSource'; @@ -135,9 +135,8 @@ export default function PostOptionsMenu({ const { logEvent } = useContext(LogContext); const { hidePost, unhidePost } = useReportPost(); const { openSharePost } = useSharePost(origin); - const { follow, unfollow } = useContentPreference(); - - const { openModal } = useLazyModal(); + const { follow, unfollow, unblock } = useContentPreference(); + const { openModal, closeModal } = useLazyModal(); const { onBlockSource, @@ -489,6 +488,37 @@ export default function PostOptionsMenu({ action: isSourceBlocked ? onUnblockSourceClick : onBlockSourceClick, }); + // todo: implement when API is updated + const isBlockedAuthor = false; + if (post?.author && post?.author?.id !== user.id) { + postOptions.push({ + icon: , + label: isBlockedAuthor + ? `Unblock ${post.author.name}` + : `Block ${post.author.name}`, + action: () => { + if (isBlockedAuthor) { + unblock({ + id: post.author.id, + entity: ContentPreferenceType.User, + entityName: post.author.name, + feedId: router.query.slugOrId ? `${router.query.slugOrId}` : null, + }); + return; + } + + openModal({ + type: LazyModal.ReportUser, + props: { + offendingUser: post.author, + defaultBlockUser: true, + onClose: () => closeModal(), + }, + }); + }, + }); + } + if (video && isVideoPost(post)) { const isEnabled = checkSettingsEnabledState(video.id); const label = isEnabled ? `Don't show` : 'Show'; diff --git a/packages/shared/src/components/modals/report/ReportUserModal.tsx b/packages/shared/src/components/modals/report/ReportUserModal.tsx index 2622049cd6..c6355a2469 100644 --- a/packages/shared/src/components/modals/report/ReportUserModal.tsx +++ b/packages/shared/src/components/modals/report/ReportUserModal.tsx @@ -28,7 +28,7 @@ const reportReasons: { value: string; label: string }[] = [ ]; type ReportUserModalProps = { - offendingUser: UserShortProfile; + offendingUser: Pick; defaultBlockUser?: boolean; onClose: () => void; }; From aa748585bddc90e9b771d7ac9c28cb1e5f0ec4ba Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Wed, 8 Jan 2025 19:14:30 +0100 Subject: [PATCH 07/19] remove need to pass onClose --- .../src/components/modals/report/ReportUserModal.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/shared/src/components/modals/report/ReportUserModal.tsx b/packages/shared/src/components/modals/report/ReportUserModal.tsx index 2622049cd6..58a2292958 100644 --- a/packages/shared/src/components/modals/report/ReportUserModal.tsx +++ b/packages/shared/src/components/modals/report/ReportUserModal.tsx @@ -13,6 +13,7 @@ import { } from '../../../graphql/contentPreference'; import { useAuthContext } from '../../../contexts/AuthContext'; import { useToastNotification } from '../../../hooks'; +import { useLazyModal } from '../../../hooks/useLazyModal'; const reportReasons: { value: string; label: string }[] = [ { value: 'inappropriate', label: 'Inappropriate or NSFW Content' }, @@ -28,16 +29,15 @@ const reportReasons: { value: string; label: string }[] = [ ]; type ReportUserModalProps = { - offendingUser: UserShortProfile; + offendingUser: Pick; defaultBlockUser?: boolean; - onClose: () => void; }; export const ReportUserModal = ({ offendingUser, defaultBlockUser, - onClose, }: ReportUserModalProps): ReactElement => { + const { closeModal: onClose } = useLazyModal(); const { displayToast } = useToastNotification(); const { user } = useAuthContext(); const [blockUser, setBlockUser] = useState(defaultBlockUser); @@ -47,7 +47,7 @@ export const ReportUserModal = ({ gqlClient.request(CONTENT_PREFERENCE_BLOCK_MUTATION, { id: offendingUser.id, entity: ContentPreferenceType.User, - feedId: user.id, + feedId: user?.id, }), onSuccess: () => { displayToast(`🚫 ${offendingUser.username} has been blocked`); @@ -84,7 +84,7 @@ export const ReportUserModal = ({ ) => { e.preventDefault(); reportUserMutation({ reason, text }); - if (defaultBlockUser) { + if (blockUser) { blockUserMutation(); } }; From 9b01e41e6717cbd3f1470afa9f5770ea6dfd9e6e Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Wed, 8 Jan 2025 19:33:28 +0100 Subject: [PATCH 08/19] capitalize values --- .../modals/report/ReportUserModal.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/shared/src/components/modals/report/ReportUserModal.tsx b/packages/shared/src/components/modals/report/ReportUserModal.tsx index 58a2292958..806f93d18c 100644 --- a/packages/shared/src/components/modals/report/ReportUserModal.tsx +++ b/packages/shared/src/components/modals/report/ReportUserModal.tsx @@ -16,16 +16,16 @@ import { useToastNotification } from '../../../hooks'; import { useLazyModal } from '../../../hooks/useLazyModal'; const reportReasons: { value: string; label: string }[] = [ - { value: 'inappropriate', label: 'Inappropriate or NSFW Content' }, - { value: 'trolling', label: 'Trolling or Disruptive Behavior' }, - { value: 'harassment', label: 'Harassment or Bullying' }, - { value: 'impersonation', label: 'Impersonation or False Identity' }, - { value: 'spam', label: 'Spam or Unsolicited Advertising' }, - { value: 'misinformation', label: 'Misinformation or False Claims' }, - { value: 'hateSpeech', label: 'Hate Speech or Discrimination' }, - { value: 'privacy', label: 'Privacy or Copyright Violation' }, - { value: 'plagiarism', label: 'Plagiarism or Content Theft' }, - { value: 'other', label: 'Other' }, + { value: 'INAPPROPRIATE', label: 'Inappropriate or NSFW Content' }, + { value: 'TROLLING', label: 'Trolling or Disruptive Behavior' }, + { value: 'HARASSMENT', label: 'Harassment or Bullying' }, + { value: 'IMPERSONATION', label: 'Impersonation or False Identity' }, + { value: 'SPAM', label: 'Spam or Unsolicited Advertising' }, + { value: 'MISINFORMATION', label: 'Misinformation or False Claims' }, + { value: 'HATE_SPEECH', label: 'Hate Speech or Discrimination' }, + { value: 'PRIVACY', label: 'Privacy or Copyright Violation' }, + { value: 'PLAGIARISM', label: 'Plagiarism or Content Theft' }, + { value: 'OTHER', label: 'Other' }, ]; type ReportUserModalProps = { From 4388eb2b0baf36f500c5a01e365e95e324d29d0e Mon Sep 17 00:00:00 2001 From: Luca Pagliaro Date: Thu, 9 Jan 2025 11:07:47 +0100 Subject: [PATCH 09/19] feat: add optimistic update on unblock --- .../shared/src/components/PostOptionsMenu.tsx | 36 ++++++++++++++----- .../shared/src/graphql/contentPreference.ts | 1 + 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/packages/shared/src/components/PostOptionsMenu.tsx b/packages/shared/src/components/PostOptionsMenu.tsx index 9d2f540695..fc2d38411a 100644 --- a/packages/shared/src/components/PostOptionsMenu.tsx +++ b/packages/shared/src/components/PostOptionsMenu.tsx @@ -46,7 +46,7 @@ import { useToastNotification, } from '../hooks'; import type { AllFeedPages } from '../lib/query'; -import { generateQueryKey } from '../lib/query'; +import { RequestKey, generateQueryKey } from '../lib/query'; import AuthContext from '../contexts/AuthContext'; import { LogEvent, Origin } from '../lib/log'; import { usePostMenuActions } from '../hooks/usePostMenuActions'; @@ -63,10 +63,15 @@ import { useBookmarkReminder } from '../hooks/notifications'; import { BookmarkReminderIcon } from './icons/Bookmark/Reminder'; import { useSourceActionsFollow } from '../hooks/source/useSourceActionsFollow'; import { useContentPreference } from '../hooks/contentPreference/useContentPreference'; -import { ContentPreferenceType } from '../graphql/contentPreference'; +import type { ContentPreference } from '../graphql/contentPreference'; +import { + ContentPreferenceStatus, + ContentPreferenceType, +} from '../graphql/contentPreference'; import { isFollowingContent } from '../hooks/contentPreference/types'; import { useIsSpecialUser } from '../hooks/auth/useIsSpecialUser'; import { useActiveFeedContext } from '../contexts'; +import { useContentPreferenceStatusQuery } from '../hooks/contentPreference/useContentPreferenceStatusQuery'; const ContextMenu = dynamic( () => import(/* webpackChunkName: "contextMenu" */ './fields/ContextMenu'), @@ -136,7 +141,7 @@ export default function PostOptionsMenu({ const { hidePost, unhidePost } = useReportPost(); const { openSharePost } = useSharePost(origin); const { follow, unfollow, unblock } = useContentPreference(); - const { openModal, closeModal } = useLazyModal(); + const { openModal } = useLazyModal(); const { onBlockSource, @@ -488,22 +493,38 @@ export default function PostOptionsMenu({ action: isSourceBlocked ? onUnblockSourceClick : onBlockSourceClick, }); - // todo: implement when API is updated - const isBlockedAuthor = false; + const { data: contentPreference } = useContentPreferenceStatusQuery({ + id: user?.id, + entity: ContentPreferenceType.User, + }); + const isBlockedAuthor = + contentPreference?.status === ContentPreferenceStatus.Blocked; if (post?.author && post?.author?.id !== user.id) { postOptions.push({ icon: , label: isBlockedAuthor ? `Unblock ${post.author.name}` : `Block ${post.author.name}`, - action: () => { + action: async () => { if (isBlockedAuthor) { - unblock({ + const commonParams = { id: post.author.id, entity: ContentPreferenceType.User, + }; + await unblock({ + ...commonParams, entityName: post.author.name, feedId: router.query.slugOrId ? `${router.query.slugOrId}` : null, }); + const queryKey = generateQueryKey( + RequestKey.ContentPreference, + user, + { ...commonParams }, + ); + client.setQueryData(queryKey, (data: ContentPreference) => ({ + ...data, + status: ContentPreferenceStatus.Follow, + })); return; } @@ -512,7 +533,6 @@ export default function PostOptionsMenu({ props: { offendingUser: post.author, defaultBlockUser: true, - onClose: () => closeModal(), }, }); }, diff --git a/packages/shared/src/graphql/contentPreference.ts b/packages/shared/src/graphql/contentPreference.ts index f9e20ea33e..ae3f81b3f8 100644 --- a/packages/shared/src/graphql/contentPreference.ts +++ b/packages/shared/src/graphql/contentPreference.ts @@ -16,6 +16,7 @@ export enum ContentPreferenceType { export enum ContentPreferenceStatus { Follow = 'follow', Subscribed = 'subscribed', + Blocked = 'blocked', } type ContentPreferenceUser = Pick< From 0271bc1c91a4ac5b1e444c542c29564a6d35721e Mon Sep 17 00:00:00 2001 From: Luca Pagliaro Date: Thu, 9 Jan 2025 12:33:50 +0100 Subject: [PATCH 10/19] feat: refactor avoiding additional query --- .../shared/src/components/PostOptionsMenu.tsx | 55 ++++++++----------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/packages/shared/src/components/PostOptionsMenu.tsx b/packages/shared/src/components/PostOptionsMenu.tsx index fc2d38411a..815a43de4f 100644 --- a/packages/shared/src/components/PostOptionsMenu.tsx +++ b/packages/shared/src/components/PostOptionsMenu.tsx @@ -46,7 +46,7 @@ import { useToastNotification, } from '../hooks'; import type { AllFeedPages } from '../lib/query'; -import { RequestKey, generateQueryKey } from '../lib/query'; +import { generateQueryKey } from '../lib/query'; import AuthContext from '../contexts/AuthContext'; import { LogEvent, Origin } from '../lib/log'; import { usePostMenuActions } from '../hooks/usePostMenuActions'; @@ -63,7 +63,6 @@ import { useBookmarkReminder } from '../hooks/notifications'; import { BookmarkReminderIcon } from './icons/Bookmark/Reminder'; import { useSourceActionsFollow } from '../hooks/source/useSourceActionsFollow'; import { useContentPreference } from '../hooks/contentPreference/useContentPreference'; -import type { ContentPreference } from '../graphql/contentPreference'; import { ContentPreferenceStatus, ContentPreferenceType, @@ -71,7 +70,6 @@ import { import { isFollowingContent } from '../hooks/contentPreference/types'; import { useIsSpecialUser } from '../hooks/auth/useIsSpecialUser'; import { useActiveFeedContext } from '../contexts'; -import { useContentPreferenceStatusQuery } from '../hooks/contentPreference/useContentPreferenceStatusQuery'; const ContextMenu = dynamic( () => import(/* webpackChunkName: "contextMenu" */ './fields/ContextMenu'), @@ -493,12 +491,8 @@ export default function PostOptionsMenu({ action: isSourceBlocked ? onUnblockSourceClick : onBlockSourceClick, }); - const { data: contentPreference } = useContentPreferenceStatusQuery({ - id: user?.id, - entity: ContentPreferenceType.User, - }); const isBlockedAuthor = - contentPreference?.status === ContentPreferenceStatus.Blocked; + post?.author?.contentPreference?.status === ContentPreferenceStatus.Blocked; if (post?.author && post?.author?.id !== user.id) { postOptions.push({ icon: , @@ -506,35 +500,32 @@ export default function PostOptionsMenu({ ? `Unblock ${post.author.name}` : `Block ${post.author.name}`, action: async () => { - if (isBlockedAuthor) { - const commonParams = { - id: post.author.id, - entity: ContentPreferenceType.User, - }; - await unblock({ - ...commonParams, - entityName: post.author.name, - feedId: router.query.slugOrId ? `${router.query.slugOrId}` : null, + if (!isBlockedAuthor) { + openModal({ + type: LazyModal.ReportUser, + props: { + offendingUser: post.author, + defaultBlockUser: true, + }, }); - const queryKey = generateQueryKey( - RequestKey.ContentPreference, - user, - { ...commonParams }, - ); - client.setQueryData(queryKey, (data: ContentPreference) => ({ - ...data, - status: ContentPreferenceStatus.Follow, - })); return; } - openModal({ - type: LazyModal.ReportUser, - props: { - offendingUser: post.author, - defaultBlockUser: true, - }, + await unblock({ + id: post.author.id, + entity: ContentPreferenceType.User, + entityName: post.author.name, + feedId: router.query.slugOrId ? `${router.query.slugOrId}` : null, }); + + const postKey = getPostByIdKey(post.id); + const cached = client.getQueryData(postKey); + if (cached) { + client.setQueryData(postKey, (data) => ({ + ...data, + author: { ...data.author, contentPreference: null }, + })); + } }, }); } From 3d6d522ad4a95a15149892717d0d6783e1cebd1e Mon Sep 17 00:00:00 2001 From: Luca Pagliaro Date: Thu, 9 Jan 2025 12:45:28 +0100 Subject: [PATCH 11/19] fix: ssr error --- packages/shared/src/components/PostOptionsMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/components/PostOptionsMenu.tsx b/packages/shared/src/components/PostOptionsMenu.tsx index 815a43de4f..ef296e7c44 100644 --- a/packages/shared/src/components/PostOptionsMenu.tsx +++ b/packages/shared/src/components/PostOptionsMenu.tsx @@ -493,7 +493,7 @@ export default function PostOptionsMenu({ const isBlockedAuthor = post?.author?.contentPreference?.status === ContentPreferenceStatus.Blocked; - if (post?.author && post?.author?.id !== user.id) { + if (post?.author && post?.author?.id !== user?.id) { postOptions.push({ icon: , label: isBlockedAuthor From a8194bf41237736a6f7a1acafd17285a2fdf3fa1 Mon Sep 17 00:00:00 2001 From: Luca Pagliaro Date: Thu, 9 Jan 2025 16:57:09 +0100 Subject: [PATCH 12/19] feat: add contentPreference data for author --- packages/shared/src/graphql/fragments.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/shared/src/graphql/fragments.ts b/packages/shared/src/graphql/fragments.ts index ddf1795aaf..fc8521cabb 100644 --- a/packages/shared/src/graphql/fragments.ts +++ b/packages/shared/src/graphql/fragments.ts @@ -197,6 +197,9 @@ export const FEED_POST_INFO_FRAGMENT = gql` image username permalink + contentPreference { + status + } } type tags From 7abf0c83cef21fcc9e2ffa04a994fd95006c31f4 Mon Sep 17 00:00:00 2001 From: Luca Pagliaro Date: Tue, 14 Jan 2025 10:27:26 +0100 Subject: [PATCH 13/19] feat: hide follow option for blocked users, change source block label; --- .../shared/src/components/PostOptionsMenu.tsx | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/shared/src/components/PostOptionsMenu.tsx b/packages/shared/src/components/PostOptionsMenu.tsx index f076c62a97..b8ad306c7d 100644 --- a/packages/shared/src/components/PostOptionsMenu.tsx +++ b/packages/shared/src/components/PostOptionsMenu.tsx @@ -70,6 +70,7 @@ import { import { isFollowingContent } from '../hooks/contentPreference/types'; import { useIsSpecialUser } from '../hooks/auth/useIsSpecialUser'; import { useActiveFeedContext } from '../contexts'; +import type { FeedData } from '../graphql/feed'; const ContextMenu = dynamic( () => import(/* webpackChunkName: "contextMenu" */ './fields/ContextMenu'), @@ -158,6 +159,8 @@ export default function PostOptionsMenu({ (excludedSource) => excludedSource.id === post?.source?.id, ); }, [feedSettings?.excludeSources, post?.source?.id]); + const isBlockedAuthor = + post?.author?.contentPreference?.status === ContentPreferenceStatus.Blocked; const shouldShowSubscribe = isLoggedIn && @@ -458,6 +461,7 @@ export default function PostOptionsMenu({ const shouldShowFollow = !useIsSpecialUser({ userId: post?.author?.id }) && post?.author && + !isBlockedAuthor && isLoggedIn; if (shouldShowFollow) { @@ -497,13 +501,11 @@ export default function PostOptionsMenu({ postOptions.push({ icon: , label: isSourceBlocked - ? `Show posts from ${post?.source?.name}` - : `Don't show posts from ${post?.source?.name}`, + ? `Unblock ${post?.source?.name}` + : `Block ${post?.source?.name}`, action: isSourceBlocked ? onUnblockSourceClick : onBlockSourceClick, }); - const isBlockedAuthor = - post?.author?.contentPreference?.status === ContentPreferenceStatus.Blocked; if (post?.author && post?.author?.id !== user?.id) { postOptions.push({ icon: , @@ -519,9 +521,7 @@ export default function PostOptionsMenu({ defaultBlockUser: true, }, }); - return; } - await unblock({ id: post.author.id, entity: ContentPreferenceType.User, @@ -529,14 +529,13 @@ export default function PostOptionsMenu({ feedId: router.query.slugOrId ? `${router.query.slugOrId}` : null, }); - const postKey = getPostByIdKey(post.id); - const cached = client.getQueryData(postKey); - if (cached) { - client.setQueryData(postKey, (data) => ({ - ...data, - author: { ...data.author, contentPreference: null }, - })); - } + client.setQueryData(feedQueryKey, (data) => { + console.log(client.getQueryData(feedQueryKey)); + if (!data) { + return data; + } + return data; + }); }, }); } From 50466d580a7d953fe6ad81ced55f3d4ca3f70fa8 Mon Sep 17 00:00:00 2001 From: Luca Pagliaro Date: Tue, 14 Jan 2025 10:54:41 +0100 Subject: [PATCH 14/19] feat: clear cache on block --- .../shared/src/components/PostOptionsMenu.tsx | 20 +++++++++++-------- .../modals/report/ReportUserModal.tsx | 3 +++ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/shared/src/components/PostOptionsMenu.tsx b/packages/shared/src/components/PostOptionsMenu.tsx index b8ad306c7d..d64aeef8c1 100644 --- a/packages/shared/src/components/PostOptionsMenu.tsx +++ b/packages/shared/src/components/PostOptionsMenu.tsx @@ -70,7 +70,6 @@ import { import { isFollowingContent } from '../hooks/contentPreference/types'; import { useIsSpecialUser } from '../hooks/auth/useIsSpecialUser'; import { useActiveFeedContext } from '../contexts'; -import type { FeedData } from '../graphql/feed'; const ContextMenu = dynamic( () => import(/* webpackChunkName: "contextMenu" */ './fields/ContextMenu'), @@ -513,15 +512,26 @@ export default function PostOptionsMenu({ ? `Unblock ${post.author.name}` : `Block ${post.author.name}`, action: async () => { + const clearCache = () => { + const postQueryKey = getPostByIdKey(post.id); + const postCache = client.getQueryData(postQueryKey); + if (postCache) { + client.invalidateQueries({ queryKey: postQueryKey }); + } + }; + if (!isBlockedAuthor) { openModal({ type: LazyModal.ReportUser, props: { offendingUser: post.author, defaultBlockUser: true, + onBlockUser: clearCache, }, }); + return; } + await unblock({ id: post.author.id, entity: ContentPreferenceType.User, @@ -529,13 +539,7 @@ export default function PostOptionsMenu({ feedId: router.query.slugOrId ? `${router.query.slugOrId}` : null, }); - client.setQueryData(feedQueryKey, (data) => { - console.log(client.getQueryData(feedQueryKey)); - if (!data) { - return data; - } - return data; - }); + clearCache(); }, }); } diff --git a/packages/shared/src/components/modals/report/ReportUserModal.tsx b/packages/shared/src/components/modals/report/ReportUserModal.tsx index 806f93d18c..bf7bdaf271 100644 --- a/packages/shared/src/components/modals/report/ReportUserModal.tsx +++ b/packages/shared/src/components/modals/report/ReportUserModal.tsx @@ -31,11 +31,13 @@ const reportReasons: { value: string; label: string }[] = [ type ReportUserModalProps = { offendingUser: Pick; defaultBlockUser?: boolean; + onBlockUser?: () => void; }; export const ReportUserModal = ({ offendingUser, defaultBlockUser, + onBlockUser, }: ReportUserModalProps): ReactElement => { const { closeModal: onClose } = useLazyModal(); const { displayToast } = useToastNotification(); @@ -52,6 +54,7 @@ export const ReportUserModal = ({ onSuccess: () => { displayToast(`🚫 ${offendingUser.username} has been blocked`); onClose(); + onBlockUser?.(); }, onError: () => { displayToast(`❌ Failed to block ${offendingUser.username}`); From 1fd60e5e811884e0c6f374be034d1ee3f3a81800 Mon Sep 17 00:00:00 2001 From: Luca Pagliaro Date: Tue, 14 Jan 2025 11:03:22 +0100 Subject: [PATCH 15/19] feat: block on custom feed feature --- packages/shared/src/components/PostOptionsMenu.tsx | 1 + .../shared/src/components/modals/report/ReportUserModal.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/components/PostOptionsMenu.tsx b/packages/shared/src/components/PostOptionsMenu.tsx index d64aeef8c1..704c84f3a9 100644 --- a/packages/shared/src/components/PostOptionsMenu.tsx +++ b/packages/shared/src/components/PostOptionsMenu.tsx @@ -527,6 +527,7 @@ export default function PostOptionsMenu({ offendingUser: post.author, defaultBlockUser: true, onBlockUser: clearCache, + ...(isCustomFeed && { feedId: customFeedId }), }, }); return; diff --git a/packages/shared/src/components/modals/report/ReportUserModal.tsx b/packages/shared/src/components/modals/report/ReportUserModal.tsx index bf7bdaf271..5ef5693d45 100644 --- a/packages/shared/src/components/modals/report/ReportUserModal.tsx +++ b/packages/shared/src/components/modals/report/ReportUserModal.tsx @@ -29,14 +29,16 @@ const reportReasons: { value: string; label: string }[] = [ ]; type ReportUserModalProps = { - offendingUser: Pick; defaultBlockUser?: boolean; + feedId?: string; + offendingUser: Pick; onBlockUser?: () => void; }; export const ReportUserModal = ({ offendingUser, defaultBlockUser, + feedId, onBlockUser, }: ReportUserModalProps): ReactElement => { const { closeModal: onClose } = useLazyModal(); @@ -49,7 +51,7 @@ export const ReportUserModal = ({ gqlClient.request(CONTENT_PREFERENCE_BLOCK_MUTATION, { id: offendingUser.id, entity: ContentPreferenceType.User, - feedId: user?.id, + feedId: feedId ?? user?.id, }), onSuccess: () => { displayToast(`🚫 ${offendingUser.username} has been blocked`); From ed004e1b13a4fc955375ac77854a4326f3f5722e Mon Sep 17 00:00:00 2001 From: Luca Pagliaro Date: Tue, 14 Jan 2025 11:08:20 +0100 Subject: [PATCH 16/19] test: update label for block/unblock --- packages/shared/src/components/Feed.spec.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/components/Feed.spec.tsx b/packages/shared/src/components/Feed.spec.tsx index 5fa83220d0..4f8075c4f8 100644 --- a/packages/shared/src/components/Feed.spec.tsx +++ b/packages/shared/src/components/Feed.spec.tsx @@ -624,7 +624,7 @@ describe('Feed logged in', () => { ); expect(data).toBeTruthy(); }); - const contextBtn = await screen.findByText("Don't show posts from Echo JS"); + const contextBtn = await screen.findByText('Block Echo JS'); fireEvent.click(contextBtn); await waitForNock(); await waitFor(() => expect(mutationCalled).toBeTruthy()); @@ -673,7 +673,7 @@ describe('Feed logged in', () => { ); expect(data).toBeTruthy(); }); - const contextBtn = await screen.findByText('Show posts from Echo JS'); + const contextBtn = await screen.findByText('Unblock Echo JS'); await waitFor(async () => { fireEvent.click(contextBtn); From adbb4148ab39f30c2a88d72d261451de933ae655 Mon Sep 17 00:00:00 2001 From: Luca Pagliaro Date: Thu, 16 Jan 2025 12:24:19 +0100 Subject: [PATCH 17/19] refactor: invalidate cache is not a utility --- .../shared/src/components/PostOptionsMenu.tsx | 20 +++++++++---------- .../squads/SquadCommentJoinBanner.tsx | 5 ++--- packages/shared/src/hooks/usePostById.ts | 11 ++++++++++ 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/packages/shared/src/components/PostOptionsMenu.tsx b/packages/shared/src/components/PostOptionsMenu.tsx index 704c84f3a9..1ca2744392 100644 --- a/packages/shared/src/components/PostOptionsMenu.tsx +++ b/packages/shared/src/components/PostOptionsMenu.tsx @@ -50,7 +50,10 @@ import { generateQueryKey } from '../lib/query'; import AuthContext from '../contexts/AuthContext'; import { LogEvent, Origin } from '../lib/log'; import { usePostMenuActions } from '../hooks/usePostMenuActions'; -import usePostById, { getPostByIdKey } from '../hooks/usePostById'; +import usePostById, { + getPostByIdKey, + invalidatePostCacheById, +} from '../hooks/usePostById'; import { useLazyModal } from '../hooks/useLazyModal'; import { LazyModal } from './modals/common/types'; import { labels } from '../lib'; @@ -512,21 +515,16 @@ export default function PostOptionsMenu({ ? `Unblock ${post.author.name}` : `Block ${post.author.name}`, action: async () => { - const clearCache = () => { - const postQueryKey = getPostByIdKey(post.id); - const postCache = client.getQueryData(postQueryKey); - if (postCache) { - client.invalidateQueries({ queryKey: postQueryKey }); - } - }; - if (!isBlockedAuthor) { openModal({ type: LazyModal.ReportUser, props: { offendingUser: post.author, defaultBlockUser: true, - onBlockUser: clearCache, + onBlockUser: invalidatePostCacheById.bind(null, [ + client, + post.id, + ]), ...(isCustomFeed && { feedId: customFeedId }), }, }); @@ -540,7 +538,7 @@ export default function PostOptionsMenu({ feedId: router.query.slugOrId ? `${router.query.slugOrId}` : null, }); - clearCache(); + invalidatePostCacheById(client, post.id); }, }); } diff --git a/packages/shared/src/components/squads/SquadCommentJoinBanner.tsx b/packages/shared/src/components/squads/SquadCommentJoinBanner.tsx index 88e5f65a52..1cf4c7956b 100644 --- a/packages/shared/src/components/squads/SquadCommentJoinBanner.tsx +++ b/packages/shared/src/components/squads/SquadCommentJoinBanner.tsx @@ -14,6 +14,7 @@ import SourceButton from '../cards/common/SourceButton'; import { SQUAD_COMMENT_JOIN_BANNER_KEY } from '../../graphql/squads'; import type { Post } from '../../graphql/posts'; import { ProfileImageSize } from '../ProfilePicture'; +import { invalidatePostCacheById } from '../../hooks/usePostById'; export type SquadCommentJoinBannerProps = { className?: string; @@ -44,9 +45,7 @@ export const SquadCommentJoinBanner = ({ displayToast(`🙌 You joined the Squad ${squad.name}`); setIsSquadMember(true); if (post?.id) { - queryClient.invalidateQueries({ - queryKey: ['post', post.id], - }); + invalidatePostCacheById(queryClient, post.id); } }, onError: () => { diff --git a/packages/shared/src/hooks/usePostById.ts b/packages/shared/src/hooks/usePostById.ts index fc79efe755..25e760f48b 100644 --- a/packages/shared/src/hooks/usePostById.ts +++ b/packages/shared/src/hooks/usePostById.ts @@ -40,6 +40,17 @@ export const POST_KEY = 'post'; export const getPostByIdKey = (id: string): QueryKey => [POST_KEY, id]; +export const invalidatePostCacheById = ( + client: QueryClient, + id: string, +): void => { + const postQueryKey = getPostByIdKey(id); + const postCache = client.getQueryData(postQueryKey); + if (postCache) { + client.invalidateQueries({ queryKey: postQueryKey }); + } +}; + export const updatePostCache = ( client: QueryClient, id: string, From 10cdb3daa27984fceaaf3cb2750742a05aa294de Mon Sep 17 00:00:00 2001 From: Luca Pagliaro Date: Thu, 16 Jan 2025 12:53:01 +0100 Subject: [PATCH 18/19] feat: added new labels and block directly without report modal --- .../shared/src/components/PostOptionsMenu.tsx | 72 +++++++++++-------- 1 file changed, 43 insertions(+), 29 deletions(-) diff --git a/packages/shared/src/components/PostOptionsMenu.tsx b/packages/shared/src/components/PostOptionsMenu.tsx index 1ca2744392..ec9440abab 100644 --- a/packages/shared/src/components/PostOptionsMenu.tsx +++ b/packages/shared/src/components/PostOptionsMenu.tsx @@ -97,6 +97,26 @@ export interface PostOptionsMenuProps { allowPin?: boolean; } +const getBlockOrUnblockLabel = ( + name: string, + { isCustomFeed, isBlocked }: Record<'isCustomFeed' | 'isBlocked', boolean>, +) => { + const blockLabel = { + global: { + block: `Block ${name}`, + unblock: `Unblock ${name}`, + }, + feed: { + block: `Remove ${name} from this feed`, + unblock: `Add ${name} to this feed`, + }, + }; + + return blockLabel[isCustomFeed ? 'feed' : 'global'][ + isBlocked ? 'unblock' : 'block' + ]; +}; + export default function PostOptionsMenu({ postIndex, post: initialPost, @@ -141,7 +161,7 @@ export default function PostOptionsMenu({ const { logEvent } = useContext(LogContext); const { hidePost, unhidePost } = useReportPost(); const { openSharePost } = useSharePost(origin); - const { follow, unfollow, unblock } = useContentPreference(); + const { follow, unfollow, unblock, block } = useContentPreference(); const { openModal } = useLazyModal(); const { @@ -500,43 +520,37 @@ export default function PostOptionsMenu({ }); } - postOptions.push({ - icon: , - label: isSourceBlocked - ? `Unblock ${post?.source?.name}` - : `Block ${post?.source?.name}`, - action: isSourceBlocked ? onUnblockSourceClick : onBlockSourceClick, - }); + if (post?.source?.name) { + postOptions.push({ + icon: , + label: getBlockOrUnblockLabel(post.source.name, { + isCustomFeed, + isBlocked: isSourceBlocked, + }), + action: isSourceBlocked ? onUnblockSourceClick : onBlockSourceClick, + }); + } if (post?.author && post?.author?.id !== user?.id) { postOptions.push({ icon: , - label: isBlockedAuthor - ? `Unblock ${post.author.name}` - : `Block ${post.author.name}`, + label: getBlockOrUnblockLabel(post.author.name, { + isCustomFeed, + isBlocked: isBlockedAuthor, + }), action: async () => { - if (!isBlockedAuthor) { - openModal({ - type: LazyModal.ReportUser, - props: { - offendingUser: post.author, - defaultBlockUser: true, - onBlockUser: invalidatePostCacheById.bind(null, [ - client, - post.id, - ]), - ...(isCustomFeed && { feedId: customFeedId }), - }, - }); - return; - } - - await unblock({ + const params = { id: post.author.id, entity: ContentPreferenceType.User, entityName: post.author.name, feedId: router.query.slugOrId ? `${router.query.slugOrId}` : null, - }); + }; + + if (isBlockedAuthor) { + await unblock(params); + } else { + await block(params); + } invalidatePostCacheById(client, post.id); }, From 8a92e2f32842ba7f98fb22b4de0dd5fb6923b19a Mon Sep 17 00:00:00 2001 From: Luca Pagliaro Date: Thu, 16 Jan 2025 14:55:54 +0100 Subject: [PATCH 19/19] feat: hide post from feed and add undo action to toast --- .../shared/src/components/PostOptionsMenu.tsx | 20 +++++++++++++++---- .../src/hooks/contentPreference/types.ts | 1 + .../contentPreference/useContentPreference.ts | 5 +++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/components/PostOptionsMenu.tsx b/packages/shared/src/components/PostOptionsMenu.tsx index ec9440abab..9c3bd2ab9d 100644 --- a/packages/shared/src/components/PostOptionsMenu.tsx +++ b/packages/shared/src/components/PostOptionsMenu.tsx @@ -97,7 +97,7 @@ export interface PostOptionsMenuProps { allowPin?: boolean; } -const getBlockOrUnblockLabel = ( +const getBlockLabel = ( name: string, { isCustomFeed, isBlocked }: Record<'isCustomFeed' | 'isBlocked', boolean>, ) => { @@ -523,7 +523,7 @@ export default function PostOptionsMenu({ if (post?.source?.name) { postOptions.push({ icon: , - label: getBlockOrUnblockLabel(post.source.name, { + label: getBlockLabel(post.source.name, { isCustomFeed, isBlocked: isSourceBlocked, }), @@ -534,7 +534,7 @@ export default function PostOptionsMenu({ if (post?.author && post?.author?.id !== user?.id) { postOptions.push({ icon: , - label: getBlockOrUnblockLabel(post.author.name, { + label: getBlockLabel(post.author.name, { isCustomFeed, isBlocked: isBlockedAuthor, }), @@ -549,7 +549,19 @@ export default function PostOptionsMenu({ if (isBlockedAuthor) { await unblock(params); } else { - await block(params); + await block({ + ...params, + opts: { + hideToast: true, + }, + }); + await showMessageAndRemovePost( + `🚫 ${post.author.name} has been ${ + isCustomFeed ? 'removed' : 'blocked' + }`, + postIndex, + () => unblock(params), + ); } invalidatePostCacheById(client, post.id); diff --git a/packages/shared/src/hooks/contentPreference/types.ts b/packages/shared/src/hooks/contentPreference/types.ts index 9ca5988c69..8df23c829e 100644 --- a/packages/shared/src/hooks/contentPreference/types.ts +++ b/packages/shared/src/hooks/contentPreference/types.ts @@ -20,6 +20,7 @@ export type ContentPreferenceMutation = ({ feedId?: string; opts?: Partial<{ extra: Record; + hideToast: boolean; }>; }) => Promise; diff --git a/packages/shared/src/hooks/contentPreference/useContentPreference.ts b/packages/shared/src/hooks/contentPreference/useContentPreference.ts index 257d90b2d0..44a9206a43 100644 --- a/packages/shared/src/hooks/contentPreference/useContentPreference.ts +++ b/packages/shared/src/hooks/contentPreference/useContentPreference.ts @@ -220,6 +220,11 @@ export const useContentPreference = ({ entity, feedId, }); + + if (opts?.hideToast) { + return; + } + if (entity === ContentPreferenceType.User) { displayToast(`🚫 ${entityName} has been blocked`); } else {