Columar modals (currently starting in staking) (#2649)
This commit is contained in:
parent
152fb4e7b6
commit
b6f90bf4e4
@ -41,9 +41,14 @@ function autoPick (targets: SortedTargets): string[] {
|
||||
}
|
||||
|
||||
function initialPick (targets: SortedTargets): Selected {
|
||||
return targets.validators?.length
|
||||
? { isAutoSelect: true, selected: autoPick(targets) }
|
||||
: { isAutoSelect: false, selected: [] };
|
||||
const selected = targets.validators?.length
|
||||
? autoPick(targets)
|
||||
: [];
|
||||
|
||||
return {
|
||||
isAutoSelect: selected.length !== 0,
|
||||
selected
|
||||
};
|
||||
}
|
||||
|
||||
function Nominate ({ className, controllerId, next, nominating, onClose, stashId, targets, validators }: Props): React.ReactElement<Props> | null {
|
||||
@ -85,58 +90,71 @@ function Nominate ({ className, controllerId, next, nominating, onClose, stashId
|
||||
<Modal
|
||||
className={`staking--Nominating ${className}`}
|
||||
header={t('Nominate Validators')}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content className='ui--signer-Signer-Content'>
|
||||
<InputAddress
|
||||
defaultValue={controllerId}
|
||||
isDisabled
|
||||
label={t('controller account')}
|
||||
/>
|
||||
<InputAddress
|
||||
defaultValue={stashId}
|
||||
isDisabled
|
||||
label={t('stash account')}
|
||||
/>
|
||||
{isAutoSelect
|
||||
? (
|
||||
<>
|
||||
<Static
|
||||
label={t('auto-selected targets for nomination')}
|
||||
value={
|
||||
selected.map((validatorId) => (
|
||||
<AddressMini
|
||||
key={validatorId}
|
||||
value={validatorId}
|
||||
/>
|
||||
))
|
||||
}
|
||||
/>
|
||||
<article className='warning'>{t('The auto-selection is done on the current profitability of the validators taking your favorites into account. It is adjusted based on the commission and current range of backing for teh validator. The calculation may and will change over time, so it is rather a selection based on the current state of the network, not a predictor of future profitability.')}</article>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<InputAddressMulti
|
||||
available={available}
|
||||
availableLabel={t('candidate accounts')}
|
||||
defaultValue={nominating}
|
||||
help={t('Filter available candidates based on name, address or short account index.')}
|
||||
maxCount={MAX_NOMINATIONS}
|
||||
onChange={_setSelected}
|
||||
valueLabel={t('nominated accounts')}
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<InputAddress
|
||||
defaultValue={controllerId}
|
||||
isDisabled
|
||||
label={t('controller account')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<Toggle
|
||||
className='auto--toggle'
|
||||
isDisabled={!targets.validators?.length}
|
||||
label={
|
||||
isAutoSelect
|
||||
? t('Use an automatic selection of the currently most profitable validators')
|
||||
: t('Select targets manually (no auto-selection based on current profitability)')
|
||||
}
|
||||
onChange={_toggleAutoSelect}
|
||||
value={isAutoSelect}
|
||||
/>
|
||||
<InputAddress
|
||||
defaultValue={stashId}
|
||||
isDisabled
|
||||
label={t('stash account')}
|
||||
/>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
{isAutoSelect
|
||||
? (
|
||||
<>
|
||||
<Static
|
||||
label={t('auto-selected targets for nomination')}
|
||||
value={
|
||||
selected.map((validatorId) => (
|
||||
<AddressMini
|
||||
key={validatorId}
|
||||
value={validatorId}
|
||||
/>
|
||||
))
|
||||
}
|
||||
/>
|
||||
<article className='warning'>{t('The auto-selection is done on the current profitability of the validators taking your favorites into account. It is adjusted based on the commission and current range of backing for the validator. The calculation may and will change over time, so it is rather a selection based on the current state of the network, not a predictor of future profitability.')}</article>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<InputAddressMulti
|
||||
available={available}
|
||||
availableLabel={t('candidate accounts')}
|
||||
defaultValue={nominating}
|
||||
help={t('Filter available candidates based on name, address or short account index.')}
|
||||
maxCount={MAX_NOMINATIONS}
|
||||
onChange={_setSelected}
|
||||
valueLabel={t('nominated accounts')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<Toggle
|
||||
className='auto--toggle'
|
||||
isDisabled={!targets.validators?.length}
|
||||
label={
|
||||
isAutoSelect
|
||||
? t('Use an automatic selection of the currently most profitable validators')
|
||||
: t('Select targets manually (no auto-selection based on current profitability)')
|
||||
}
|
||||
onChange={_toggleAutoSelect}
|
||||
value={isAutoSelect}
|
||||
/>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{t('Nominators can be selected automatically based on the current on-chain conditions or supplied manually as selected from the list of all currently available validators. In both cases, your favorites appear for the selection.')}</p>
|
||||
<p>{t('Once transmitted the new selection will only take effect in 2 eras since the selection criteria for the next era was done at the end of the previous era. Until then, then nominations will show as inactive.')}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions onCancel={onClose}>
|
||||
<TxButton
|
||||
|
||||
@ -6,13 +6,15 @@ import { SubmittableExtrinsic } from '@polkadot/api/types';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Dropdown, InputAddress, InputBalance, Modal, TxButton } from '@polkadot/react-components';
|
||||
import { Button, Dropdown, InputAddress, InputBalance, Modal, TxButton, Static } from '@polkadot/react-components';
|
||||
import { useApi, useToggle } from '@polkadot/react-hooks';
|
||||
import { Available, BlockToTime } from '@polkadot/react-query';
|
||||
|
||||
import { useTranslation } from '../translate';
|
||||
import InputValidateAmount from './Account/InputValidateAmount';
|
||||
import InputValidationController from './Account/InputValidationController';
|
||||
import { rewardDestinationOptions } from './constants';
|
||||
import useBondBlocks from './useBondBlocks';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@ -30,6 +32,7 @@ function NewStake ({ className, isInElection }: Props): React.ReactElement<Props
|
||||
const [destination, setDestination] = useState(0);
|
||||
const [extrinsic, setExtrinsic] = useState<SubmittableExtrinsic<'promise'> | null>(null);
|
||||
const [stashId, setStashId] = useState<string | null>(null);
|
||||
const bondedBlocks = useBondBlocks();
|
||||
|
||||
useEffect((): void => {
|
||||
setExtrinsic(
|
||||
@ -57,47 +60,84 @@ function NewStake ({ className, isInElection }: Props): React.ReactElement<Props
|
||||
<Modal
|
||||
className='staking--Bonding'
|
||||
header={t('Bonding Preferences')}
|
||||
size='small'
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content className='ui--signer-Signer-Content'>
|
||||
<InputAddress
|
||||
label={t('stash account')}
|
||||
onChange={setStashId}
|
||||
type='account'
|
||||
value={stashId}
|
||||
/>
|
||||
<InputAddress
|
||||
help={t('The controller is the account that will be used to control any nominating or validating actions. Should not match another stash or controller.')}
|
||||
label={t('controller account')}
|
||||
onChange={setControllerId}
|
||||
type='account'
|
||||
value={controllerId}
|
||||
/>
|
||||
<InputValidationController
|
||||
accountId={stashId}
|
||||
controllerId={controllerId}
|
||||
onError={setControllerError}
|
||||
/>
|
||||
<InputBalance
|
||||
autoFocus
|
||||
help={t('The total amount of the stash balance that will be at stake in any forthcoming rounds (should be less than the total amount available)')}
|
||||
isError={!hasValue || !!amountError}
|
||||
label={t('value bonded')}
|
||||
onChange={setAmount}
|
||||
/>
|
||||
<InputValidateAmount
|
||||
accountId={stashId}
|
||||
onError={setAmountError}
|
||||
value={amount}
|
||||
/>
|
||||
<Dropdown
|
||||
defaultValue={0}
|
||||
help={t('The destination account for any payments as either a nominator or validator')}
|
||||
label={t('payment destination')}
|
||||
onChange={setDestination}
|
||||
options={rewardDestinationOptions}
|
||||
value={destination}
|
||||
/>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<InputAddress
|
||||
label={t('stash account')}
|
||||
onChange={setStashId}
|
||||
type='account'
|
||||
value={stashId}
|
||||
/>
|
||||
<InputAddress
|
||||
help={t('The controller is the account that will be used to control any nominating or validating actions. Should not match another stash or controller.')}
|
||||
label={t('controller account')}
|
||||
onChange={setControllerId}
|
||||
type='account'
|
||||
value={controllerId}
|
||||
/>
|
||||
<InputValidationController
|
||||
accountId={stashId}
|
||||
controllerId={controllerId}
|
||||
onError={setControllerError}
|
||||
/>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{t('Think of the stash as your cold wallet and the controller as your hot wallet. Funding operations are controlled by the stash, any other non-funding actions by the controller itself.')}</p>
|
||||
<p>{t('To ensure optimal fund security using the same stash/controller is strongly discouraged, but not forbidden.')}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<InputBalance
|
||||
autoFocus
|
||||
help={t('The total amount of the stash balance that will be at stake in any forthcoming rounds (should be less than the total amount available)')}
|
||||
isError={!hasValue || !!amountError}
|
||||
label={t('value bonded')}
|
||||
labelExtra={
|
||||
<Available
|
||||
label={<span className='label'>{t('available')}</span>}
|
||||
params={stashId}
|
||||
/>
|
||||
}
|
||||
onChange={setAmount}
|
||||
/>
|
||||
{bondedBlocks && (
|
||||
<Static
|
||||
help={t('The bonding duration for any staked funds. Needs to be unlocked and withdrawn to become available.')}
|
||||
label={t('on-chain bonding duration')}
|
||||
>
|
||||
<BlockToTime blocks={bondedBlocks} />
|
||||
</Static>
|
||||
)}
|
||||
<InputValidateAmount
|
||||
accountId={stashId}
|
||||
onError={setAmountError}
|
||||
value={amount}
|
||||
/>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{t('The amount placed at-stake should be no more that 95% of your available amount to protect against slashing events.')}</p>
|
||||
<p>{t('Once bonded, it wil need to be unlocked/withdrawn and will be locked for at least the bonding duration.')}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<Dropdown
|
||||
defaultValue={0}
|
||||
help={t('The destination account for any payments as either a nominator or validator')}
|
||||
label={t('payment destination')}
|
||||
onChange={setDestination}
|
||||
options={rewardDestinationOptions}
|
||||
value={destination}
|
||||
/>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{t('Rewards (once paid) can be deposited to either the stash or controller, with different effects.')}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions onCancel={toggleVisible}>
|
||||
<TxButton
|
||||
|
||||
25
packages/page-staking/src/Actions/useBondBlocks.ts
Normal file
25
packages/page-staking/src/Actions/useBondBlocks.ts
Normal file
@ -0,0 +1,25 @@
|
||||
// Copyright 2017-2020 @polkadot/app-staking authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { DeriveSessionProgress } from '@polkadot/api-derive/types';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useApi, useCall } from '@polkadot/react-hooks';
|
||||
|
||||
const ONE = new BN(1);
|
||||
|
||||
export default function useBondBlocks (): BN | undefined {
|
||||
const { api } = useApi();
|
||||
const progress = useCall<DeriveSessionProgress>(api.derive.session.progress, []);
|
||||
const [duration, setDuration] = useState<BN | undefined>();
|
||||
|
||||
useEffect((): void => {
|
||||
progress?.sessionLength.gt(ONE) && setDuration(
|
||||
progress.eraLength.mul(api.consts.staking.bondingDuration)
|
||||
);
|
||||
}, [api, progress]);
|
||||
|
||||
return duration;
|
||||
}
|
||||
@ -54,32 +54,47 @@ function Nominate ({ className, ownNominators, targets }: Props): React.ReactEle
|
||||
<Modal
|
||||
className={className}
|
||||
header={t('Nominate validators')}
|
||||
size='large'
|
||||
>
|
||||
<Modal.Content>
|
||||
<InputAddress
|
||||
filter={filter}
|
||||
help={t('Your stash account. The transaction will be sent from the associated controller.')}
|
||||
label={t('the stash account to nominate with')}
|
||||
onChange={_onChangeStash}
|
||||
value={ids?.stashId}
|
||||
/>
|
||||
<InputAddress
|
||||
isDisabled
|
||||
label={t('the associated controller')}
|
||||
onChange={_onChangeStash}
|
||||
value={ids?.controllerId}
|
||||
/>
|
||||
<Static
|
||||
label={t('selected validators')}
|
||||
value={
|
||||
targets.map((validatorId) => (
|
||||
<AddressMini
|
||||
key={validatorId}
|
||||
value={validatorId}
|
||||
/>
|
||||
))
|
||||
}
|
||||
/>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<InputAddress
|
||||
filter={filter}
|
||||
help={t('Your stash account. The transaction will be sent from the associated controller.')}
|
||||
label={t('the stash account to nominate with')}
|
||||
onChange={_onChangeStash}
|
||||
value={ids?.stashId}
|
||||
/>
|
||||
<InputAddress
|
||||
isDisabled
|
||||
label={t('the associated controller')}
|
||||
value={ids?.controllerId}
|
||||
/>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{t('One of your available nomination accounts, keyed by the stash. The transaction will be sent from the controller.')}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<Static
|
||||
label={t('selected validators')}
|
||||
value={
|
||||
targets.map((validatorId) => (
|
||||
<AddressMini
|
||||
key={validatorId}
|
||||
value={validatorId}
|
||||
/>
|
||||
))
|
||||
}
|
||||
/>
|
||||
</Modal.Column>
|
||||
<Modal.Column>
|
||||
<p>{t('The selected validators to nominate, either via the "currently best algorithm" or via a manual selection.')}</p>
|
||||
<p>{t('Once transmitted the new selection will only take effect in 2 eras since the selection criteria for the next era was done at the end of the previous era. Until then, then nominations will show as inactive.')}</p>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
</Modal.Content>
|
||||
<Modal.Actions onCancel={toggleOpen}>
|
||||
<TxButton
|
||||
|
||||
28
packages/react-components/src/Modal/Actions.tsx
Normal file
28
packages/react-components/src/Modal/Actions.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
// Copyright 2017-2020 @polkadot/react-components authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { ActionsProps } from './types';
|
||||
|
||||
import React from 'react';
|
||||
import SUIModal from 'semantic-ui-react/dist/commonjs/modules/Modal/Modal';
|
||||
|
||||
import Button from '../Button';
|
||||
import ButtonCancel from '../ButtonCancel';
|
||||
|
||||
function Actions ({ cancelLabel, children, className, onCancel, withOr = true }: ActionsProps): React.ReactElement<ActionsProps> {
|
||||
return (
|
||||
<SUIModal.Actions>
|
||||
<Button.Group className={className}>
|
||||
<ButtonCancel
|
||||
label={cancelLabel}
|
||||
onClick={onCancel}
|
||||
/>
|
||||
{withOr && <Button.Or />}
|
||||
{children}
|
||||
</Button.Group>
|
||||
</SUIModal.Actions>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Actions);
|
||||
41
packages/react-components/src/Modal/Column.tsx
Normal file
41
packages/react-components/src/Modal/Column.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
// Copyright 2017-2020 @polkadot/react-components authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { ColumnProps } from './types';
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
function Column ({ children, className }: ColumnProps): React.ReactElement<ColumnProps> {
|
||||
return (
|
||||
<div className={`ui--Modal-Column ${className}`}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(Column)`
|
||||
&:nth-child(1) {
|
||||
flex: 100%;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
display: none;
|
||||
flex: 0%;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 1024px) {
|
||||
&:nth-child(1),
|
||||
&:only-child {
|
||||
flex: 0 66.6%;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
flex: 0 33.3%;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
}
|
||||
`);
|
||||
19
packages/react-components/src/Modal/Columns.tsx
Normal file
19
packages/react-components/src/Modal/Columns.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
// Copyright 2017-2020 @polkadot/react-components authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { ColumnProps } from './types';
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
function Columns ({ children, className }: ColumnProps): React.ReactElement<ColumnProps> {
|
||||
return (
|
||||
<div className={`ui--Modal-Columns ${className}`}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(Columns)`
|
||||
display: flex;
|
||||
flex-layout: row;
|
||||
`);
|
||||
@ -2,31 +2,19 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { BareProps } from './types';
|
||||
import { ActionsProps, ColumnProps, ModalProps } from './types';
|
||||
|
||||
import React from 'react';
|
||||
import SUIModal from 'semantic-ui-react/dist/commonjs/modules/Modal/Modal';
|
||||
|
||||
import Button from './Button';
|
||||
import ButtonCancel from './ButtonCancel';
|
||||
import { classes } from './util';
|
||||
|
||||
interface ModalProps extends BareProps {
|
||||
children: React.ReactNode;
|
||||
header?: React.ReactNode;
|
||||
open?: boolean;
|
||||
[index: string]: any;
|
||||
}
|
||||
|
||||
interface ActionsProps extends BareProps {
|
||||
cancelLabel?: string;
|
||||
children: React.ReactNode;
|
||||
withOr?: boolean;
|
||||
onCancel: () => void;
|
||||
}
|
||||
import Actions from './Actions';
|
||||
import Column from './Column';
|
||||
import Columns from './Columns';
|
||||
|
||||
type ModalType = React.FC<ModalProps> & {
|
||||
Actions: React.FC<ActionsProps>;
|
||||
Column: React.FC<ColumnProps>;
|
||||
Columns: React.FC<ColumnProps>;
|
||||
Content: typeof SUIModal.Content;
|
||||
Header: typeof SUIModal.Header;
|
||||
Description: typeof SUIModal.Description;
|
||||
@ -38,7 +26,7 @@ function ModalBase (props: ModalProps): React.ReactElement<ModalProps> {
|
||||
return (
|
||||
<SUIModal
|
||||
{...props}
|
||||
className={classes('theme--default', 'ui--Modal', className)}
|
||||
className={`theme--default ui--Modal ${className}`}
|
||||
dimmer='inverted'
|
||||
header={undefined}
|
||||
open={open}
|
||||
@ -51,24 +39,11 @@ function ModalBase (props: ModalProps): React.ReactElement<ModalProps> {
|
||||
);
|
||||
}
|
||||
|
||||
function Actions ({ cancelLabel, children, className, onCancel, withOr = true }: ActionsProps): React.ReactElement<ActionsProps> {
|
||||
return (
|
||||
<SUIModal.Actions>
|
||||
<Button.Group className={className}>
|
||||
<ButtonCancel
|
||||
label={cancelLabel}
|
||||
onClick={onCancel}
|
||||
/>
|
||||
{withOr && <Button.Or />}
|
||||
{children}
|
||||
</Button.Group>
|
||||
</SUIModal.Actions>
|
||||
);
|
||||
}
|
||||
|
||||
const Modal = React.memo(ModalBase) as unknown as ModalType;
|
||||
|
||||
Modal.Actions = React.memo(Actions);
|
||||
Modal.Actions = Actions;
|
||||
Modal.Column = Column;
|
||||
Modal.Columns = Columns;
|
||||
Modal.Content = SUIModal.Content;
|
||||
Modal.Header = SUIModal.Header;
|
||||
Modal.Description = SUIModal.Description;
|
||||
26
packages/react-components/src/Modal/types.tsx
Normal file
26
packages/react-components/src/Modal/types.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright 2017-2020 @polkadot/react-components authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { BareProps } from '../types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export interface ColumnProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface ModalProps extends BareProps {
|
||||
children: React.ReactNode;
|
||||
header?: React.ReactNode;
|
||||
open?: boolean;
|
||||
[index: string]: any;
|
||||
}
|
||||
|
||||
export interface ActionsProps extends BareProps {
|
||||
cancelLabel?: string;
|
||||
children: React.ReactNode;
|
||||
withOr?: boolean;
|
||||
onCancel: () => void;
|
||||
}
|
||||
@ -146,7 +146,9 @@ export default createGlobalStyle<Props>`
|
||||
|
||||
&.error,
|
||||
&.warning {
|
||||
font-size: 0.9rem;
|
||||
margin-left: 2.25rem;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
&.nomargin {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user