Compare commits
118 Commits
feat/reque
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c65c08aac | ||
|
|
f31d2c4729 | ||
|
|
fd4204f0f5 | ||
|
|
8a5943f206 | ||
|
|
f32a9c20ef | ||
|
|
2dc0009675 | ||
|
|
3ad7faaf9b | ||
|
|
400639a94e | ||
|
|
ef3d428076 | ||
|
|
2c76b17919 | ||
|
|
e98d93db1a | ||
|
|
ba84a32c0d | ||
|
|
fe5e7cbf4b | ||
|
|
45ce57687a | ||
|
|
4b43747bcc | ||
|
|
714f244d6a | ||
|
|
acfaf7eb45 | ||
|
|
b83d8c8f5c | ||
|
|
dd5d2698a6 | ||
|
|
c5e59616fa | ||
|
|
fc79fdabf4 | ||
|
|
7dd683c3d8 | ||
|
|
4a5421a9ba | ||
|
|
24b9c00b2a | ||
|
|
5125abdbf0 | ||
|
|
724f8bb538 | ||
|
|
512f6e51e4 | ||
|
|
1b36dded3f | ||
|
|
77511bf199 | ||
|
|
26d77af3a4 | ||
|
|
2b111f1c34 | ||
|
|
a6392cb2db | ||
|
|
6d55b50e5e | ||
|
|
f7dc6256ba | ||
|
|
f1df1caea5 | ||
|
|
d23c120dc0 | ||
|
|
353efa39d0 | ||
|
|
21cdf7d113 | ||
|
|
f858f025fe | ||
|
|
c92bb134d4 | ||
|
|
7e0e2a92e3 | ||
|
|
e840489e57 | ||
|
|
800f4668ec | ||
|
|
f2c37babeb | ||
|
|
24095eac03 | ||
|
|
8c782e0c93 | ||
|
|
08ce7f24b8 | ||
|
|
67e794aca4 | ||
|
|
c9c429d027 | ||
|
|
31ac11f4eb | ||
|
|
f072a4e11d | ||
|
|
b24c23e549 | ||
|
|
e602e9f758 | ||
|
|
afd46ebee8 | ||
|
|
be47d1e56c | ||
|
|
0e612f01f1 | ||
|
|
cd9d9ae8d2 | ||
|
|
ea2765f8c7 | ||
|
|
448e7c3a7d | ||
|
|
6ab3cd77a7 | ||
|
|
3dfbb0e0a5 | ||
|
|
726f62b9b6 | ||
|
|
81c30347f6 | ||
|
|
a397893217 | ||
|
|
12c5bbd778 | ||
|
|
06261c1118 | ||
|
|
686fe6fdf8 | ||
|
|
c80a1fa261 | ||
|
|
6167011ee5 | ||
|
|
bc399d95a0 | ||
|
|
519dd17d37 | ||
|
|
0761593411 | ||
|
|
caa45415b1 | ||
|
|
ed297d9d6c | ||
|
|
c6486c3643 | ||
|
|
f9695068a8 | ||
|
|
fd90adba4d | ||
|
|
970aa612fe | ||
|
|
71169944dc | ||
|
|
d77f9eb44e | ||
|
|
22f2037ea6 | ||
|
|
2cdf755c20 | ||
|
|
f99ab47c01 | ||
|
|
7e4175ff21 | ||
|
|
4157432aa7 | ||
|
|
741b2c4e9f | ||
|
|
6dbe9ea814 | ||
|
|
96328ab5c6 | ||
|
|
c31709681a | ||
|
|
0a3d7c5915 | ||
|
|
912c63e562 | ||
|
|
077513151e | ||
|
|
191f39e8bd | ||
|
|
048b6c1d90 | ||
|
|
a6a065ac0b | ||
|
|
abb0b2d763 | ||
|
|
d2668d3f49 | ||
|
|
bee6eb288f | ||
|
|
3473aed27e | ||
|
|
a9144a2167 | ||
|
|
59822d6e42 | ||
|
|
0bd1bee1fa | ||
|
|
46326f9d16 | ||
|
|
54b96cc451 | ||
|
|
7d2a187865 | ||
|
|
17df5dd072 | ||
|
|
4b48a36ad3 | ||
|
|
16bd3e9fd5 | ||
|
|
6360c82898 | ||
|
|
f1df9eeb2f | ||
|
|
a3973791fb | ||
|
|
157ab80be1 | ||
|
|
ac0f95b773 | ||
|
|
45ebfb937e | ||
|
|
85325cda7f | ||
|
|
5fc02d0d94 | ||
|
|
16902f85fb | ||
|
|
17b5a0bf9c |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -9,6 +9,8 @@ jobs:
|
||||
semantic-release:
|
||||
name: Tag and release latest version
|
||||
runs-on: ubuntu-20.04
|
||||
env:
|
||||
HUSKY: 0
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
1527
CHANGELOG.md
Normal file
1527
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "overseerr",
|
||||
"version": "0.1.0",
|
||||
"version": "1.33.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "nodemon -e ts --watch server --watch overseerr-api.yml -e .json,.ts,.yml -x ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/index.ts",
|
||||
|
||||
@@ -31,7 +31,7 @@ import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import useSWR from 'swr';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -144,7 +144,6 @@ const IssueDetails = () => {
|
||||
autoDismiss: true,
|
||||
});
|
||||
revalidateIssue();
|
||||
mutate('/api/v1/issue/count');
|
||||
} catch (e) {
|
||||
addToast(intl.formatMessage(messages.toaststatusupdatefailed), {
|
||||
appearance: 'error',
|
||||
@@ -156,7 +155,6 @@ const IssueDetails = () => {
|
||||
const deleteIssue = async () => {
|
||||
try {
|
||||
await axios.delete(`/api/v1/issue/${issueData.id}`);
|
||||
mutate('/api/v1/issue/count');
|
||||
|
||||
addToast(intl.formatMessage(messages.toastissuedeleted), {
|
||||
appearance: 'success',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Badge from '@app/components/Common/Badge';
|
||||
import { menuMessages } from '@app/components/Layout/Sidebar';
|
||||
import useClickOutside from '@app/hooks/useClickOutside';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
@@ -28,11 +27,6 @@ import { useRouter } from 'next/router';
|
||||
import { cloneElement, useRef, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
interface MobileMenuProps {
|
||||
pendingRequestsCount: number;
|
||||
openIssuesCount: number;
|
||||
}
|
||||
|
||||
interface MenuLink {
|
||||
href: string;
|
||||
svgIcon: JSX.Element;
|
||||
@@ -45,10 +39,7 @@ interface MenuLink {
|
||||
dataTestId?: string;
|
||||
}
|
||||
|
||||
const MobileMenu = ({
|
||||
pendingRequestsCount,
|
||||
openIssuesCount,
|
||||
}: MobileMenuProps) => {
|
||||
const MobileMenu = () => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const intl = useIntl();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -153,7 +144,7 @@ const MobileMenu = ({
|
||||
return (
|
||||
<Link key={`mobile-menu-link-${link.href}`} href={link.href}>
|
||||
<a
|
||||
className={`flex items-center ${
|
||||
className={`flex items-center space-x-2 ${
|
||||
isActive ? 'text-indigo-500' : ''
|
||||
}`}
|
||||
onKeyDown={(e) => {
|
||||
@@ -168,25 +159,7 @@ const MobileMenu = ({
|
||||
{cloneElement(isActive ? link.svgIconSelected : link.svgIcon, {
|
||||
className: 'h-5 w-5',
|
||||
})}
|
||||
<span className="ml-2">{link.content}</span>
|
||||
{link.href === '/requests' &&
|
||||
pendingRequestsCount > 0 &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<div className="ml-auto">
|
||||
<Badge className="rounded-md border-indigo-500 bg-gradient-to-br from-indigo-600 to-purple-600">
|
||||
{pendingRequestsCount}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
{link.href === '/issues' &&
|
||||
openIssuesCount > 0 &&
|
||||
hasPermission(Permission.MANAGE_ISSUES) && (
|
||||
<div className="ml-auto">
|
||||
<Badge className="rounded-md border-indigo-500 bg-gradient-to-br from-indigo-600 to-purple-600">
|
||||
{openIssuesCount}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
<span>{link.content}</span>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
@@ -202,7 +175,7 @@ const MobileMenu = ({
|
||||
return (
|
||||
<Link key={`mobile-menu-link-${link.href}`} href={link.href}>
|
||||
<a
|
||||
className={`relative flex flex-col items-center space-y-1 ${
|
||||
className={`flex flex-col items-center space-y-1 ${
|
||||
isActive ? 'text-indigo-500' : ''
|
||||
}`}
|
||||
>
|
||||
@@ -212,21 +185,6 @@ const MobileMenu = ({
|
||||
className: 'h-6 w-6',
|
||||
}
|
||||
)}
|
||||
{link.href === '/requests' &&
|
||||
pendingRequestsCount > 0 &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<div className="absolute left-3 bottom-3">
|
||||
<Badge
|
||||
className={`bg-gradient-to-br ${
|
||||
router.pathname.match(link.activeRegExp)
|
||||
? 'border-indigo-600 from-indigo-700 to-purple-700'
|
||||
: 'border-indigo-500 from-indigo-600 to-purple-600'
|
||||
} !px-1 leading-none`}
|
||||
>
|
||||
{pendingRequestsCount}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Badge from '@app/components/Common/Badge';
|
||||
import VersionStatus from '@app/components/Layout/VersionStatus';
|
||||
import useClickOutside from '@app/hooks/useClickOutside';
|
||||
import { Permission, useUser } from '@app/hooks/useUser';
|
||||
@@ -31,8 +30,6 @@ export const menuMessages = defineMessages({
|
||||
interface SidebarProps {
|
||||
open?: boolean;
|
||||
setClosed: () => void;
|
||||
pendingRequestsCount: number;
|
||||
openIssuesCount: number;
|
||||
}
|
||||
|
||||
interface SidebarLinkProps {
|
||||
@@ -101,12 +98,7 @@ const SidebarLinks: SidebarLinkProps[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const Sidebar = ({
|
||||
open,
|
||||
setClosed,
|
||||
pendingRequestsCount,
|
||||
openIssuesCount,
|
||||
}: SidebarProps) => {
|
||||
const Sidebar = ({ open, setClosed }: SidebarProps) => {
|
||||
const navRef = useRef<HTMLDivElement>(null);
|
||||
const router = useRouter();
|
||||
const intl = useIntl();
|
||||
@@ -262,40 +254,6 @@ const Sidebar = ({
|
||||
{intl.formatMessage(
|
||||
menuMessages[sidebarLink.messagesKey]
|
||||
)}
|
||||
{sidebarLink.messagesKey === 'requests' &&
|
||||
pendingRequestsCount > 0 &&
|
||||
hasPermission(Permission.MANAGE_REQUESTS) && (
|
||||
<div className="ml-auto">
|
||||
<Badge
|
||||
className={`rounded-md bg-gradient-to-br ${
|
||||
router.pathname.match(
|
||||
sidebarLink.activeRegExp
|
||||
)
|
||||
? 'border-indigo-600 from-indigo-700 to-purple-700'
|
||||
: 'border-indigo-500 from-indigo-600 to-purple-600'
|
||||
}`}
|
||||
>
|
||||
{pendingRequestsCount}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
{sidebarLink.messagesKey === 'issues' &&
|
||||
openIssuesCount > 0 &&
|
||||
hasPermission(Permission.MANAGE_ISSUES) && (
|
||||
<div className="ml-auto">
|
||||
<Badge
|
||||
className={`rounded-md bg-gradient-to-br ${
|
||||
router.pathname.match(
|
||||
sidebarLink.activeRegExp
|
||||
)
|
||||
? 'border-indigo-600 from-indigo-700 to-purple-700'
|
||||
: 'border-indigo-500 from-indigo-600 to-purple-600'
|
||||
}`}
|
||||
>
|
||||
{openIssuesCount}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useUser } from '@app/hooks/useUser';
|
||||
import { ArrowLeftIcon, Bars3BottomLeftIcon } from '@heroicons/react/24/solid';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
type LayoutProps = {
|
||||
children: React.ReactNode;
|
||||
@@ -23,8 +22,6 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
const router = useRouter();
|
||||
const { currentSettings } = useSettings();
|
||||
const { setLocale } = useLocale();
|
||||
const { data: requestResponse } = useSWR('/api/v1/request/count');
|
||||
const { data: issueResponse } = useSWR('/api/v1/issue/count');
|
||||
|
||||
useEffect(() => {
|
||||
if (setLocale && user) {
|
||||
@@ -58,17 +55,9 @@ const Layout = ({ children }: LayoutProps) => {
|
||||
<div className="absolute top-0 h-64 w-full bg-gradient-to-bl from-gray-800 to-gray-900">
|
||||
<div className="relative inset-0 h-full w-full bg-gradient-to-t from-gray-900 to-transparent" />
|
||||
</div>
|
||||
<Sidebar
|
||||
open={isSidebarOpen}
|
||||
setClosed={() => setSidebarOpen(false)}
|
||||
pendingRequestsCount={requestResponse?.pending}
|
||||
openIssuesCount={issueResponse?.open}
|
||||
/>
|
||||
<Sidebar open={isSidebarOpen} setClosed={() => setSidebarOpen(false)} />
|
||||
<div className="sm:hidden">
|
||||
<MobileMenu
|
||||
pendingRequestsCount={requestResponse?.pending}
|
||||
openIssuesCount={issueResponse?.open}
|
||||
/>
|
||||
<MobileMenu />
|
||||
</div>
|
||||
|
||||
<div className="relative mb-16 flex w-0 min-w-0 flex-1 flex-col lg:ml-64">
|
||||
|
||||
@@ -20,7 +20,6 @@ import axios from 'axios';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { mutate } from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
seasons: '{seasonCount, plural, one {Season} other {Seasons}}',
|
||||
@@ -57,7 +56,6 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
|
||||
|
||||
if (onUpdate) {
|
||||
onUpdate();
|
||||
mutate('/api/v1/request/count');
|
||||
}
|
||||
setIsUpdating(false);
|
||||
};
|
||||
@@ -68,7 +66,6 @@ const RequestBlock = ({ request, onUpdate }: RequestBlockProps) => {
|
||||
|
||||
if (onUpdate) {
|
||||
onUpdate();
|
||||
mutate('/api/v1/request/count');
|
||||
}
|
||||
|
||||
setIsUpdating(false);
|
||||
|
||||
@@ -15,7 +15,6 @@ import type { MediaRequest } from '@server/entity/MediaRequest';
|
||||
import axios from 'axios';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { mutate } from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
viewrequest: 'View Request',
|
||||
@@ -98,7 +97,6 @@ const RequestButton = ({
|
||||
|
||||
if (response) {
|
||||
onUpdate();
|
||||
mutate('/api/v1/request/count');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -117,7 +115,6 @@ const RequestButton = ({
|
||||
);
|
||||
|
||||
onUpdate();
|
||||
mutate('/api/v1/request/count');
|
||||
};
|
||||
|
||||
const buttons: ButtonOption[] = [];
|
||||
|
||||
@@ -75,7 +75,6 @@ const RequestCardError = ({ requestData }: RequestCardErrorProps) => {
|
||||
await axios.delete(`/api/v1/media/${requestData?.media.id}`);
|
||||
mutate('/api/v1/media?filter=allavailable&take=20&sort=mediaAdded');
|
||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||
mutate('/api/v1/request/count');
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -253,14 +252,12 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
||||
|
||||
if (response) {
|
||||
revalidate();
|
||||
mutate('/api/v1/request/count');
|
||||
}
|
||||
};
|
||||
|
||||
const deleteRequest = async () => {
|
||||
await axios.delete(`/api/v1/request/${request.id}`);
|
||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||
mutate('/api/v1/request/count');
|
||||
};
|
||||
|
||||
const retryRequest = async () => {
|
||||
|
||||
@@ -25,7 +25,7 @@ import { useState } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { defineMessages, FormattedRelativeTime, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
seasons: '{seasonCount, plural, one {Season} other {Seasons}}',
|
||||
@@ -62,7 +62,6 @@ const RequestItemError = ({
|
||||
const deleteRequest = async () => {
|
||||
await axios.delete(`/api/v1/media/${requestData?.media.id}`);
|
||||
revalidateList();
|
||||
mutate('/api/v1/request/count');
|
||||
};
|
||||
|
||||
const { plexUrl, plexUrl4k } = useDeepLinks({
|
||||
@@ -312,7 +311,6 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||
|
||||
if (response) {
|
||||
revalidate();
|
||||
mutate('/api/v1/request/count');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -320,7 +318,6 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||
await axios.delete(`/api/v1/request/${request.id}`);
|
||||
|
||||
revalidateList();
|
||||
mutate('/api/v1/request/count');
|
||||
};
|
||||
|
||||
const retryRequest = async () => {
|
||||
|
||||
@@ -16,7 +16,7 @@ import axios from 'axios';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const messages = defineMessages({
|
||||
requestadmin: 'This request will be approved automatically.',
|
||||
@@ -211,7 +211,6 @@ const CollectionRequestModal = ({
|
||||
? MediaStatus.UNKNOWN
|
||||
: MediaStatus.PARTIALLY_AVAILABLE
|
||||
);
|
||||
mutate('/api/v1/request/count');
|
||||
}
|
||||
|
||||
addToast(
|
||||
@@ -231,16 +230,7 @@ const CollectionRequestModal = ({
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
}, [
|
||||
requestOverrides,
|
||||
data?.parts,
|
||||
data?.name,
|
||||
onComplete,
|
||||
addToast,
|
||||
intl,
|
||||
selectedParts,
|
||||
is4k,
|
||||
]);
|
||||
}, [requestOverrides, data, onComplete, addToast, intl, selectedParts, is4k]);
|
||||
|
||||
const hasAutoApprove = hasPermission(
|
||||
[
|
||||
|
||||
@@ -95,7 +95,6 @@ const MovieRequestModal = ({
|
||||
...overrideParams,
|
||||
});
|
||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||
mutate('/api/v1/request/count');
|
||||
|
||||
if (response.data) {
|
||||
if (onComplete) {
|
||||
@@ -130,16 +129,7 @@ const MovieRequestModal = ({
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
}, [
|
||||
requestOverrides,
|
||||
data?.id,
|
||||
data?.title,
|
||||
is4k,
|
||||
onComplete,
|
||||
addToast,
|
||||
intl,
|
||||
hasPermission,
|
||||
]);
|
||||
}, [data, onComplete, addToast, requestOverrides, hasPermission, intl, is4k]);
|
||||
|
||||
const cancelRequest = async () => {
|
||||
setIsUpdating(true);
|
||||
@@ -149,7 +139,6 @@ const MovieRequestModal = ({
|
||||
`/api/v1/request/${editRequest?.id}`
|
||||
);
|
||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||
mutate('/api/v1/request/count');
|
||||
|
||||
if (response.status === 204) {
|
||||
if (onComplete) {
|
||||
@@ -187,7 +176,6 @@ const MovieRequestModal = ({
|
||||
await axios.post(`/api/v1/request/${editRequest?.id}/approve`);
|
||||
}
|
||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||
mutate('/api/v1/request/count');
|
||||
|
||||
addToast(
|
||||
<span>
|
||||
|
||||
@@ -106,7 +106,6 @@ const TvRequestModal = ({
|
||||
|
||||
if (onUpdating) {
|
||||
onUpdating(true);
|
||||
mutate('/api/v1/request/count');
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -129,7 +128,6 @@ const TvRequestModal = ({
|
||||
await axios.delete(`/api/v1/request/${editRequest.id}`);
|
||||
}
|
||||
mutate('/api/v1/request?filter=all&take=10&sort=modified&skip=0');
|
||||
mutate('/api/v1/request/count');
|
||||
|
||||
addToast(
|
||||
<span>
|
||||
@@ -178,7 +176,6 @@ const TvRequestModal = ({
|
||||
|
||||
if (onUpdating) {
|
||||
onUpdating(true);
|
||||
mutate('/api/v1/request/count');
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user