mirror of
https://github.com/Radarr/Radarr.git
synced 2024-11-20 01:42:35 +01:00
New: Improve status label and progress bar style for deleted movies
Closes #7127
This commit is contained in:
parent
fa1d6ad109
commit
9ad6b3a611
@ -285,6 +285,7 @@ class AddNewMovieSearchResult extends Component {
|
||||
{
|
||||
isExistingMovie && isSmallScreen &&
|
||||
<MovieStatusLabel
|
||||
status={status}
|
||||
hasMovieFiles={hasMovieFile}
|
||||
monitored={monitored}
|
||||
isAvailable={isAvailable}
|
||||
|
@ -3,10 +3,10 @@ import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails';
|
||||
import getStatusStyle from 'Calendar/getStatusStyle';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AgendaEvent.css';
|
||||
|
||||
@ -82,7 +82,7 @@ class AgendaEvent extends Component {
|
||||
startTime = moment(startTime);
|
||||
const downloading = !!(queueItem || grabbed);
|
||||
const isMonitored = monitored;
|
||||
const statusStyle = getStatusStyle(null, isMonitored, hasFile, isAvailable, 'style', downloading);
|
||||
const statusStyle = getStatusStyle(hasFile, downloading, isMonitored, isAvailable);
|
||||
const joinedGenres = genres.slice(0, 2).join(', ');
|
||||
const link = `/movie/${titleSlug}`;
|
||||
|
||||
|
@ -2,10 +2,10 @@ import classNames from 'classnames';
|
||||
import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import getStatusStyle from 'Calendar/getStatusStyle';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import CalendarEventQueueDetails from './CalendarEventQueueDetails';
|
||||
import styles from './CalendarEvent.css';
|
||||
@ -39,7 +39,7 @@ class CalendarEvent extends Component {
|
||||
|
||||
const isDownloading = !!(queueItem || grabbed);
|
||||
const isMonitored = monitored;
|
||||
const statusStyle = getStatusStyle(null, isMonitored, hasFile, isAvailable, 'style', isDownloading);
|
||||
const statusStyle = getStatusStyle(hasFile, isDownloading, isMonitored, isAvailable);
|
||||
const joinedGenres = genres.slice(0, 2).join(', ');
|
||||
const link = `/movie/${titleSlug}`;
|
||||
const eventType = [];
|
||||
|
25
frontend/src/Calendar/getStatusStyle.js
Normal file
25
frontend/src/Calendar/getStatusStyle.js
Normal file
@ -0,0 +1,25 @@
|
||||
function getStatusStyle(hasFile, downloading, isMonitored, isAvailable) {
|
||||
if (downloading) {
|
||||
return 'queue';
|
||||
}
|
||||
|
||||
if (hasFile && isMonitored) {
|
||||
return 'downloaded';
|
||||
}
|
||||
|
||||
if (hasFile && !isMonitored) {
|
||||
return 'unmonitored';
|
||||
}
|
||||
|
||||
if (isAvailable && isMonitored) {
|
||||
return 'missingMonitored';
|
||||
}
|
||||
|
||||
if (!isMonitored) {
|
||||
return 'missingUnmonitored';
|
||||
}
|
||||
|
||||
return 'continuing';
|
||||
}
|
||||
|
||||
export default getStatusStyle;
|
@ -2,7 +2,7 @@ import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
|
||||
import getProgressBarKind from 'Utilities/Movie/getProgressBarKind';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './CollectionMovieLabel.css';
|
||||
|
||||
@ -45,7 +45,7 @@ class CollectionMovieLabel extends Component {
|
||||
<div
|
||||
className={classNames(
|
||||
styles.movieStatus,
|
||||
styles[getStatusStyle(status, monitored, hasFile, isAvailable, 'kinds')]
|
||||
styles[getProgressBarKind(status, monitored, hasFile, isAvailable)]
|
||||
)}
|
||||
>
|
||||
{
|
||||
|
@ -8,7 +8,7 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.title {
|
||||
.name {
|
||||
margin-bottom: 2px;
|
||||
color: var(--helpTextColor);
|
||||
font-size: 10px;
|
||||
|
2
frontend/src/Components/InfoLabel.css.d.ts
vendored
2
frontend/src/Components/InfoLabel.css.d.ts
vendored
@ -4,9 +4,9 @@ interface CssExports {
|
||||
'label': string;
|
||||
'large': string;
|
||||
'medium': string;
|
||||
'name': string;
|
||||
'outline': string;
|
||||
'small': string;
|
||||
'title': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
@ -7,7 +7,7 @@ import styles from './InfoLabel.css';
|
||||
function InfoLabel(props) {
|
||||
const {
|
||||
className,
|
||||
title,
|
||||
name,
|
||||
kind,
|
||||
size,
|
||||
outline,
|
||||
@ -25,8 +25,8 @@ function InfoLabel(props) {
|
||||
)}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.title}>
|
||||
{title}
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
</div>
|
||||
<div>
|
||||
{children}
|
||||
@ -37,7 +37,7 @@ function InfoLabel(props) {
|
||||
|
||||
InfoLabel.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||
size: PropTypes.oneOf(sizes.all).isRequired,
|
||||
outline: PropTypes.bool.isRequired,
|
||||
|
@ -21,6 +21,10 @@
|
||||
background-color: var(--darkGray);
|
||||
}
|
||||
|
||||
&.inverse {
|
||||
background-color: var(--inverseLabelColor);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background-color: var(--primaryColor);
|
||||
}
|
||||
@ -61,10 +65,18 @@
|
||||
.frontTextContainer {
|
||||
z-index: 1;
|
||||
color: var(--progressBarFrontTextColor);
|
||||
|
||||
&.inverse {
|
||||
color: var(--inverseLabelTextColor);
|
||||
}
|
||||
}
|
||||
|
||||
.backTextContainer {
|
||||
color: var(--progressBarBackTextColor);
|
||||
|
||||
&.inverse {
|
||||
color: var(--inverseLabelTextColor);
|
||||
}
|
||||
}
|
||||
|
||||
.backTextContainer,
|
||||
|
1
frontend/src/Components/ProgressBar.css.d.ts
vendored
1
frontend/src/Components/ProgressBar.css.d.ts
vendored
@ -9,6 +9,7 @@ interface CssExports {
|
||||
'frontText': string;
|
||||
'frontTextContainer': string;
|
||||
'info': string;
|
||||
'inverse': string;
|
||||
'large': string;
|
||||
'medium': string;
|
||||
'primary': string;
|
||||
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { ColorImpairedConsumer } from 'App/ColorImpairedContext';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ProgressBar.css';
|
||||
|
||||
function ProgressBar(props) {
|
||||
@ -57,7 +58,7 @@ function ProgressBar(props) {
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
role="meter"
|
||||
aria-label={`Progress Bar at ${progress.toFixed(0)}%`}
|
||||
aria-label={translate('ProgressBarProgress', { progress: progress.toFixed(0) })}
|
||||
aria-valuenow={progress.toFixed(0)}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
|
@ -24,6 +24,7 @@ import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
||||
import MovieHistoryModal from 'Movie/History/MovieHistoryModal';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import MovieInteractiveSearchModalConnector from 'Movie/Search/MovieInteractiveSearchModalConnector';
|
||||
@ -246,6 +247,7 @@ class MovieDetails extends Component {
|
||||
genres,
|
||||
collection,
|
||||
overview,
|
||||
status,
|
||||
youTubeTrailerId,
|
||||
isAvailable,
|
||||
images,
|
||||
@ -283,6 +285,8 @@ class MovieDetails extends Component {
|
||||
titleWidth
|
||||
} = this.state;
|
||||
|
||||
const statusDetails = getMovieStatusDetails(status);
|
||||
|
||||
const fanartUrl = getFanartUrl(images);
|
||||
const marqueeWidth = isSmallScreen ? titleWidth : (titleWidth - 150);
|
||||
|
||||
@ -524,7 +528,7 @@ class MovieDetails extends Component {
|
||||
<div className={styles.detailsLabels}>
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
title={translate('Path')}
|
||||
name={translate('Path')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<span className={styles.path}>
|
||||
@ -534,12 +538,14 @@ class MovieDetails extends Component {
|
||||
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
title={translate('Status')}
|
||||
name={translate('Status')}
|
||||
title={statusDetails.message}
|
||||
kind={kinds.DELETE}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<span className={styles.statusName}>
|
||||
<MovieStatusLabel
|
||||
status={status}
|
||||
hasMovieFiles={hasMovieFiles}
|
||||
monitored={monitored}
|
||||
isAvailable={isAvailable}
|
||||
@ -550,7 +556,7 @@ class MovieDetails extends Component {
|
||||
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
title={translate('QualityProfile')}
|
||||
name={translate('QualityProfile')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<span className={styles.qualityProfileName}>
|
||||
@ -564,7 +570,7 @@ class MovieDetails extends Component {
|
||||
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
title={translate('Size')}
|
||||
name={translate('Size')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<span className={styles.sizeOnDisk}>
|
||||
@ -576,7 +582,7 @@ class MovieDetails extends Component {
|
||||
collection ?
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
title={translate('Collection')}
|
||||
name={translate('Collection')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<div className={styles.collection}>
|
||||
@ -592,7 +598,7 @@ class MovieDetails extends Component {
|
||||
originalLanguage?.name && !isSmallScreen ?
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
title={translate('OriginalLanguage')}
|
||||
name={translate('OriginalLanguage')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<span className={styles.originalLanguage}>
|
||||
@ -606,7 +612,7 @@ class MovieDetails extends Component {
|
||||
studio && !isSmallScreen ?
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
title={translate('Studio')}
|
||||
name={translate('Studio')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<span className={styles.studio}>
|
||||
@ -620,7 +626,7 @@ class MovieDetails extends Component {
|
||||
genres.length && !isSmallScreen ?
|
||||
<InfoLabel
|
||||
className={styles.detailsInfoLabel}
|
||||
title={translate('Genres')}
|
||||
name={translate('Genres')}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<span className={styles.genres}>
|
||||
|
@ -27,3 +27,9 @@
|
||||
padding-left: 2px;
|
||||
border-left: 4px solid var(--warningColor);
|
||||
}
|
||||
|
||||
.deleted {
|
||||
padding-left: 2px;
|
||||
border-left: 4px solid var(--inverseLabelColor);
|
||||
color: var(--dangerColor);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
interface CssExports {
|
||||
'availNotMonitored': string;
|
||||
'continuing': string;
|
||||
'deleted': string;
|
||||
'ended': string;
|
||||
'missingMonitored': string;
|
||||
'missingUnmonitored': string;
|
||||
|
@ -7,7 +7,7 @@ import firstCharToUpper from 'Utilities/String/firstCharToUpper';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './MovieStatusLabel.css';
|
||||
|
||||
function getMovieStatus(hasFile, isMonitored, isAvailable, queueItem = false) {
|
||||
function getMovieStatus(status, hasFile, isMonitored, isAvailable, queueItem = false) {
|
||||
if (queueItem) {
|
||||
const queueStatus = queueItem.status;
|
||||
const queueState = queueItem.trackedDownloadStatus;
|
||||
@ -26,6 +26,10 @@ function getMovieStatus(hasFile, isMonitored, isAvailable, queueItem = false) {
|
||||
return 'ended';
|
||||
}
|
||||
|
||||
if (status === 'deleted') {
|
||||
return 'deleted';
|
||||
}
|
||||
|
||||
if (isAvailable && !isMonitored && !hasFile) {
|
||||
return 'missingUnmonitored';
|
||||
}
|
||||
@ -39,6 +43,7 @@ function getMovieStatus(hasFile, isMonitored, isAvailable, queueItem = false) {
|
||||
|
||||
function MovieStatusLabel(props) {
|
||||
const {
|
||||
status,
|
||||
hasMovieFiles,
|
||||
monitored,
|
||||
isAvailable,
|
||||
@ -47,17 +52,15 @@ function MovieStatusLabel(props) {
|
||||
colorImpairedMode
|
||||
} = props;
|
||||
|
||||
let status = getMovieStatus(hasMovieFiles, monitored, isAvailable, queueItem);
|
||||
let statusClass = status;
|
||||
let movieStatus = getMovieStatus(status, hasMovieFiles, monitored, isAvailable, queueItem);
|
||||
let statusClass = movieStatus;
|
||||
|
||||
if (status === 'availNotMonitored' || status === 'ended') {
|
||||
status = 'downloaded';
|
||||
}
|
||||
if (status === 'missingMonitored' || status === 'missingUnmonitored') {
|
||||
status = 'missing';
|
||||
}
|
||||
if (status === 'continuing') {
|
||||
status = 'notAvailable';
|
||||
if (movieStatus === 'availNotMonitored' || movieStatus === 'ended') {
|
||||
movieStatus = 'downloaded';
|
||||
} else if (movieStatus === 'missingMonitored' || movieStatus === 'missingUnmonitored') {
|
||||
movieStatus = 'missing';
|
||||
} else if (movieStatus === 'continuing') {
|
||||
movieStatus = 'notAvailable';
|
||||
}
|
||||
|
||||
if (queueItem) {
|
||||
@ -83,6 +86,9 @@ function MovieStatusLabel(props) {
|
||||
case 'missingUnmonitored':
|
||||
kind = kinds.WARNING;
|
||||
break;
|
||||
case 'deleted':
|
||||
kind = kinds.INVERSE;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
@ -92,7 +98,7 @@ function MovieStatusLabel(props) {
|
||||
size={sizes.LARGE}
|
||||
colorImpairedMode={colorImpairedMode}
|
||||
>
|
||||
{translate(firstCharToUpper(status))}
|
||||
{translate(firstCharToUpper(movieStatus))}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
@ -101,12 +107,13 @@ function MovieStatusLabel(props) {
|
||||
<span
|
||||
className={styles[statusClass]}
|
||||
>
|
||||
{translate(firstCharToUpper(status))}
|
||||
{translate(firstCharToUpper(movieStatus))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
MovieStatusLabel.propTypes = {
|
||||
status: PropTypes.string.isRequired,
|
||||
hasMovieFiles: PropTypes.bool.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
isAvailable: PropTypes.bool.isRequired,
|
||||
@ -115,4 +122,9 @@ MovieStatusLabel.propTypes = {
|
||||
colorImpairedMode: PropTypes.bool
|
||||
};
|
||||
|
||||
MovieStatusLabel.defaultProps = {
|
||||
useLabel: false,
|
||||
colorImpairedMode: false
|
||||
};
|
||||
|
||||
export default MovieStatusLabel;
|
||||
|
@ -5,8 +5,9 @@ import { sizes } from 'Helpers/Props';
|
||||
import createMovieQueueItemsDetailsSelector, {
|
||||
MovieQueueDetails,
|
||||
} from 'Movie/Index/createMovieQueueDetailsSelector';
|
||||
import { MovieStatus } from 'Movie/Movie';
|
||||
import { MovieFile } from 'MovieFile/MovieFile';
|
||||
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
|
||||
import getProgressBarKind from 'Utilities/Movie/getProgressBarKind';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './MovieIndexProgressBar.css';
|
||||
|
||||
@ -14,7 +15,7 @@ interface MovieIndexProgressBarProps {
|
||||
movieId: number;
|
||||
movieFile: MovieFile;
|
||||
monitored: boolean;
|
||||
status: string;
|
||||
status: MovieStatus;
|
||||
hasFile: boolean;
|
||||
isAvailable: boolean;
|
||||
width: number;
|
||||
@ -44,20 +45,14 @@ function MovieIndexProgressBar(props: MovieIndexProgressBarProps) {
|
||||
const progress = 100;
|
||||
const queueStatusText =
|
||||
queueDetails.count > 0 ? translate('Downloading') : null;
|
||||
let movieStatus = status === 'released' && hasFile ? 'downloaded' : status;
|
||||
|
||||
if (movieStatus === 'deleted') {
|
||||
movieStatus = translate('Missing');
|
||||
|
||||
if (hasFile) {
|
||||
movieStatus = movieFile?.quality?.quality.name ?? translate('Downloaded');
|
||||
}
|
||||
} else if (hasFile) {
|
||||
let movieStatus = translate('NotAvailable');
|
||||
if (hasFile) {
|
||||
movieStatus = movieFile?.quality?.quality.name ?? translate('Downloaded');
|
||||
} else if (status === 'deleted') {
|
||||
movieStatus = translate('Deleted');
|
||||
} else if (isAvailable && !hasFile) {
|
||||
movieStatus = translate('Missing');
|
||||
} else {
|
||||
movieStatus = translate('NotAvailable');
|
||||
}
|
||||
|
||||
const attachedClassName = bottomRadius
|
||||
@ -70,12 +65,11 @@ function MovieIndexProgressBar(props: MovieIndexProgressBarProps) {
|
||||
className={styles.progressBar}
|
||||
containerClassName={containerClassName}
|
||||
progress={progress}
|
||||
kind={getStatusStyle(
|
||||
kind={getProgressBarKind(
|
||||
status,
|
||||
monitored,
|
||||
hasFile,
|
||||
isAvailable,
|
||||
'kinds',
|
||||
queueDetails.count > 0
|
||||
)}
|
||||
size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL}
|
||||
|
38
frontend/src/Utilities/Movie/getProgressBarKind.ts
Normal file
38
frontend/src/Utilities/Movie/getProgressBarKind.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { MovieStatus } from 'Movie/Movie';
|
||||
|
||||
function getProgressBarKind(
|
||||
status: MovieStatus,
|
||||
monitored: boolean,
|
||||
hasFile: boolean,
|
||||
isAvailable: boolean,
|
||||
isDownloading: boolean = false
|
||||
) {
|
||||
if (isDownloading) {
|
||||
return kinds.PURPLE;
|
||||
}
|
||||
|
||||
if (hasFile && monitored) {
|
||||
return kinds.SUCCESS;
|
||||
}
|
||||
|
||||
if (hasFile && !monitored) {
|
||||
return kinds.DEFAULT;
|
||||
}
|
||||
|
||||
if (status === 'deleted') {
|
||||
return kinds.INVERSE;
|
||||
}
|
||||
|
||||
if (isAvailable && monitored) {
|
||||
return kinds.DANGER;
|
||||
}
|
||||
|
||||
if (!monitored) {
|
||||
return kinds.WARNING;
|
||||
}
|
||||
|
||||
return kinds.PRIMARY;
|
||||
}
|
||||
|
||||
export default getProgressBarKind;
|
@ -1,27 +0,0 @@
|
||||
import { kinds } from 'Helpers/Props';
|
||||
|
||||
function getStatusStyle(status, monitored, hasFile, isAvailable, returnType, queue = false) {
|
||||
if (queue) {
|
||||
return returnType === 'kinds' ? kinds.QUEUE : 'queue';
|
||||
}
|
||||
|
||||
if (hasFile && monitored) {
|
||||
return returnType === 'kinds' ? kinds.SUCCESS : 'downloaded';
|
||||
}
|
||||
|
||||
if (hasFile && !monitored) {
|
||||
return returnType === 'kinds' ? kinds.DEFAULT : 'unmonitored';
|
||||
}
|
||||
|
||||
if (isAvailable && monitored) {
|
||||
return returnType === 'kinds' ? kinds.DANGER : 'missingMonitored';
|
||||
}
|
||||
|
||||
if (!monitored) {
|
||||
return returnType === 'kinds' ? kinds.WARNING : 'missingUnmonitored';
|
||||
}
|
||||
|
||||
return returnType === 'kinds' ? kinds.PRIMARY : 'continuing';
|
||||
}
|
||||
|
||||
export default getStatusStyle;
|
@ -1306,6 +1306,7 @@
|
||||
"Profiles": "Profiles",
|
||||
"ProfilesSettingsSummary": "Quality, Language, Delay and Release profiles",
|
||||
"Progress": "Progress",
|
||||
"ProgressBarProgress": "Progress Bar at {progress}%",
|
||||
"Proper": "Proper",
|
||||
"Protocol": "Protocol",
|
||||
"ProtocolHelpText": "Choose which protocol(s) to use and which one is preferred when choosing between otherwise equal releases",
|
||||
|
Loading…
Reference in New Issue
Block a user