Merge branch 'develop' into router-migration
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@@ -12,16 +12,17 @@ enum ReportableEntities {
|
||||
}
|
||||
|
||||
type ReportedEntity = {
|
||||
status?: Pick<Status, 'id' | 'reblog_id'>;
|
||||
status?: Pick<Status, 'id'>;
|
||||
statusId?: string;
|
||||
}
|
||||
|
||||
const initReport = (entityType: ReportableEntities, account: Pick<Account, 'id'>, entities?: ReportedEntity) => (dispatch: AppDispatch) => {
|
||||
const { status } = entities || {};
|
||||
const { status, statusId } = entities || {};
|
||||
|
||||
return useModalsStore.getState().actions.openModal('REPORT', {
|
||||
accountId: account.id,
|
||||
entityType,
|
||||
statusIds: status ? [status.id] : [],
|
||||
statusIds: [status?.id, statusId].filter((id): id is string => !!id),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -110,6 +110,7 @@ interface IAccount {
|
||||
items?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
muteExpiresAt?: string | null;
|
||||
blockExpiresAt?: string | null;
|
||||
}
|
||||
|
||||
const Account = ({
|
||||
@@ -138,6 +139,7 @@ const Account = ({
|
||||
items,
|
||||
disabled,
|
||||
muteExpiresAt,
|
||||
blockExpiresAt,
|
||||
}: IAccount) => {
|
||||
const overflowRef = useRef<HTMLDivElement>(null);
|
||||
const actionRef = useRef<HTMLDivElement>(null);
|
||||
@@ -376,6 +378,14 @@ const Account = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{actionType === 'blocking' && blockExpiresAt ? (
|
||||
<>
|
||||
<span className='⁂-separator' />
|
||||
|
||||
<Text theme='muted' size='sm'><RelativeTimestamp timestamp={blockExpiresAt} futureDate /></Text>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{actionType === 'muting' && muteExpiresAt ? (
|
||||
<>
|
||||
<span className='⁂-separator' />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMatch, useNavigate } from '@tanstack/react-router';
|
||||
import { type Account, type CustomEmoji, type Group, GroupRoles } from 'pl-api';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { redactStatus } from 'pl-fe/actions/admin';
|
||||
import { directCompose, mentionCompose, quoteCompose, replyCompose } from 'pl-fe/actions/compose';
|
||||
@@ -24,7 +24,7 @@ import { useClient } from 'pl-fe/hooks/use-client';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import { useInstance } from 'pl-fe/hooks/use-instance';
|
||||
import { useOwnAccount } from 'pl-fe/hooks/use-own-account';
|
||||
import { useBlockAccountMutation, useUnblockAccountMutation } from 'pl-fe/queries/accounts/use-relationship';
|
||||
import { useUnblockAccountMutation } from 'pl-fe/queries/accounts/use-relationship';
|
||||
import { useChats } from 'pl-fe/queries/chats';
|
||||
import { useBlockGroupUserMutation } from 'pl-fe/queries/groups/use-group-blocks';
|
||||
import { useCustomEmojis } from 'pl-fe/queries/instance/use-custom-emojis';
|
||||
@@ -51,8 +51,6 @@ const messages = defineMessages({
|
||||
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||
blocked: { id: 'group.group_mod_block.success', defaultMessage: '@{name} is banned' },
|
||||
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block and report' },
|
||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
|
||||
bookmarkSetFolder: { id: 'status.bookmark_folder', defaultMessage: 'Set bookmark folder' },
|
||||
bookmarkChangeFolder: { id: 'status.bookmark_folder_change', defaultMessage: 'Change bookmark folder' },
|
||||
@@ -579,7 +577,6 @@ const MenuButton: React.FC<IMenuButton> = ({
|
||||
const { mutate: unbookmarkStatus } = useUnbookmarkStatus(status.id);
|
||||
const { mutate: pinStatus } = usePinStatus(status?.id!);
|
||||
const { mutate: unpinStatus } = useUnpinStatus(status?.id!);
|
||||
const { mutate: blockAccount } = useBlockAccountMutation(status.account_id);
|
||||
const { mutate: unblockAccount } = useUnblockAccountMutation(status.account_id);
|
||||
|
||||
const { groupRelationship } = useGroupRelationship(status.group_id || undefined);
|
||||
@@ -683,23 +680,11 @@ const MenuButton: React.FC<IMenuButton> = ({
|
||||
};
|
||||
|
||||
const handleMuteClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
openModal('MUTE', { accountId: status.account.id });
|
||||
openModal('BLOCK_MUTE', { accountId: status.account.id, action: 'MUTE' });
|
||||
};
|
||||
|
||||
const handleBlockClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
const account = status.account;
|
||||
|
||||
openModal('CONFIRM', {
|
||||
heading: <FormattedMessage id='confirmations.block.heading' defaultMessage='Block @{name}' values={{ name: account.acct }} />,
|
||||
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong className='break-words'>@{account.acct}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.blockConfirm),
|
||||
onConfirm: () => blockAccount(),
|
||||
secondary: intl.formatMessage(messages.blockAndReport),
|
||||
onSecondary: () => {
|
||||
blockAccount();
|
||||
dispatch(initReport(ReportableEntities.STATUS, account, { status }));
|
||||
},
|
||||
});
|
||||
openModal('BLOCK_MUTE', { accountId: status.account.id, statusId: status.id, action: 'BLOCK' });
|
||||
};
|
||||
|
||||
const handleUnblockClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
|
||||
@@ -2,6 +2,8 @@ import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { useOwnAccount } from 'pl-fe/hooks/use-own-account';
|
||||
|
||||
import Button from './button';
|
||||
import { ButtonThemes } from './button/useButtonStyles';
|
||||
import IconButton from './icon-button';
|
||||
@@ -12,6 +14,16 @@ const messages = defineMessages({
|
||||
confirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||
});
|
||||
|
||||
const useDefaultCloseIcon = (): string => {
|
||||
const { account } = useOwnAccount();
|
||||
|
||||
if (account?.url === 'https://donotsta.re/users/pmysl' || account?.url === 'https://to.juz.sie.federu.je/@pmysl') {
|
||||
return require('@phosphor-icons/core/regular/twitter-logo.svg');
|
||||
}
|
||||
|
||||
return require('@phosphor-icons/core/regular/x.svg');
|
||||
};
|
||||
|
||||
interface IModal {
|
||||
/** Callback when the modal is cancelled. */
|
||||
cancelAction?: () => void;
|
||||
@@ -50,7 +62,7 @@ const Modal = React.forwardRef<HTMLDivElement, IModal>(({
|
||||
cancelAction,
|
||||
cancelText,
|
||||
children,
|
||||
closeIcon = require('@phosphor-icons/core/regular/x.svg'),
|
||||
closeIcon,
|
||||
closePosition = 'right',
|
||||
confirmationAction,
|
||||
confirmationDisabled,
|
||||
@@ -69,6 +81,10 @@ const Modal = React.forwardRef<HTMLDivElement, IModal>(({
|
||||
const buttonRef = React.useRef<HTMLButtonElement>(null);
|
||||
const [firstRender, setFirstRender] = React.useState(true);
|
||||
|
||||
const defaultCloseIcon = useDefaultCloseIcon();
|
||||
|
||||
closeIcon = closeIcon || defaultCloseIcon;
|
||||
|
||||
React.useEffect(() => {
|
||||
setFirstRender(false);
|
||||
}, []);
|
||||
|
||||
@@ -29,7 +29,6 @@ import { useClient } from 'pl-fe/hooks/use-client';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import { useOwnAccount } from 'pl-fe/hooks/use-own-account';
|
||||
import {
|
||||
useBlockAccountMutation,
|
||||
useFollowAccountMutation,
|
||||
usePinAccountMutation,
|
||||
useRemoveAccountFromFollowersMutation,
|
||||
@@ -82,9 +81,7 @@ const messages = defineMessages({
|
||||
search: { id: 'account.search', defaultMessage: 'Search from @{name}' },
|
||||
searchSelf: { id: 'account.search_self', defaultMessage: 'Search your posts' },
|
||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
|
||||
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block and report' },
|
||||
removeFromFollowersConfirm: { id: 'confirmations.remove_from_followers.confirm', defaultMessage: 'Remove' },
|
||||
userEndorsed: { id: 'account.endorse.success', defaultMessage: 'You are now featuring @{acct} on your profile' },
|
||||
userUnendorsed: { id: 'account.unendorse.success', defaultMessage: 'You are no longer featuring @{acct}' },
|
||||
@@ -146,7 +143,6 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||
const features = useFeatures();
|
||||
const { account: ownAccount } = useOwnAccount();
|
||||
const { mutate: followAccount } = useFollowAccountMutation(account?.id!);
|
||||
const { mutate: blockAccount } = useBlockAccountMutation(account?.id!);
|
||||
const { mutate: unblockAccount } = useUnblockAccountMutation(account?.id!);
|
||||
const { mutate: unmuteAccount } = useUnmuteAccountMutation(account?.id!);
|
||||
const { mutate: pinAccount } = usePinAccountMutation(account?.id!);
|
||||
@@ -201,17 +197,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||
if (account.relationship?.blocking) {
|
||||
unblockAccount();
|
||||
} else {
|
||||
openModal('CONFIRM', {
|
||||
heading: <FormattedMessage id='confirmations.block.heading' defaultMessage='Block @{name}' values={{ name: account.acct }} />,
|
||||
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong className='break-words'>@{account.acct}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.blockConfirm),
|
||||
onConfirm: () => blockAccount(),
|
||||
secondary: intl.formatMessage(messages.blockAndReport),
|
||||
onSecondary: () => {
|
||||
blockAccount();
|
||||
dispatch(initReport(ReportableEntities.ACCOUNT, account));
|
||||
},
|
||||
});
|
||||
openModal('BLOCK_MUTE', { accountId: account.id, action: 'BLOCK' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -276,7 +262,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||
if (account.relationship?.muting) {
|
||||
unmuteAccount();
|
||||
} else {
|
||||
openModal('MUTE', { accountId: account.id });
|
||||
openModal('BLOCK_MUTE', { accountId: account.id, action: 'MUTE' });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import VerificationBadge from 'pl-fe/components/verification-badge';
|
||||
import { useChatContext } from 'pl-fe/contexts/chat-context';
|
||||
import { chatRoute } from 'pl-fe/features/ui/router';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import { useBlockAccountMutation, useUnblockAccountMutation, useRelationshipQuery } from 'pl-fe/queries/accounts/use-relationship';
|
||||
import { useUnblockAccountMutation, useRelationshipQuery } from 'pl-fe/queries/accounts/use-relationship';
|
||||
import { useChat, useChatActions, useChats } from 'pl-fe/queries/chats';
|
||||
import { useModalsActions } from 'pl-fe/stores/modals';
|
||||
|
||||
@@ -48,7 +48,6 @@ const ChatPageMain = () => {
|
||||
const { currentChatId } = useChatContext();
|
||||
const { chatsQuery: { data: chats, isLoading } } = useChats();
|
||||
|
||||
const { mutate: blockAccount } = useBlockAccountMutation(chat?.account.id!);
|
||||
const { mutate: unblockAccount } = useUnblockAccountMutation(chat?.account.id!);
|
||||
|
||||
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
@@ -58,11 +57,9 @@ const ChatPageMain = () => {
|
||||
const isBlocked = !!useRelationshipQuery(chat?.account.id).data?.blocked_by;
|
||||
|
||||
const handleBlockUser = () => {
|
||||
openModal('CONFIRM', {
|
||||
heading: intl.formatMessage(messages.blockHeading, { acct: chat?.account.acct }),
|
||||
message: intl.formatMessage(messages.blockMessage),
|
||||
confirm: intl.formatMessage(messages.blockConfirm),
|
||||
onConfirm: () => blockAccount(),
|
||||
openModal('BLOCK_MUTE', {
|
||||
accountId: chat!.account.id,
|
||||
action: 'BLOCK',
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import { ChatWidgetScreens, useChatContext } from 'pl-fe/contexts/chat-context';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import { useBlockAccountMutation, useUnblockAccountMutation, useRelationshipQuery } from 'pl-fe/queries/accounts/use-relationship';
|
||||
import { useUnblockAccountMutation, useRelationshipQuery } from 'pl-fe/queries/accounts/use-relationship';
|
||||
import { useChatActions } from 'pl-fe/queries/chats';
|
||||
import { useModalsActions } from 'pl-fe/stores/modals';
|
||||
|
||||
@@ -38,7 +38,6 @@ const ChatSettings = () => {
|
||||
const { chat, changeScreen, toggleChatPane } = useChatContext();
|
||||
const { deleteChat } = useChatActions(chat?.id as string);
|
||||
|
||||
const { mutate: blockAccount } = useBlockAccountMutation(chat?.account.id!);
|
||||
const { mutate: unblockAccount } = useUnblockAccountMutation(chat?.account.id!);
|
||||
|
||||
const isBlocked = !!useRelationshipQuery(chat?.account.id).data?.blocked_by;
|
||||
@@ -53,11 +52,9 @@ const ChatSettings = () => {
|
||||
};
|
||||
|
||||
const handleBlockUser = () => {
|
||||
openModal('CONFIRM', {
|
||||
heading: intl.formatMessage(messages.blockHeading, { acct: chat?.account.acct }),
|
||||
message: intl.formatMessage(messages.blockMessage),
|
||||
confirm: intl.formatMessage(messages.blockConfirm),
|
||||
onConfirm: () => blockAccount(),
|
||||
openModal('BLOCK_MUTE', {
|
||||
accountId: chat?.account.id!,
|
||||
action: 'BLOCK',
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import { useOwnAccount } from 'pl-fe/hooks/use-own-account';
|
||||
import { useBlockAccountMutation } from 'pl-fe/queries/accounts/use-relationship';
|
||||
import { useChats } from 'pl-fe/queries/chats';
|
||||
import { useBookmarkStatus, usePinStatus, useReblogStatus, useUnbookmarkStatus, useUnpinStatus, useUnreblogStatus } from 'pl-fe/queries/statuses/use-status-interactions';
|
||||
import { useModalsActions } from 'pl-fe/stores/modals';
|
||||
@@ -64,8 +63,6 @@ const messages = defineMessages({
|
||||
markStatusSensitive: { id: 'admin.statuses.actions.mark_status_sensitive', defaultMessage: 'Mark post sensitive' },
|
||||
markStatusNotSensitive: { id: 'admin.statuses.actions.mark_status_not_sensitive', defaultMessage: 'Mark post not sensitive' },
|
||||
deleteStatus: { id: 'admin.statuses.actions.delete_status', defaultMessage: 'Delete post' },
|
||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block and report' },
|
||||
deleteConfirm: { id: 'confirmations.delete_event.confirm', defaultMessage: 'Delete' },
|
||||
deleteHeading: { id: 'confirmations.delete_event.heading', defaultMessage: 'Delete event' },
|
||||
deleteMessage: { id: 'confirmations.delete_event.message', defaultMessage: 'Are you sure you want to delete this event?' },
|
||||
@@ -95,7 +92,6 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
|
||||
const { mutate: unbookmarkStatus } = useUnbookmarkStatus(status?.id!);
|
||||
const { mutate: pinStatus } = usePinStatus(status?.id!);
|
||||
const { mutate: unpinStatus } = useUnpinStatus(status?.id!);
|
||||
const { mutate: blockAccount } = useBlockAccountMutation(status?.account.id!);
|
||||
|
||||
if (!status || !status.event) {
|
||||
return (
|
||||
@@ -184,21 +180,11 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
|
||||
};
|
||||
|
||||
const handleMuteClick = () => {
|
||||
openModal('MUTE', { accountId: account.id });
|
||||
openModal('BLOCK_MUTE', { accountId: account.id, action: 'MUTE' });
|
||||
};
|
||||
|
||||
const handleBlockClick = () => {
|
||||
openModal('CONFIRM', {
|
||||
heading: <FormattedMessage id='confirmations.block.heading' defaultMessage='Block @{name}' values={{ name: account.acct }} />,
|
||||
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.acct}</strong> }} />,
|
||||
confirm: intl.formatMessage(messages.blockConfirm),
|
||||
onConfirm: () => blockAccount(),
|
||||
secondary: intl.formatMessage(messages.blockAndReport),
|
||||
onSecondary: () => {
|
||||
blockAccount();
|
||||
dispatch(initReport(ReportableEntities.STATUS, account, { status }));
|
||||
},
|
||||
});
|
||||
openModal('BLOCK_MUTE', { accountId: account.id, action: 'BLOCK' });
|
||||
};
|
||||
|
||||
const handleReport = () => {
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useLoggedIn } from 'pl-fe/hooks/use-logged-in';
|
||||
import { useAcceptFollowRequestMutation, useRejectFollowRequestMutation } from 'pl-fe/queries/accounts/use-follow-requests';
|
||||
import {
|
||||
useRelationshipQuery,
|
||||
useBlockAccountMutation,
|
||||
useUnblockAccountMutation,
|
||||
useMuteAccountMutation,
|
||||
useUnmuteAccountMutation,
|
||||
@@ -65,7 +64,6 @@ const ActionButton: React.FC<IActionButton> = ({ account, actionType, small = tr
|
||||
|
||||
const { mutate: followAccount, isPending: isPendingFollow } = useFollowAccountMutation(account.id);
|
||||
const { mutate: unfollowAccount, isPending: isPendingUnfollow } = useUnfollowAccountMutation(account.id);
|
||||
const { mutate: blockAccount } = useBlockAccountMutation(account.id);
|
||||
const { mutate: unblockAccount } = useUnblockAccountMutation(account.id);
|
||||
const { mutate: muteAccount } = useMuteAccountMutation(account.id);
|
||||
const { mutate: unmuteAccount } = useUnmuteAccountMutation(account.id);
|
||||
@@ -87,7 +85,7 @@ const ActionButton: React.FC<IActionButton> = ({ account, actionType, small = tr
|
||||
if (relationship?.blocking) {
|
||||
unblockAccount();
|
||||
} else {
|
||||
blockAccount();
|
||||
openModal('BLOCK_MUTE', { accountId: account.id, action: 'BLOCK' });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import ModalLoading from './modal-loading';
|
||||
const MODAL_COMPONENTS = {
|
||||
ALT_TEXT: lazy(() => import('pl-fe/modals/alt-text-modal')),
|
||||
BIRTHDAYS: lazy(() => import('pl-fe/modals/birthdays-modal')),
|
||||
BLOCK_MUTE: lazy(() => import('pl-fe/modals/block-mute-modal')),
|
||||
BOOST: lazy(() => import('pl-fe/modals/boost-modal')),
|
||||
CIRCLE_EDITOR: lazy(() => import('pl-fe/modals/circle-editor-modal')),
|
||||
COMPARE_HISTORY: lazy(() => import('pl-fe/modals/compare-history-modal')),
|
||||
@@ -39,7 +40,6 @@ const MODAL_COMPONENTS = {
|
||||
MEDIA: lazy(() => import('pl-fe/modals/media-modal')),
|
||||
MENTIONS: lazy(() => import('pl-fe/modals/mentions-modal')),
|
||||
MISSING_DESCRIPTION: lazy(() => import('pl-fe/modals/missing-description-modal')),
|
||||
MUTE: lazy(() => import('pl-fe/modals/mute-modal')),
|
||||
REACTIONS: lazy(() => import('pl-fe/modals/reactions-modal')),
|
||||
REBLOGS: lazy(() => import('pl-fe/modals/reblogs-modal')),
|
||||
REPLY_MENTIONS: lazy(() => import('pl-fe/modals/reply-mentions-modal')),
|
||||
|
||||
@@ -283,6 +283,8 @@
|
||||
"badge_input.placeholder": "Enter a badge…",
|
||||
"birthday_panel.title": "Birthdays",
|
||||
"birthdays_modal.empty": "None of your friends have birthday today.",
|
||||
"block_modal.auto_expire": "Automatically expire block?",
|
||||
"block_modal.note.hint": "You can leave an optional note to remember why you blocked this account. This note is only visible to you.",
|
||||
"bookmark_folders.add.fail": "Failed to create bookmark folder",
|
||||
"bookmark_folders.add.success": "Bookmark folder created successfully",
|
||||
"bookmark_folders.all_bookmarks": "All bookmarks",
|
||||
@@ -1272,6 +1274,9 @@
|
||||
"mute_modal.auto_expire": "Automatically expire mute?",
|
||||
"mute_modal.duration": "Duration",
|
||||
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
||||
"mute_modal.note.hint": "You can leave an optional note to remember why you muted this account. This note is only visible to you.",
|
||||
"mute_modal.note.label.add": "Add account note",
|
||||
"mute_modal.note.label.edit": "Edit account note",
|
||||
"my_groups_panel.title": "My groups",
|
||||
"navigation.chats": "Chats",
|
||||
"navigation.compose": "Compose",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/// <reference path="../node_modules/@types/dom-chromium-ai/index.d.ts" />
|
||||
(window as any).__PL_API_FALLBACK_ACCOUNT = { id: '', acct: 'undefined', url: location.host };
|
||||
(window as any).__PL_API_FALLBACK_ACCOUNT = { id: '', acct: 'undefined', url: location.origin };
|
||||
|
||||
import './polyfills';
|
||||
|
||||
|
||||
210
packages/pl-fe/src/modals/block-mute-modal.tsx
Normal file
210
packages/pl-fe/src/modals/block-mute-modal.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import React, { useState } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { initReport, ReportableEntities } from 'pl-fe/actions/reports';
|
||||
import { useAccount } from 'pl-fe/api/hooks/accounts/use-account';
|
||||
import FormGroup from 'pl-fe/components/ui/form-group';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Modal from 'pl-fe/components/ui/modal';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Textarea from 'pl-fe/components/ui/textarea';
|
||||
import Toggle from 'pl-fe/components/ui/toggle';
|
||||
import DurationSelector from 'pl-fe/features/compose/components/polls/duration-selector';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import { useBlockAccountMutation, useMuteAccountMutation, useUpdateAccountNoteMutation } from 'pl-fe/queries/accounts/use-relationship';
|
||||
import toast from 'pl-fe/toast';
|
||||
|
||||
import type { BlockAccountParams, MuteAccountParams } from 'pl-api';
|
||||
import type { BaseModalProps } from 'pl-fe/features/ui/components/modal-root';
|
||||
|
||||
const messages = defineMessages({
|
||||
notePlaceholder: { id: 'account_note.placeholder', defaultMessage: 'Add a note' },
|
||||
noteSaveFailed: { id: 'account_note.fail', defaultMessage: 'Failed to save note' },
|
||||
});
|
||||
|
||||
interface BlockMuteModalProps {
|
||||
action: 'BLOCK' | 'MUTE';
|
||||
accountId: string;
|
||||
statusId?: string;
|
||||
}
|
||||
|
||||
const BlockMuteModal: React.FC<BlockMuteModalProps & BaseModalProps> = ({ accountId, statusId, onClose, action }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const { account } = useAccount(accountId || undefined, { withRelationship: true });
|
||||
const [notifications, setNotifications] = useState(true);
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [note, setNote] = useState<string | undefined>(undefined);
|
||||
const { notes, blocksDuration, mutesDuration } = useFeatures();
|
||||
const canSetDuration = action === 'MUTE' ? mutesDuration : blocksDuration;
|
||||
|
||||
const currentNote = account?.relationship?.note;
|
||||
|
||||
const { mutate: muteAccount } = useMuteAccountMutation(accountId);
|
||||
const { mutate: blockAccount } = useBlockAccountMutation(accountId);
|
||||
const { mutate: updateAccountNote } = useUpdateAccountNoteMutation(accountId);
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
const handleClick = (callback?: () => void) => {
|
||||
setIsSubmitting(true);
|
||||
const params: MuteAccountParams | BlockAccountParams = { duration: duration || undefined };
|
||||
if (action === 'MUTE') {
|
||||
(params as MuteAccountParams).notifications = notifications;
|
||||
}
|
||||
(action === 'MUTE' ? muteAccount : blockAccount)(params, {
|
||||
onSuccess: () => {
|
||||
setIsSubmitting(false);
|
||||
onClose('BLOCK_MUTE');
|
||||
if (callback) callback();
|
||||
},
|
||||
});
|
||||
if (notes && note !== undefined && note !== currentNote) {
|
||||
updateAccountNote(note, {
|
||||
onError: () => toast.error(messages.noteSaveFailed),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlockAndReport = () => {
|
||||
handleClick(() => dispatch(initReport(ReportableEntities.STATUS, account, { statusId })));
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
onClose('BLOCK_MUTE');
|
||||
};
|
||||
|
||||
const toggleNotifications = () => {
|
||||
setNotifications(notifications => !notifications);
|
||||
};
|
||||
|
||||
const handleChangeMuteDuration = (expiresIn: number): void => {
|
||||
setDuration(expiresIn);
|
||||
};
|
||||
|
||||
const toggleAutoExpire = () => setDuration(duration ? 0 : 2 * 60 * 60 * 24);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={action === 'MUTE' ? (
|
||||
<FormattedMessage id='confirmations.mute.heading' defaultMessage='Mute @{name}' values={{ name: account.acct }} />
|
||||
) : (
|
||||
<FormattedMessage id='confirmations.block.heading' defaultMessage='Block @{name}' values={{ name: account.acct }} />
|
||||
)}
|
||||
onClose={handleCancel}
|
||||
confirmationAction={() => handleClick()}
|
||||
confirmationText={action === 'MUTE' ? (
|
||||
<FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' />
|
||||
) : (
|
||||
<FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' />
|
||||
)}
|
||||
confirmationDisabled={isSubmitting}
|
||||
secondaryAction={action === 'BLOCK' ? handleBlockAndReport : undefined}
|
||||
secondaryText={<FormattedMessage id='confirmations.block.block_and_report' defaultMessage='Block and report' />}
|
||||
secondaryDisabled={isSubmitting}
|
||||
cancelText={<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />}
|
||||
cancelAction={handleCancel}
|
||||
>
|
||||
<Stack space={4}>
|
||||
<Text>
|
||||
{action === 'MUTE' ? (
|
||||
<FormattedMessage
|
||||
id='confirmations.mute.message'
|
||||
defaultMessage='Are you sure you want to mute {name}?'
|
||||
values={{ name: <strong className='break-words'>@{account.acct}</strong> }}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='confirmations.block.message'
|
||||
defaultMessage='Are you sure you want to block {name}?'
|
||||
values={{ name: <strong className='break-words'>@{account.acct}</strong> }}
|
||||
/>
|
||||
)}
|
||||
</Text>
|
||||
|
||||
{action === 'MUTE' && (
|
||||
<label>
|
||||
<HStack alignItems='center' space={2}>
|
||||
<Text tag='span' theme='muted'>
|
||||
<FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' />
|
||||
</Text>
|
||||
|
||||
<Toggle
|
||||
checked={notifications}
|
||||
onChange={toggleNotifications}
|
||||
/>
|
||||
</HStack>
|
||||
</label>
|
||||
)}
|
||||
|
||||
{notes && (
|
||||
<FormGroup
|
||||
labelText={(
|
||||
currentNote ? (
|
||||
<FormattedMessage id='mute_modal.note.label.edit' defaultMessage='Edit account note' />
|
||||
) : (
|
||||
<FormattedMessage id='mute_modal.note.label.add' defaultMessage='Add account note' />
|
||||
)
|
||||
)}
|
||||
hintText={
|
||||
action === 'MUTE' ? (
|
||||
<FormattedMessage
|
||||
id='mute_modal.note.hint'
|
||||
defaultMessage='You can leave an optional note to remember why you muted this account. This note is only visible to you.'
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='block_modal.note.hint'
|
||||
defaultMessage='You can leave an optional note to remember why you blocked this account. This note is only visible to you.'
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Textarea
|
||||
className='mt-1'
|
||||
value={note === undefined ? currentNote || '' : note}
|
||||
onChange={({ target }) => setNote(target.value)}
|
||||
autoComplete='off'
|
||||
placeholder={intl.formatMessage(messages.notePlaceholder)}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
||||
{canSetDuration && (
|
||||
<>
|
||||
<label>
|
||||
<HStack alignItems='center' space={2}>
|
||||
<Text tag='span'>
|
||||
{action === 'MUTE' ? (
|
||||
<FormattedMessage id='mute_modal.auto_expire' defaultMessage='Automatically expire mute?' />
|
||||
) : (
|
||||
<FormattedMessage id='block_modal.auto_expire' defaultMessage='Automatically expire block?' />
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<Toggle
|
||||
checked={duration !== 0}
|
||||
onChange={toggleAutoExpire}
|
||||
/>
|
||||
</HStack>
|
||||
</label>
|
||||
|
||||
{duration !== 0 && (
|
||||
<Stack space={2}>
|
||||
<Text weight='medium'><FormattedMessage id='mute_modal.duration' defaultMessage='Duration' />: </Text>
|
||||
|
||||
<DurationSelector onDurationChange={handleChangeMuteDuration} value={duration} />
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export { BlockMuteModal as default, type BlockMuteModalProps };
|
||||
@@ -1,122 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useAccount } from 'pl-fe/api/hooks/accounts/use-account';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Modal from 'pl-fe/components/ui/modal';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Toggle from 'pl-fe/components/ui/toggle';
|
||||
import DurationSelector from 'pl-fe/features/compose/components/polls/duration-selector';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import { useMuteAccountMutation } from 'pl-fe/queries/accounts/use-relationship';
|
||||
|
||||
import type { BaseModalProps } from 'pl-fe/features/ui/components/modal-root';
|
||||
|
||||
interface MuteModalProps {
|
||||
accountId: string;
|
||||
}
|
||||
|
||||
const MuteModal: React.FC<MuteModalProps & BaseModalProps> = ({ accountId, onClose }) => {
|
||||
const { account } = useAccount(accountId || undefined);
|
||||
const [notifications, setNotifications] = useState(true);
|
||||
const [duration, setDuration] = useState(0);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const mutesDuration = useFeatures().mutesDuration;
|
||||
|
||||
const { mutate: muteAccount } = useMuteAccountMutation(accountId);
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
const handleClick = () => {
|
||||
setIsSubmitting(true);
|
||||
muteAccount({ notifications, duration }, {
|
||||
onSuccess: () => {
|
||||
setIsSubmitting(false);
|
||||
onClose('MUTE');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
onClose('MUTE');
|
||||
};
|
||||
|
||||
const toggleNotifications = () => {
|
||||
setNotifications(notifications => !notifications);
|
||||
};
|
||||
|
||||
const handleChangeMuteDuration = (expiresIn: number): void => {
|
||||
setDuration(expiresIn);
|
||||
};
|
||||
|
||||
const toggleAutoExpire = () => setDuration(duration ? 0 : 2 * 60 * 60 * 24);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='confirmations.mute.heading'
|
||||
defaultMessage='Mute @{name}'
|
||||
values={{ name: account.acct }}
|
||||
/>
|
||||
}
|
||||
onClose={handleCancel}
|
||||
confirmationAction={handleClick}
|
||||
confirmationText={<FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' />}
|
||||
confirmationDisabled={isSubmitting}
|
||||
cancelText={<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />}
|
||||
cancelAction={handleCancel}
|
||||
>
|
||||
<Stack space={4}>
|
||||
<Text>
|
||||
<FormattedMessage
|
||||
id='confirmations.mute.message'
|
||||
defaultMessage='Are you sure you want to mute {name}?'
|
||||
values={{ name: <strong className='break-words'>@{account.acct}</strong> }}
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<label>
|
||||
<HStack alignItems='center' space={2}>
|
||||
<Text tag='span' theme='muted'>
|
||||
<FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' />
|
||||
</Text>
|
||||
|
||||
<Toggle
|
||||
checked={notifications}
|
||||
onChange={toggleNotifications}
|
||||
/>
|
||||
</HStack>
|
||||
</label>
|
||||
|
||||
{mutesDuration && (
|
||||
<>
|
||||
<label>
|
||||
<HStack alignItems='center' space={2}>
|
||||
<Text tag='span'>
|
||||
<FormattedMessage id='mute_modal.auto_expire' defaultMessage='Automatically expire mute?' />
|
||||
</Text>
|
||||
|
||||
<Toggle
|
||||
checked={duration !== 0}
|
||||
onChange={toggleAutoExpire}
|
||||
/>
|
||||
</HStack>
|
||||
</label>
|
||||
|
||||
{duration !== 0 && (
|
||||
<Stack space={2}>
|
||||
<Text weight='medium'><FormattedMessage id='mute_modal.duration' defaultMessage='Duration' />: </Text>
|
||||
|
||||
<DurationSelector onDurationChange={handleChangeMuteDuration} value={duration} />
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export { MuteModal as default, type MuteModalProps };
|
||||
@@ -111,7 +111,7 @@ const ReportModal: React.FC<BaseModalProps & ReportModalProps> = ({ onClose, acc
|
||||
});
|
||||
|
||||
if (block && account) {
|
||||
blockAccount();
|
||||
blockAccount(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ const BlocksPage: React.FC = () => {
|
||||
itemClassName={clsx('pb-4', { 'last:pb-0': !hasNextPage })}
|
||||
isLoading={isFetching}
|
||||
>
|
||||
{data.map((accountId) => (
|
||||
<AccountContainer key={accountId} id={accountId} actionType='blocking' />
|
||||
{data.map(([accountId, blockExpiresAt]) => (
|
||||
<AccountContainer key={accountId} id={accountId} actionType='blocking' blockExpiresAt={blockExpiresAt} />
|
||||
))}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
|
||||
@@ -26,13 +26,11 @@ const messages = defineMessages({
|
||||
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
|
||||
redraftHeading: { id: 'confirmations.redraft.heading', defaultMessage: 'Delete & redraft' },
|
||||
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this post and re-draft it? Favorites and reposts will be lost, and replies to the original post will be orphaned.' },
|
||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||
revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
|
||||
hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
|
||||
detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
|
||||
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
||||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block and report' },
|
||||
treeView: { id: 'status.thread.tree_view', defaultMessage: 'Tree view' },
|
||||
linearView: { id: 'status.thread.linear_view', defaultMessage: 'Linear view' },
|
||||
expandAll: { id: 'status.thread.expand_all', defaultMessage: 'Expand all posts' },
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { makePaginatedResponseQuery } from '../utils/make-paginated-response-query';
|
||||
import { minifyAccountList, minifyMutedAccountList } from '../utils/minify-list';
|
||||
import { minifyBlockedAccountList, minifyMutedAccountList } from '../utils/minify-list';
|
||||
|
||||
const useBlocks = makePaginatedResponseQuery(
|
||||
['accountsLists', 'blocked'],
|
||||
(client) => client.filtering.getBlocks({ with_relationships: true }).then(minifyAccountList),
|
||||
(client) => client.filtering.getBlocks({ with_relationships: true }).then(minifyBlockedAccountList),
|
||||
);
|
||||
|
||||
const useMutes = makePaginatedResponseQuery(
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useClient } from 'pl-fe/hooks/use-client';
|
||||
import { useLoggedIn } from 'pl-fe/hooks/use-logged-in';
|
||||
|
||||
import type { MinifiedSuggestion } from '../trends/use-suggested-accounts';
|
||||
import type { FollowAccountParams, MuteAccountParams, Relationship } from 'pl-api';
|
||||
import type { BlockAccountParams, FollowAccountParams, MuteAccountParams, Relationship } from 'pl-api';
|
||||
|
||||
const updateRelationship = (accountId: string, changes: Partial<Relationship> | ((relationship: Relationship) => Relationship), queryClient: ReturnType<typeof useQueryClient>) => {
|
||||
const previousRelationship = queryClient.getQueryData<Relationship>(['accountRelationships', accountId]);
|
||||
@@ -89,7 +89,7 @@ const useBlockAccountMutation = (accountId: string) => {
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ['accountRelationships', accountId],
|
||||
mutationFn: () => client.filtering.blockAccount(accountId),
|
||||
mutationFn: (params?: BlockAccountParams) => client.filtering.blockAccount(accountId, params),
|
||||
onMutate: () => updateRelationship(accountId, {
|
||||
blocking: true,
|
||||
followed_by: false,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { store } from 'pl-fe/store';
|
||||
|
||||
import { queryClient } from '../client';
|
||||
|
||||
import type { Account, AdminAccount, AdminReport, MutedAccount, PaginatedResponse, Status } from 'pl-api';
|
||||
import type { Account, AdminAccount, AdminReport, BlockedAccount, MutedAccount, PaginatedResponse, Status } from 'pl-api';
|
||||
|
||||
const minifyList = <T1, T2>({ previous, next, items, ...response }: PaginatedResponse<T1>, minifier: (value: T1) => T2, importer?: (items: Array<T1>) => void): PaginatedResponse<T2> => {
|
||||
importer?.(items);
|
||||
@@ -26,6 +26,11 @@ const minifyAccountList = (response: PaginatedResponse<Account>): PaginatedRespo
|
||||
store.dispatch(importEntities({ accounts }) as any);
|
||||
});
|
||||
|
||||
const minifyBlockedAccountList = (response: PaginatedResponse<BlockedAccount>): PaginatedResponse<[string, string | null]> =>
|
||||
minifyList(response, (account) => [account.id, account.block_expires_at], (accounts) => {
|
||||
store.dispatch(importEntities({ accounts }) as any);
|
||||
});
|
||||
|
||||
const minifyMutedAccountList = (response: PaginatedResponse<MutedAccount>): PaginatedResponse<[string, string | null]> =>
|
||||
minifyList(response, (account) => [account.id, account.mute_expires_at], (accounts) => {
|
||||
store.dispatch(importEntities({ accounts }) as any);
|
||||
@@ -75,4 +80,4 @@ const minifyAdminReportList = (response: PaginatedResponse<AdminReport>) =>
|
||||
}
|
||||
});
|
||||
|
||||
export { minifyList, minifyAccountList, minifyMutedAccountList, minifyStatusList, minifyAdminAccount, minifyAdminAccountList, minifyAdminReport, minifyAdminReportList };
|
||||
export { minifyList, minifyAccountList, minifyBlockedAccountList, minifyMutedAccountList, minifyStatusList, minifyAdminAccount, minifyAdminAccountList, minifyAdminReport, minifyAdminReportList };
|
||||
|
||||
@@ -4,6 +4,7 @@ import { mutative } from 'zustand-mutative';
|
||||
import type { ICryptoAddress } from 'pl-fe/features/crypto-donate/components/crypto-address';
|
||||
import type { ModalType } from 'pl-fe/features/ui/components/modal-root';
|
||||
import type { AltTextModalProps } from 'pl-fe/modals/alt-text-modal';
|
||||
import type { BlockMuteModalProps } from 'pl-fe/modals/block-mute-modal';
|
||||
import type { BoostModalProps } from 'pl-fe/modals/boost-modal';
|
||||
import type { CircleEditorModalProps } from 'pl-fe/modals/circle-editor-modal';
|
||||
import type { CompareHistoryModalProps } from 'pl-fe/modals/compare-history-modal';
|
||||
@@ -29,7 +30,6 @@ import type { ListEditorModalProps } from 'pl-fe/modals/list-editor-modal';
|
||||
import type { MediaModalProps } from 'pl-fe/modals/media-modal';
|
||||
import type { MentionsModalProps } from 'pl-fe/modals/mentions-modal';
|
||||
import type { MissingDescriptionModalProps } from 'pl-fe/modals/missing-description-modal';
|
||||
import type { MuteModalProps } from 'pl-fe/modals/mute-modal';
|
||||
import type { ReactionsModalProps } from 'pl-fe/modals/reactions-modal';
|
||||
import type { ReblogsModalProps } from 'pl-fe/modals/reblogs-modal';
|
||||
import type { ReplyMentionsModalProps } from 'pl-fe/modals/reply-mentions-modal';
|
||||
@@ -68,7 +68,7 @@ type OpenModalProps =
|
||||
| [type: 'MEDIA', props: MediaModalProps]
|
||||
| [type: 'MENTIONS', props: MentionsModalProps]
|
||||
| [type: 'MISSING_DESCRIPTION', props: MissingDescriptionModalProps]
|
||||
| [type: 'MUTE', props: MuteModalProps]
|
||||
| [type: 'BLOCK_MUTE', props: BlockMuteModalProps]
|
||||
| [type: 'REACTIONS', props: ReactionsModalProps]
|
||||
| [type: 'REBLOGS', props: ReblogsModalProps]
|
||||
| [type: 'REPLY_MENTIONS', props: ReplyMentionsModalProps]
|
||||
|
||||
Reference in New Issue
Block a user