Skip to content

Commit

Permalink
feat: add block user option on post context menu (#4053)
Browse files Browse the repository at this point in the history
* feat: ReportUserModal

* formatting

* lint

* remove unnecessary check

* update conditional

* feat: add block user option in post context menu

* remove need to pass onClose

* capitalize values

* feat: add optimistic update on unblock

* feat: refactor avoiding additional query

* fix: ssr error

* feat: add contentPreference data for author

* feat: hide follow option for blocked users, change source block label;

* feat: clear cache on block

* feat: block on custom feed feature

* test: update label for block/unblock

* refactor: invalidate cache is not a utility

* feat: added new labels and block directly without report modal

* feat: hide post from feed and add undo action to toast

---------

Co-authored-by: Amar Trebinjac <[email protected]>
  • Loading branch information
ilasw and AmarTrebinjac authored Jan 16, 2025
1 parent 13bf6b0 commit 6cb0889
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 34 deletions.
4 changes: 2 additions & 2 deletions packages/shared/src/components/Feed.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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);
Expand Down
123 changes: 96 additions & 27 deletions packages/shared/src/components/PostOptionsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -63,7 +66,10 @@ 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 {
ContentPreferenceStatus,
ContentPreferenceType,
} from '../graphql/contentPreference';
import { isFollowingContent } from '../hooks/contentPreference/types';
import { useIsSpecialUser } from '../hooks/auth/useIsSpecialUser';
import { useActiveFeedContext } from '../contexts';
Expand Down Expand Up @@ -91,6 +97,26 @@ export interface PostOptionsMenuProps {
allowPin?: boolean;
}

const getBlockLabel = (
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,
Expand Down Expand Up @@ -135,8 +161,7 @@ export default function PostOptionsMenu({
const { logEvent } = useContext(LogContext);
const { hidePost, unhidePost } = useReportPost();
const { openSharePost } = useSharePost(origin);
const { follow, unfollow } = useContentPreference();

const { follow, unfollow, unblock, block } = useContentPreference();
const { openModal } = useLazyModal();

const {
Expand All @@ -156,6 +181,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 &&
Expand Down Expand Up @@ -456,6 +483,7 @@ export default function PostOptionsMenu({
const shouldShowFollow =
!useIsSpecialUser({ userId: post?.author?.id }) &&
post?.author &&
!isBlockedAuthor &&
isLoggedIn;

if (shouldShowFollow) {
Expand Down Expand Up @@ -492,13 +520,54 @@ export default function PostOptionsMenu({
});
}

postOptions.push({
icon: <MenuIcon Icon={BlockIcon} />,
label: isSourceBlocked
? `Show posts from ${post?.source?.name}`
: `Don't show posts from ${post?.source?.name}`,
action: isSourceBlocked ? onUnblockSourceClick : onBlockSourceClick,
});
if (post?.source?.name) {
postOptions.push({
icon: <MenuIcon Icon={BlockIcon} />,
label: getBlockLabel(post.source.name, {
isCustomFeed,
isBlocked: isSourceBlocked,
}),
action: isSourceBlocked ? onUnblockSourceClick : onBlockSourceClick,
});
}

if (post?.author && post?.author?.id !== user?.id) {
postOptions.push({
icon: <MenuIcon Icon={BlockIcon} />,
label: getBlockLabel(post.author.name, {
isCustomFeed,
isBlocked: isBlockedAuthor,
}),
action: async () => {
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,
opts: {
hideToast: true,
},
});
await showMessageAndRemovePost(
`🚫 ${post.author.name} has been ${
isCustomFeed ? 'removed' : 'blocked'
}`,
postIndex,
() => unblock(params),
);
}

invalidatePostCacheById(client, post.id);
},
});
}

if (video && isVideoPost(post)) {
const isEnabled = checkSettingsEnabledState(video.id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,17 @@ const reportReasons: { value: string; label: string }[] = [
];

type ReportUserModalProps = {
offendingUser: Pick<UserShortProfile, 'id' | 'username'>;
defaultBlockUser?: boolean;
feedId?: string;
offendingUser: Pick<UserShortProfile, 'id' | 'username'>;
onBlockUser?: () => void;
};

export const ReportUserModal = ({
offendingUser,
defaultBlockUser,
feedId,
onBlockUser,
}: ReportUserModalProps): ReactElement => {
const { closeModal: onClose } = useLazyModal();
const { displayToast } = useToastNotification();
Expand All @@ -47,11 +51,12 @@ 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`);
onClose();
onBlockUser?.();
},
onError: () => {
displayToast(`❌ Failed to block ${offendingUser.username}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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: () => {
Expand Down
3 changes: 3 additions & 0 deletions packages/shared/src/graphql/fragments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ export const FEED_POST_INFO_FRAGMENT = gql`
image
username
permalink
contentPreference {
status
}
}
type
tags
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/hooks/contentPreference/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type ContentPreferenceMutation = ({
feedId?: string;
opts?: Partial<{
extra: Record<string, unknown>;
hideToast: boolean;
}>;
}) => Promise<void>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,11 @@ export const useContentPreference = ({
entity,
feedId,
});

if (opts?.hideToast) {
return;
}

if (entity === ContentPreferenceType.User) {
displayToast(`🚫 ${entityName} has been blocked`);
} else {
Expand Down
11 changes: 11 additions & 0 deletions packages/shared/src/hooks/usePostById.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 6cb0889

Please sign in to comment.