Skip to content

Commit

Permalink
feat: Report and block users (#4056)
Browse files Browse the repository at this point in the history
* feat: ReportUserModal (#4051)

* feat: ReportUserModal

* formatting

* lint

* remove unnecessary check

* update conditional

* remove need to pass onClose

* capitalize values

* feat: Add block and report to user profile (#4054)

* feat: ReportUserModal

* formatting

* lint

* remove unnecessary check

* update conditional

* remove need to pass onClose

* capitalize values

* feat: Add block and report to user profile

* feat: Block, report, share on profile available for everyone (#4059)

* feat: Block, report, share on profile available for everyone

* update copy

* update to still show share button when only 1 opt available

* fix: Should not show following when blocked (#4064)

* fix: Should not show following when blocked

* update syntax

* fix: No longer open report modal when reporting user (#4069)

* fix: No longer open report modal when reporting user

* add block request key to status map

* feat: add block user option on post context menu (#4053)

* 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 <amartrebinjac@gmail.com>

* update hateful key

* Update packages/shared/src/components/modals/report/ReportUserModal.tsx

Co-authored-by: Lee Hansel Solevilla <13744167+sshanzel@users.noreply.github.com>

* fix: cache invalidation (#4078)

* move query invalidation

* remove unused hook

* remove follow button on profile when blocked

* feat: Remove default add btn in advanced settings, add block btn on u… (#4075)

* feat: Remove default add btn in advanced settings, add block btn on users

* make reusable component

* invalidate specific feed

* undo feed key

* use onsettled to close

* add author block

* use username instead of name

* try invalidate userblocked request key

* use correct username

* remove unnecessary feed invalidation

* fix: block button inside link in source list (#4077)

* feat: Remove default add btn in advanced settings, add block btn on users

* make reusable component

* fix: button inside link

* fix: lint unused var

* fix: moved key from children to parent

* fix: UserList open in new tab

---------

Co-authored-by: Amar Trebinjac <amartrebinjac@gmail.com>
Co-authored-by: Amar Trebinjac <36768584+AmarTrebinjac@users.noreply.github.com>

* fix: invalidation of blocked users for settings

---------

Co-authored-by: Luca Pagliaro <pagliaroluca@gmail.com>
Co-authored-by: Lee Hansel Solevilla <13744167+sshanzel@users.noreply.github.com>
Co-authored-by: capJavert <dev@kickass.website>
  • Loading branch information
4 people authored Jan 20, 2025
1 parent ba8f283 commit c0e04c6
Show file tree
Hide file tree
Showing 23 changed files with 582 additions and 123 deletions.
15 changes: 11 additions & 4 deletions packages/shared/src/components/CustomFeedOptionsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type CustomFeedOptionsMenuProps = {
onUndo?: (feedId: string) => void;
className?: string;
shareProps: UseShareOrCopyLinkProps;
additionalOptions?: MenuItemProps[];
};

const CustomFeedOptionsMenu = ({
Expand All @@ -28,6 +29,7 @@ const CustomFeedOptionsMenu = ({
onAdd,
onUndo,
onCreateNewFeed,
additionalOptions = [],
}: CustomFeedOptionsMenuProps): ReactElement => {
const { showPlusSubscription } = usePlusSubscription();
const { openModal } = useLazyModal();
Expand Down Expand Up @@ -57,14 +59,17 @@ const CustomFeedOptionsMenu = ({
label: 'Share',
action: () => onShareOrCopyLink(),
},
{
];

if (showPlusSubscription) {
options.push({
icon: <MenuIcon Icon={HashtagIcon} />,
label: 'Add to custom feed',
action: handleOpenModal,
},
];
});
}

if (!showPlusSubscription) {
if (additionalOptions.length === 0 && !showPlusSubscription) {
return (
<Button
variant={ButtonVariant.Float}
Expand All @@ -75,6 +80,8 @@ const CustomFeedOptionsMenu = ({
);
}

options.push(...additionalOptions);

return (
<>
<Button
Expand Down
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
138 changes: 110 additions & 28 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 @@ -46,11 +46,14 @@ import {
useToastNotification,
} from '../hooks';
import type { AllFeedPages } from '../lib/query';
import { generateQueryKey } from '../lib/query';
import { generateQueryKey, RequestKey } 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,67 @@ 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),
);
}

client.invalidateQueries({
queryKey: generateQueryKey(
RequestKey.ContentPreference,
user,
RequestKey.UserBlocked,
{
feedId: customFeedId || user?.id,
entity: ContentPreferenceType.User,
},
),
});

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

if (video && isVideoPost(post)) {
const isEnabled = checkSettingsEnabledState(video.id);
Expand Down
39 changes: 37 additions & 2 deletions packages/shared/src/components/comments/CommentActionButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
FlagIcon,
DownvoteIcon,
AddUserIcon,
BlockIcon,
} from '../icons';
import type { Comment } from '../../graphql/comments';
import { Roles } from '../../lib/user';
Expand All @@ -38,7 +39,7 @@ import { labels, largeNumberFormat } from '../../lib';
import { useToastNotification } from '../../hooks/useToastNotification';
import type { VoteEntityPayload } from '../../hooks';
import { useVoteComment, voteMutationHandlers } from '../../hooks';
import { RequestKey } from '../../lib/query';
import { generateQueryKey, RequestKey } from '../../lib/query';
import { useRequestProtocol } from '../../hooks/useRequestProtocol';
import { getCompanionWrapper } from '../../lib/extension';
import { useContentPreference } from '../../hooks/contentPreference/useContentPreference';
Expand Down Expand Up @@ -89,7 +90,7 @@ export default function CommentActionButtons({
userState: comment.userState,
};
});
const { follow, unfollow } = useContentPreference();
const { follow, unfollow, block, unblock } = useContentPreference();

useEffect(() => {
setVoteState({
Expand Down Expand Up @@ -176,6 +177,40 @@ export default function CommentActionButtons({
});
}

if (user && user.id !== comment.author.id) {
commentOptions.push({
icon: <BlockIcon />,
label: `Block ${post.author.username}`,
action: async () => {
const params = {
id: comment.author.id,
entity: ContentPreferenceType.User,
entityName: comment.author.username,
feedId: user.id,
opts: {
hideToast: true,
},
};

await block(params);

const commentQueryKey = generateQueryKey(RequestKey.PostComments);
client.invalidateQueries({
queryKey: commentQueryKey,
});

displayToast(`🚫 ${comment.author.name} has been blocked`, {
onUndo: () => {
unblock(params);
client.invalidateQueries({
queryKey: commentQueryKey,
});
},
});
},
});
}

const shouldShowFollow =
!useIsSpecialUser({ userId: comment?.author?.id }) &&
isLoggedIn &&
Expand Down
59 changes: 59 additions & 0 deletions packages/shared/src/components/contentPreference/BlockButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { ReactElement } from 'react';
import React from 'react';
import type { ContentPreferenceType } from '../../graphql/contentPreference';
import { ContentPreferenceStatus } from '../../graphql/contentPreference';
import type { ButtonProps } from '../buttons/Button';
import { Button } from '../buttons/Button';
import { ButtonVariant, ButtonSize } from '../buttons/common';
import { useContentPreference } from '../../hooks/contentPreference/useContentPreference';

type BlockButtonProps = {
feedId: string;
entityId: string;
entityName: string;
status?: ContentPreferenceStatus;
entityType: ContentPreferenceType;
} & Pick<ButtonProps<'button'>, 'variant' | 'size' | 'className'>;

const BlockButton = ({
variant = ButtonVariant.Secondary,
size = ButtonSize.Small,
feedId,
entityId,
entityName,
entityType,
status,
...attrs
}: BlockButtonProps): ReactElement => {
const { block, unblock } = useContentPreference();

return (
<Button
{...attrs}
onClick={() => {
if (status === ContentPreferenceStatus.Blocked) {
unblock({
id: entityId,
entity: entityType,
entityName,
feedId,
});
} else {
block({
id: entityId,
entity: entityType,
entityName,
feedId,
});
}
}}
type="button"
variant={variant}
size={size}
>
{status === ContentPreferenceStatus.Blocked ? 'Unblock' : 'Block'}
</Button>
);
};

export default BlockButton;
Loading

0 comments on commit c0e04c6

Please sign in to comment.