Store/use parentAddress alongside derivation (#2526)
* Store/use parentAddress alongside derivation * Show actual parent relationships * Tooltip for address
This commit is contained in:
parent
6bd5c9d817
commit
c906794397
@ -5,10 +5,11 @@
|
||||
import { DeriveAccountInfo, DeriveBalancesAll } from '@polkadot/api-derive/types';
|
||||
import { ActionStatus } from '@polkadot/react-components/Status/types';
|
||||
import { RecoveryConfig } from '@polkadot/types/interfaces';
|
||||
import { SortedAccount } from './types';
|
||||
|
||||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { AddressInfo, AddressSmall, Badge, Button, ChainLock, CryptoType, Forget, Icon, IdentityIcon, Input, InputTags, LinkExternal, Menu, Popup, Tag } from '@polkadot/react-components';
|
||||
import { AddressInfo, AddressMini, AddressSmall, Badge, Button, ChainLock, CryptoType, Forget, Icon, IdentityIcon, Input, InputTags, LinkExternal, Menu, Popup, Tag } from '@polkadot/react-components';
|
||||
import { useApi, useCall, useToggle } from '@polkadot/react-hooks';
|
||||
import { Option } from '@polkadot/types';
|
||||
import keyring from '@polkadot/ui-keyring';
|
||||
@ -23,22 +24,31 @@ import RecoverAccount from './modals/RecoverAccount';
|
||||
import RecoverSetup from './modals/RecoverSetup';
|
||||
import Transfer from './modals/Transfer';
|
||||
|
||||
interface Props {
|
||||
address: string;
|
||||
interface Props extends SortedAccount {
|
||||
className?: string;
|
||||
filter: string;
|
||||
isFavorite: boolean;
|
||||
toggleFavorite: (address: string) => void;
|
||||
}
|
||||
|
||||
function Account ({ address, className, filter, isFavorite, toggleFavorite }: Props): React.ReactElement<Props> | null {
|
||||
function calcVisible (filter: string, name: string, tags: string[]): boolean {
|
||||
if (filter.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const _filter = filter.toLowerCase();
|
||||
|
||||
return tags.reduce((result: boolean, tag: string): boolean => {
|
||||
return result || tag.toLowerCase().includes(_filter);
|
||||
}, name.toLowerCase().includes(_filter));
|
||||
}
|
||||
|
||||
function Account ({ account: { address, meta }, className, filter, isFavorite, toggleFavorite }: Props): React.ReactElement<Props> | null {
|
||||
const { t } = useTranslation();
|
||||
const api = useApi();
|
||||
const info = useCall<DeriveAccountInfo>(api.api.derive.accounts.info as any, [address]);
|
||||
const balancesAll = useCall<DeriveBalancesAll>(api.api.derive.balances.all as any, [address]);
|
||||
const recoveryInfo = useCall<RecoveryConfig | null>(api.api.query.recovery?.recoverable, [address], {
|
||||
transform: (opt: Option<RecoveryConfig>): RecoveryConfig | null =>
|
||||
opt.unwrapOr(null)
|
||||
transform: (opt: Option<RecoveryConfig>) => opt.unwrapOr(null)
|
||||
});
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
const [accName, setAccName] = useState('');
|
||||
@ -88,23 +98,16 @@ function Account ({ address, className, filter, isFavorite, toggleFavorite }: Pr
|
||||
}, [address, _setTags]);
|
||||
|
||||
useEffect((): void => {
|
||||
if (filter.length === 0) {
|
||||
setIsVisible(true);
|
||||
} else {
|
||||
const _filter = filter.toLowerCase();
|
||||
|
||||
setIsVisible(
|
||||
tags.reduce((result: boolean, tag: string): boolean => {
|
||||
return result || tag.toLowerCase().includes(_filter);
|
||||
}, accName.toLowerCase().includes(_filter))
|
||||
);
|
||||
}
|
||||
setIsVisible(
|
||||
calcVisible(filter, accName, tags)
|
||||
);
|
||||
}, [accName, filter, tags]);
|
||||
|
||||
const _onFavorite = useCallback(
|
||||
(): void => toggleFavorite(address),
|
||||
[address, toggleFavorite]
|
||||
);
|
||||
|
||||
const _onGenesisChange = useCallback(
|
||||
(genesisHash: string | null): void => {
|
||||
const account = keyring.getPair(address);
|
||||
@ -115,6 +118,7 @@ function Account ({ address, className, filter, isFavorite, toggleFavorite }: Pr
|
||||
},
|
||||
[address]
|
||||
);
|
||||
|
||||
const _saveName = useCallback(
|
||||
(): void => {
|
||||
toggleEditName();
|
||||
@ -133,6 +137,7 @@ function Account ({ address, className, filter, isFavorite, toggleFavorite }: Pr
|
||||
},
|
||||
[accName, address, toggleEditName]
|
||||
);
|
||||
|
||||
const _saveTags = useCallback(
|
||||
(): void => {
|
||||
toggleEditTags();
|
||||
@ -151,6 +156,7 @@ function Account ({ address, className, filter, isFavorite, toggleFavorite }: Pr
|
||||
},
|
||||
[address, tags, toggleEditTags]
|
||||
);
|
||||
|
||||
const _onForget = useCallback(
|
||||
(): void => {
|
||||
if (!address) {
|
||||
@ -306,6 +312,11 @@ function Account ({ address, className, filter, isFavorite, toggleFavorite }: Pr
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className='address'>
|
||||
{meta.parentAddress && (
|
||||
<AddressMini value={meta.parentAddress} />
|
||||
)}
|
||||
</td>
|
||||
<td className='number'>
|
||||
<CryptoType accountId={address} />
|
||||
</td>
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
import { KeyringAddress } from '@polkadot/ui-keyring/types';
|
||||
import { ComponentProps as Props } from '../types';
|
||||
import { SortedAccount } from './types';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
@ -19,8 +20,6 @@ import QrModal from './modals/Qr';
|
||||
import Account from './Account';
|
||||
import Banner from './Banner';
|
||||
|
||||
type SortedAccount = { address: string; isFavorite: boolean };
|
||||
|
||||
const STORE_FAVS = 'accounts:favorites';
|
||||
|
||||
// query the ledger for the address, adding it to the keyring
|
||||
@ -36,6 +35,53 @@ async function queryLedger (): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function expandList (mapped: SortedAccount[], entry: SortedAccount): SortedAccount[] {
|
||||
mapped.push(entry);
|
||||
|
||||
entry.children.forEach((entry): void => {
|
||||
expandList(mapped, entry);
|
||||
});
|
||||
|
||||
return mapped;
|
||||
}
|
||||
|
||||
function sortAccounts (addresses: string[], favorites: string[]): SortedAccount[] {
|
||||
const mapped = addresses
|
||||
.map((address) => keyring.getAccount(address))
|
||||
.filter((account): account is KeyringAddress => !!account)
|
||||
.map((account): SortedAccount => ({
|
||||
account,
|
||||
children: [],
|
||||
isFavorite: favorites.includes(account.address)
|
||||
}))
|
||||
.sort((a, b) => (a.account.meta.whenCreated || 0) - (b.account.meta.whenCreated || 0));
|
||||
|
||||
return mapped
|
||||
.filter((entry): boolean => {
|
||||
const parentAddress = entry.account.meta.parentAddress;
|
||||
|
||||
if (parentAddress) {
|
||||
const parent = mapped.find(({ account: { address } }) => address === parentAddress);
|
||||
|
||||
if (parent) {
|
||||
parent.children.push(entry);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.reduce(expandList, [])
|
||||
.sort((a, b): number =>
|
||||
a.isFavorite === b.isFavorite
|
||||
? 0
|
||||
: b.isFavorite
|
||||
? 1
|
||||
: -1
|
||||
);
|
||||
}
|
||||
|
||||
function Overview ({ className, onStatusChange }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { allAccounts } = useAccounts();
|
||||
@ -48,21 +94,7 @@ function Overview ({ className, onStatusChange }: Props): React.ReactElement<Pro
|
||||
|
||||
useEffect((): void => {
|
||||
setSortedAccounts(
|
||||
allAccounts
|
||||
.map((address): SortedAccount => ({ address, isFavorite: favorites.includes(address) }))
|
||||
.sort((a, b): number => {
|
||||
const accA = keyring.getAccount(a.address) as KeyringAddress;
|
||||
const accB = keyring.getAccount(b.address) as KeyringAddress;
|
||||
|
||||
return (accA.meta.whenCreated || 0) - (accB.meta.whenCreated || 0);
|
||||
})
|
||||
.sort((a, b): number =>
|
||||
a.isFavorite === b.isFavorite
|
||||
? 0
|
||||
: b.isFavorite
|
||||
? 1
|
||||
: -1
|
||||
)
|
||||
sortAccounts(allAccounts, favorites)
|
||||
);
|
||||
}, [allAccounts, favorites]);
|
||||
|
||||
@ -131,6 +163,7 @@ function Overview ({ className, onStatusChange }: Props): React.ReactElement<Pro
|
||||
}
|
||||
header={[
|
||||
[t('accounts'), 'start', 3],
|
||||
[t('parent'), 'address'],
|
||||
[t('type')],
|
||||
[t('tags'), 'start'],
|
||||
[t('transactions')],
|
||||
@ -138,12 +171,12 @@ function Overview ({ className, onStatusChange }: Props): React.ReactElement<Pro
|
||||
[undefined, undefined, 2]
|
||||
]}
|
||||
>
|
||||
{sortedAccounts.map(({ address, isFavorite }): React.ReactNode => (
|
||||
{sortedAccounts.map(({ account, isFavorite }): React.ReactNode => (
|
||||
<Account
|
||||
address={address}
|
||||
account={account}
|
||||
filter={filter}
|
||||
isFavorite={isFavorite}
|
||||
key={address}
|
||||
key={account.address}
|
||||
toggleFavorite={toggleFavorite}
|
||||
/>
|
||||
))}
|
||||
|
||||
@ -49,7 +49,7 @@ function createAccount (source: KeyringPair, suri: string, name: string, passwor
|
||||
try {
|
||||
const derived = source.derive(suri);
|
||||
|
||||
derived.setMeta({ ...derived.meta, name, tags: [] });
|
||||
derived.setMeta({ ...derived.meta, name, parentAddress: source.address, tags: [] });
|
||||
|
||||
const result = keyring.addPair(derived, password || '');
|
||||
const { address } = result.pair;
|
||||
|
||||
11
packages/page-accounts/src/Accounts/types.ts
Normal file
11
packages/page-accounts/src/Accounts/types.ts
Normal file
@ -0,0 +1,11 @@
|
||||
// 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 { KeyringAddress } from '@polkadot/ui-keyring/types';
|
||||
|
||||
export interface SortedAccount {
|
||||
account: KeyringAddress;
|
||||
children: SortedAccount[];
|
||||
isFavorite: boolean;
|
||||
}
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
import { DeriveAccountInfo } from '@polkadot/api-derive/types';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useApi, useCall } from '@polkadot/react-hooks';
|
||||
|
||||
@ -54,7 +54,10 @@ function AddressToggle ({ address, className, filter, isHidden, noToggle, onChan
|
||||
setIsFiltered(getIsFiltered(address, filter, info));
|
||||
}, [address, filter, info]);
|
||||
|
||||
const _onClick = (): void => onChange && onChange(!value);
|
||||
const _onClick = useCallback(
|
||||
(): void => onChange && onChange(!value),
|
||||
[onChange, value]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useApi } from '@polkadot/react-hooks';
|
||||
import chains from '@polkadot/ui-settings/defaults/chains';
|
||||
@ -39,17 +39,20 @@ function ChainLock ({ className, genesisHash, isDisabled, onChange, preventDefau
|
||||
setTied(calcLock(api.genesisHash.toHex(), genesisHash));
|
||||
}, [api, genesisHash]);
|
||||
|
||||
const _onChange = useCallback(
|
||||
(isTiedToChain: boolean) =>
|
||||
onChange(
|
||||
isTiedToChain
|
||||
? api.genesisHash.toHex()
|
||||
: null
|
||||
),
|
||||
[api, onChange]
|
||||
);
|
||||
|
||||
if (isDevelopment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const _onChange = (isTiedToChain: boolean): void =>
|
||||
onChange(
|
||||
isTiedToChain
|
||||
? api.genesisHash.toHex()
|
||||
: null
|
||||
);
|
||||
|
||||
return (
|
||||
<Toggle
|
||||
className={className}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
import { BareProps } from './types';
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||
|
||||
import StatusContext from './Status/Context';
|
||||
@ -24,14 +24,17 @@ function CopyButton ({ children, className, icon = 'copy', isAddress = false, va
|
||||
const { t } = useTranslation();
|
||||
const { queueAction } = useContext(StatusContext);
|
||||
|
||||
const _onCopy = (): void => {
|
||||
isAddress && queueAction && queueAction({
|
||||
account: value,
|
||||
action: t('clipboard'),
|
||||
message: t('address copied'),
|
||||
status: 'queued'
|
||||
});
|
||||
};
|
||||
const _onCopy = useCallback(
|
||||
(): void => {
|
||||
isAddress && queueAction && queueAction({
|
||||
account: value,
|
||||
action: t('clipboard'),
|
||||
message: t('address copied'),
|
||||
status: 'queued'
|
||||
});
|
||||
},
|
||||
[isAddress, queueAction, t, value]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
|
||||
@ -71,11 +71,17 @@ function BaseDropdown<Option> ({ allowAdd = false, className, defaultValue, drop
|
||||
_setStored(isUndefined(value) ? defaultValue : value);
|
||||
}, [_setStored, defaultValue, value]);
|
||||
|
||||
const _onAdd = (_: React.SyntheticEvent<HTMLElement>, { value }: DropdownProps): void =>
|
||||
onAdd && onAdd(value);
|
||||
const _onAdd = useCallback(
|
||||
(_: React.SyntheticEvent<HTMLElement>, { value }: DropdownProps): void =>
|
||||
onAdd && onAdd(value),
|
||||
[onAdd]
|
||||
);
|
||||
|
||||
const _onChange = (_: React.SyntheticEvent<HTMLElement> | null, { value }: DropdownProps): void =>
|
||||
_setStored(value);
|
||||
const _onChange = useCallback(
|
||||
(_: React.SyntheticEvent<HTMLElement> | null, { value }: DropdownProps): void =>
|
||||
_setStored(value),
|
||||
[_setStored]
|
||||
);
|
||||
|
||||
const dropdown = (
|
||||
<SUIDropdown
|
||||
|
||||
@ -4,9 +4,10 @@
|
||||
|
||||
import { BareProps } from './types';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import ReactMd from 'react-markdown';
|
||||
import styled from 'styled-components';
|
||||
import { useToggle } from '@polkadot/react-hooks';
|
||||
|
||||
import Icon from './Icon';
|
||||
|
||||
@ -15,23 +16,21 @@ interface Props extends BareProps {
|
||||
}
|
||||
|
||||
function HelpOverlay ({ className, md }: Props): React.ReactElement<Props> {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const _toggleVisible = (): void => setIsVisible(!isVisible);
|
||||
const [isVisible, toggleVisible] = useToggle();
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className='help-button'>
|
||||
<Icon
|
||||
name='help circle'
|
||||
onClick={_toggleVisible}
|
||||
onClick={toggleVisible}
|
||||
/>
|
||||
</div>
|
||||
<div className={`help-slideout ${isVisible ? 'open' : 'closed'}`}>
|
||||
<div className='help-button'>
|
||||
<Icon
|
||||
name='close'
|
||||
onClick={_toggleVisible}
|
||||
onClick={toggleVisible}
|
||||
/>
|
||||
</div>
|
||||
<ReactMd
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
import { DeriveAccountInfo } from '@polkadot/api-derive/types';
|
||||
import { IdentityProps as Props } from '@polkadot/react-identicon/types';
|
||||
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { getSystemIcon } from '@polkadot/apps-config/ui';
|
||||
import { useApi, useCall } from '@polkadot/react-hooks';
|
||||
import BaseIdentityIcon from '@polkadot/react-identicon';
|
||||
@ -13,13 +13,17 @@ import uiSettings from '@polkadot/ui-settings';
|
||||
import { ValidatorsContext } from '@polkadot/react-query';
|
||||
|
||||
import StatusContext from './Status/Context';
|
||||
import Tooltip from './Tooltip';
|
||||
import { useTranslation } from './translate';
|
||||
|
||||
export function getIdentityTheme (systemName: string): 'substrate' {
|
||||
return ((uiSettings.icon === 'default' && getSystemIcon(systemName)) || uiSettings.icon) as 'substrate';
|
||||
}
|
||||
|
||||
function IdentityIcon ({ className, onCopy, prefix, size, style, theme, value }: Props): React.ReactElement<Props> {
|
||||
let id = 0;
|
||||
|
||||
function IdentityIcon ({ className, onCopy, prefix, size, theme, value }: Props): React.ReactElement<Props> {
|
||||
const [trigger] = useState(`tooltip-identicon-${++id}`);
|
||||
const { api, isApiReady, systemName } = useApi();
|
||||
const { t } = useTranslation();
|
||||
const info = useCall<DeriveAccountInfo>(isApiReady && api.derive.accounts.info as any, [value]);
|
||||
@ -30,34 +34,50 @@ function IdentityIcon ({ className, onCopy, prefix, size, style, theme, value }:
|
||||
const thisTheme = theme || getIdentityTheme(systemName);
|
||||
|
||||
useEffect((): void => {
|
||||
value && setIsValidator(validators.includes(value.toString()));
|
||||
value && setIsValidator(
|
||||
validators.includes(value.toString())
|
||||
);
|
||||
}, [value, validators]);
|
||||
|
||||
useEffect((): void => {
|
||||
info && setAddress(info.accountId?.toString());
|
||||
info && setAddress(
|
||||
info.accountId?.toString()
|
||||
);
|
||||
}, [info]);
|
||||
|
||||
const _onCopy = (account: string): void => {
|
||||
onCopy && onCopy(account);
|
||||
queueAction && queueAction({
|
||||
account,
|
||||
action: t('clipboard'),
|
||||
message: t('address copied'),
|
||||
status: 'queued'
|
||||
});
|
||||
};
|
||||
const _onCopy = useCallback(
|
||||
(account: string): void => {
|
||||
onCopy && onCopy(account);
|
||||
queueAction && queueAction({
|
||||
account,
|
||||
action: t('clipboard'),
|
||||
message: t('address copied'),
|
||||
status: 'queued'
|
||||
});
|
||||
},
|
||||
[onCopy, queueAction, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseIdentityIcon
|
||||
<span
|
||||
className={className}
|
||||
isHighlight={isValidator}
|
||||
onCopy={_onCopy}
|
||||
prefix={prefix}
|
||||
size={size}
|
||||
style={style}
|
||||
theme={thisTheme as 'substrate'}
|
||||
value={address}
|
||||
/>
|
||||
data-for={trigger}
|
||||
data-tip={true}
|
||||
>
|
||||
<BaseIdentityIcon
|
||||
isHighlight={isValidator}
|
||||
onCopy={_onCopy}
|
||||
prefix={prefix}
|
||||
size={size}
|
||||
theme={thisTheme as 'substrate'}
|
||||
value={address}
|
||||
/>
|
||||
<Tooltip
|
||||
className='address'
|
||||
text={address}
|
||||
trigger={trigger}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
import { BareProps } from './types';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import SUIInput from 'semantic-ui-react/dist/commonjs/elements/Input/Input';
|
||||
import { isUndefined } from '@polkadot/util';
|
||||
|
||||
@ -93,35 +93,45 @@ let counter = 0;
|
||||
function Input ({ autoFocus = false, children, className, defaultValue, help, icon, inputClassName, isAction = false, isDisabled = false, isDisabledError = false, isEditable = false, isError = false, isFull = false, isHidden = false, isReadOnly = false, label, labelExtra, max, maxLength, min, name, onBlur, onChange, onEnter, onEscape, onKeyDown, onKeyUp, onPaste, placeholder, style, tabIndex, type = 'text', value, withEllipsis, withLabel }: Props): React.ReactElement<Props> {
|
||||
const [stateName] = useState(`in_${counter++}_at_${Date.now()}`);
|
||||
|
||||
const _onBlur = (): void => {
|
||||
onBlur && onBlur();
|
||||
};
|
||||
const _onBlur = useCallback(
|
||||
() => onBlur && onBlur(),
|
||||
[onBlur]
|
||||
);
|
||||
|
||||
const _onChange = ({ target }: React.SyntheticEvent<HTMLInputElement>): void => {
|
||||
onChange && onChange((target as HTMLInputElement).value);
|
||||
};
|
||||
const _onChange = useCallback(
|
||||
({ target }: React.SyntheticEvent<HTMLInputElement>): void =>
|
||||
onChange && onChange((target as HTMLInputElement).value),
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const _onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
|
||||
onKeyDown && onKeyDown(event);
|
||||
};
|
||||
const _onKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLInputElement>): void =>
|
||||
onKeyDown && onKeyDown(event),
|
||||
[onKeyDown]
|
||||
);
|
||||
|
||||
const _onKeyUp = (event: React.KeyboardEvent<HTMLInputElement>): void => {
|
||||
onKeyUp && onKeyUp(event);
|
||||
const _onKeyUp = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLInputElement>): void => {
|
||||
onKeyUp && onKeyUp(event);
|
||||
|
||||
if (onEnter && event.keyCode === 13) {
|
||||
(event.target as any).blur();
|
||||
onEnter();
|
||||
}
|
||||
if (onEnter && event.keyCode === 13) {
|
||||
(event.target as any).blur();
|
||||
onEnter();
|
||||
}
|
||||
|
||||
if (onEscape && event.keyCode === 27) {
|
||||
(event.target as any).blur();
|
||||
onEscape();
|
||||
}
|
||||
};
|
||||
if (onEscape && event.keyCode === 27) {
|
||||
(event.target as any).blur();
|
||||
onEscape();
|
||||
}
|
||||
},
|
||||
[onEnter, onEscape, onKeyUp]
|
||||
);
|
||||
|
||||
const _onPaste = (event: React.ClipboardEvent<HTMLInputElement>): void => {
|
||||
onPaste && onPaste(event);
|
||||
};
|
||||
const _onPaste = useCallback(
|
||||
(event: React.ClipboardEvent<HTMLInputElement>): void =>
|
||||
onPaste && onPaste(event),
|
||||
[onPaste]
|
||||
);
|
||||
|
||||
return (
|
||||
<Labelled
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
import { BareProps } from './types';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import addressToAddress from './util/toAddress';
|
||||
@ -25,13 +25,16 @@ interface Props extends BareProps {
|
||||
function InputAddressSimple ({ children, className, defaultValue, help, isFull, label, onChange, onEnter, onEscape }: Props): React.ReactElement<Props> {
|
||||
const [address, setAddress] = useState<string | null>(defaultValue || null);
|
||||
|
||||
const _onChange = (_address: string): void => {
|
||||
const address = addressToAddress(_address) || null;
|
||||
const _onChange = useCallback(
|
||||
(_address: string): void => {
|
||||
const address = addressToAddress(_address) || null;
|
||||
|
||||
setAddress(address);
|
||||
setAddress(address);
|
||||
|
||||
onChange && onChange(address);
|
||||
};
|
||||
onChange && onChange(address);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
import { BareProps } from './types';
|
||||
|
||||
import React, { useState, createRef } from 'react';
|
||||
import React, { useCallback, useState, createRef } from 'react';
|
||||
import Dropzone, { DropzoneRef } from 'react-dropzone';
|
||||
import styled from 'styled-components';
|
||||
import { formatNumber, isHex, u8aToString, hexToU8a } from '@polkadot/util';
|
||||
@ -58,29 +58,32 @@ function InputFile ({ accept, className, clearContent, convertHex, help, isDisab
|
||||
const dropRef = createRef<DropzoneRef>();
|
||||
const [file, setFile] = useState<FileState | undefined>();
|
||||
|
||||
const _onDrop = (files: File[]): void => {
|
||||
files.forEach((file): void => {
|
||||
const reader = new FileReader();
|
||||
const _onDrop = useCallback(
|
||||
(files: File[]): void => {
|
||||
files.forEach((file): void => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onabort = NOOP;
|
||||
reader.onerror = NOOP;
|
||||
reader.onabort = NOOP;
|
||||
reader.onerror = NOOP;
|
||||
|
||||
reader.onload = ({ target }: ProgressEvent<FileReader>): void => {
|
||||
if (target && target.result) {
|
||||
const name = file.name;
|
||||
const data = convertResult(target.result as ArrayBuffer, convertHex);
|
||||
reader.onload = ({ target }: ProgressEvent<FileReader>): void => {
|
||||
if (target && target.result) {
|
||||
const name = file.name;
|
||||
const data = convertResult(target.result as ArrayBuffer, convertHex);
|
||||
|
||||
onChange && onChange(data, name);
|
||||
dropRef && setFile({
|
||||
name,
|
||||
size: data.length
|
||||
});
|
||||
}
|
||||
};
|
||||
onChange && onChange(data, name);
|
||||
dropRef && setFile({
|
||||
name,
|
||||
size: data.length
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
},
|
||||
[convertHex, dropRef, onChange]
|
||||
);
|
||||
|
||||
const dropZone = (
|
||||
<Dropzone
|
||||
|
||||
@ -6,7 +6,7 @@ import { SiDef } from '@polkadot/util/types';
|
||||
import { BareProps, BitLength } from './types';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { formatBalance } from '@polkadot/util';
|
||||
|
||||
import { classes } from './util';
|
||||
@ -137,30 +137,6 @@ function inputToBn (input: string, si: SiDef | null, props: Props): [BN, boolean
|
||||
];
|
||||
}
|
||||
|
||||
// function bnToInput (bn: BN, si: SiDef | null): string {
|
||||
// const [siPower] = getSiPowers(si);
|
||||
//
|
||||
// const base = TEN.pow(siPower);
|
||||
// const div = bn.div(base);
|
||||
// const mod = bn.mod(base);
|
||||
//
|
||||
// return `${
|
||||
// div.gt(ZERO) ? div.toString() : '0'
|
||||
// }${
|
||||
// mod.gt(ZERO)
|
||||
// ? ((): string => {
|
||||
// const padding = Math.max(
|
||||
// mod.toString().length,
|
||||
// base.toString().length - div.toString().length,
|
||||
// bn.toString().length - div.toString().length
|
||||
// );
|
||||
//
|
||||
// return `.${mod.toString(10, padding).replace(/0*$/, '')}`;
|
||||
// })()
|
||||
// : ''
|
||||
// }`;
|
||||
// }
|
||||
|
||||
function getValuesFromString (value: string, si: SiDef | null, props: Props): [string, BN, boolean] {
|
||||
const [valueBn, isValid] = inputToBn(value, si, props);
|
||||
|
||||
@ -220,48 +196,55 @@ function InputNumber (props: Props): React.ReactElement<Props> {
|
||||
onChange && onChange(valueBn);
|
||||
}, [onChange, valueBn]);
|
||||
|
||||
const _onChange = (input: string): void => {
|
||||
setValues(getValuesFromString(input, si, props));
|
||||
};
|
||||
const _onChange = useCallback(
|
||||
(input: string) => setValues(getValuesFromString(input, si, props)),
|
||||
[props, si]
|
||||
);
|
||||
|
||||
const _onKeyDown = (event: React.KeyboardEvent<Element>): void => {
|
||||
if (KEYS_PRE.includes(event.key)) {
|
||||
setIsPreKeyDown(true);
|
||||
const _onKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<Element>): void => {
|
||||
if (KEYS_PRE.includes(event.key)) {
|
||||
setIsPreKeyDown(true);
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key.length === 1 && !isPreKeyDown) {
|
||||
const { selectionEnd: j, selectionStart: i, value } = event.target as HTMLInputElement;
|
||||
const newValue = `${value.substring(0, i || 0)}${event.key}${value.substring(j || 0)}`;
|
||||
if (event.key.length === 1 && !isPreKeyDown) {
|
||||
const { selectionEnd: j, selectionStart: i, value } = event.target as HTMLInputElement;
|
||||
const newValue = `${value.substring(0, i || 0)}${event.key}${value.substring(j || 0)}`;
|
||||
|
||||
if (!getRegex(isDecimal || !!si).test(newValue)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
},
|
||||
[isDecimal, isPreKeyDown, si]
|
||||
);
|
||||
|
||||
const _onKeyUp = useCallback(
|
||||
(event: React.KeyboardEvent<Element>): void => {
|
||||
if (KEYS_PRE.includes(event.key)) {
|
||||
setIsPreKeyDown(false);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const _onPaste = useCallback(
|
||||
(event: React.ClipboardEvent<Element>): void => {
|
||||
const { value: newValue } = event.target as HTMLInputElement;
|
||||
|
||||
if (!getRegex(isDecimal || !!si).test(newValue)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
[isDecimal, si]
|
||||
);
|
||||
|
||||
const _onKeyUp = (event: React.KeyboardEvent<Element>): void => {
|
||||
if (KEYS_PRE.includes(event.key)) {
|
||||
setIsPreKeyDown(false);
|
||||
}
|
||||
};
|
||||
|
||||
const _onPaste = (event: React.ClipboardEvent<Element>): void => {
|
||||
const { value: newValue } = event.target as HTMLInputElement;
|
||||
|
||||
if (!getRegex(isDecimal || !!si).test(newValue)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const _onSelectSiUnit = (siUnit: string): void => {
|
||||
setSi(formatBalance.findSi(siUnit));
|
||||
};
|
||||
|
||||
// const _onClickMaxButton = (): void => {
|
||||
// !!maxValue && setValue(bnToInput(maxValue, si));
|
||||
// };
|
||||
const _onSelectSiUnit = useCallback(
|
||||
(siUnit: string) => setSi(formatBalance.findSi(siUnit)),
|
||||
[]
|
||||
);
|
||||
|
||||
const maxValueLength = getGlobalMaxValue(bitLength).toString().length - 1;
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { compactAddLength } from '@polkadot/util';
|
||||
import InputFile, { InputFileProps } from './InputFile';
|
||||
|
||||
@ -12,12 +12,15 @@ interface Props extends Omit<InputFileProps, 'accept'> {
|
||||
}
|
||||
|
||||
function InputWasm ({ isValidRef, onChange, ...props }: Props): React.ReactElement<Props> {
|
||||
const _onChange = (wasm: Uint8Array, name: string): void => {
|
||||
const isWasmValid = wasm.subarray(0, 4).toString() === '0,97,115,109'; // '\0asm'
|
||||
const _onChange = useCallback(
|
||||
(wasm: Uint8Array, name: string): void => {
|
||||
const isWasmValid = wasm.subarray(0, 4).toString() === '0,97,115,109'; // '\0asm'
|
||||
|
||||
isValidRef.current = isWasmValid;
|
||||
onChange(compactAddLength(wasm), name);
|
||||
};
|
||||
isValidRef.current = isWasmValid;
|
||||
onChange(compactAddLength(wasm), name);
|
||||
},
|
||||
[isValidRef, onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<InputFile
|
||||
|
||||
@ -2,9 +2,10 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { useToggle } from '@polkadot/react-hooks';
|
||||
|
||||
import Icon from './Icon';
|
||||
import { classes } from './util';
|
||||
@ -23,18 +24,19 @@ export interface InsetProps {
|
||||
|
||||
function Inset ({ children, className, header, href, isCollapsible, isError, isSuccess, withBottomMargin, withTopMargin }: InsetProps): React.ReactElement<InsetProps> | null {
|
||||
const history = useHistory();
|
||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||
const [isCollapsed, toggleCollapsed] = useToggle();
|
||||
|
||||
const _onClick = useCallback(
|
||||
(): void => {
|
||||
href && history.push(href);
|
||||
},
|
||||
[history, href]
|
||||
);
|
||||
|
||||
if (!children) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const _onClick = (): void => {
|
||||
href && history.push(href);
|
||||
};
|
||||
|
||||
const _toggleCollapsed = (): void => setIsCollapsed(!isCollapsed);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
@ -53,7 +55,7 @@ function Inset ({ children, className, header, href, isCollapsible, isError, isS
|
||||
{isCollapsible && (
|
||||
<div
|
||||
className='header'
|
||||
onClick={_toggleCollapsed}
|
||||
onClick={toggleCollapsed}
|
||||
>
|
||||
<h3>{header}</h3>
|
||||
<Icon
|
||||
|
||||
@ -6,6 +6,7 @@ import { BareProps } from './types';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useToggle } from '@polkadot/react-hooks';
|
||||
|
||||
import Icon from './Icon';
|
||||
import { classes } from './util';
|
||||
@ -15,10 +16,11 @@ interface Props extends BareProps {
|
||||
help: React.ReactNode;
|
||||
}
|
||||
|
||||
function LabelHelp ({ className, help, style }: Props): React.ReactElement<Props> {
|
||||
const [isTooltipOpen, setIsTooltipOpen] = useState(false);
|
||||
let id = 0;
|
||||
|
||||
const _toggleTooltip = (): void => setIsTooltipOpen(!isTooltipOpen);
|
||||
function LabelHelp ({ className, help, style }: Props): React.ReactElement<Props> {
|
||||
const [trigger] = useState(`label-help-${++id}`);
|
||||
const [isTooltipOpen, toggleTooltip] = useToggle();
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -26,16 +28,16 @@ function LabelHelp ({ className, help, style }: Props): React.ReactElement<Props
|
||||
style={style}
|
||||
>
|
||||
<Icon
|
||||
data-for='controlled-trigger'
|
||||
data-for={trigger}
|
||||
data-tip
|
||||
name='help circle'
|
||||
onMouseOut={_toggleTooltip}
|
||||
onMouseOver={_toggleTooltip}
|
||||
onMouseOut={toggleTooltip}
|
||||
onMouseOver={toggleTooltip}
|
||||
/>
|
||||
{isTooltipOpen && (
|
||||
<Tooltip
|
||||
text={help}
|
||||
trigger='controlled-trigger'
|
||||
trigger={trigger}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -4,8 +4,9 @@
|
||||
|
||||
import { BareProps } from './types';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { MAX_PASS_LEN } from '@polkadot/ui-keyring/defaults';
|
||||
import { useToggle } from '@polkadot/react-hooks';
|
||||
|
||||
import { classes } from './util';
|
||||
import Button from './Button';
|
||||
@ -31,9 +32,7 @@ interface Props extends BareProps {
|
||||
}
|
||||
|
||||
function Password ({ autoFocus, children, className, defaultValue, help, isDisabled, isError, isFull, label, labelExtra, name, onChange, onEnter, onEscape, style, tabIndex, value, withLabel }: Props): React.ReactElement<Props> {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const _toggleVisible = (): void => setIsVisible(!isVisible);
|
||||
const [isVisible, toggleVisible] = useToggle();
|
||||
|
||||
return (
|
||||
<Input
|
||||
@ -68,7 +67,7 @@ function Password ({ autoFocus, children, className, defaultValue, help, isDisab
|
||||
? 'hide'
|
||||
: 'unhide'
|
||||
}
|
||||
onClick={_toggleVisible}
|
||||
onClick={toggleVisible}
|
||||
/>
|
||||
{children}
|
||||
</Input>
|
||||
|
||||
@ -23,33 +23,27 @@ interface Props extends BareProps {
|
||||
}
|
||||
|
||||
function Progress ({ className, color = 'blue', percent, style, total, value }: Props): React.ReactElement<Props> | null {
|
||||
let calculated: number | undefined;
|
||||
const _total = bnToBn(total);
|
||||
const _value = bnToBn(value);
|
||||
|
||||
if (_total.gtn(0)) {
|
||||
calculated = 100.0 * _value.toNumber() / _total.toNumber();
|
||||
} else {
|
||||
calculated = isBn(percent) ? percent.toNumber() : percent;
|
||||
}
|
||||
const calculated = _total.gtn(0)
|
||||
? 100.0 * _value.toNumber() / _total.toNumber()
|
||||
: isBn(percent) ? percent.toNumber() : percent;
|
||||
|
||||
if (isUndefined(calculated) || calculated < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let rainbow: BaseColors;
|
||||
|
||||
if (color === 'auto' || color === 'autoReverse') {
|
||||
if (calculated > 66.6) {
|
||||
rainbow = color === 'auto' ? 'green' : 'red';
|
||||
} else if (calculated > 33.3) {
|
||||
rainbow = 'orange';
|
||||
} else {
|
||||
rainbow = color === 'auto' ? 'red' : 'green';
|
||||
}
|
||||
} else {
|
||||
rainbow = color;
|
||||
}
|
||||
const rainbow = (color === 'auto' || color === 'autoReverse')
|
||||
? (calculated > 66.6)
|
||||
? color === 'auto'
|
||||
? 'green'
|
||||
: 'red'
|
||||
: (calculated > 33.3)
|
||||
? 'orange'
|
||||
: color === 'auto'
|
||||
? 'red'
|
||||
: 'green'
|
||||
: color;
|
||||
|
||||
return (
|
||||
<SUIProgress
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
import { BareProps } from './types';
|
||||
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import SUICheckbox from 'semantic-ui-react/dist/commonjs/modules/Checkbox';
|
||||
import styled from 'styled-components';
|
||||
|
||||
@ -19,14 +19,17 @@ interface Props extends BareProps {
|
||||
}
|
||||
|
||||
function Toggle ({ asSwitch = true, className, defaultValue, isDisabled, label, onChange, preventDefault, value }: Props): React.ReactElement<Props> {
|
||||
const _onChange = (event: React.FormEvent<HTMLInputElement>, { checked }: any): void => {
|
||||
if (preventDefault) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
const _onChange = useCallback(
|
||||
(event: React.FormEvent<HTMLInputElement>, { checked }: any): void => {
|
||||
if (preventDefault) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
onChange && onChange(checked);
|
||||
};
|
||||
onChange && onChange(checked);
|
||||
},
|
||||
[onChange, preventDefault]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
|
||||
@ -52,13 +52,18 @@ function Tooltip ({ className, effect = 'solid', offset, place = 'top', text, tr
|
||||
offset={offset}
|
||||
place={place}
|
||||
>
|
||||
{text}
|
||||
{className?.includes('address') ? <div>{text}</div> : text}
|
||||
</ReactTooltip>,
|
||||
tooltipContainer
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(Tooltip)`
|
||||
&.address div {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
table {
|
||||
border: 0;
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
import { TxButtonProps as Props } from './types';
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import { SubmittableResult } from '@polkadot/api';
|
||||
import { useApi, useToggle } from '@polkadot/react-hooks';
|
||||
import { assert, isFunction, isUndefined } from '@polkadot/util';
|
||||
@ -20,53 +20,62 @@ function TxButton ({ accountId, className, extrinsic: propsExtrinsic, icon, isBa
|
||||
const [isSending, , setIsSending] = useToggle(false);
|
||||
const needsAccount = !isUnsigned && !accountId;
|
||||
|
||||
const _onFailed = (result: SubmittableResult | null): void => {
|
||||
setIsSending(false);
|
||||
const _onFailed = useCallback(
|
||||
(result: SubmittableResult | null): void => {
|
||||
setIsSending(false);
|
||||
|
||||
onFailed && onFailed(result);
|
||||
};
|
||||
onFailed && onFailed(result);
|
||||
},
|
||||
[onFailed, setIsSending]
|
||||
);
|
||||
|
||||
const _onSuccess = (result: SubmittableResult): void => {
|
||||
setIsSending(false);
|
||||
const _onSuccess = useCallback(
|
||||
(result: SubmittableResult): void => {
|
||||
setIsSending(false);
|
||||
|
||||
onSuccess && onSuccess(result);
|
||||
};
|
||||
onSuccess && onSuccess(result);
|
||||
},
|
||||
[onSuccess, setIsSending]
|
||||
);
|
||||
|
||||
const _onSend = (): void => {
|
||||
let extrinsic: any;
|
||||
const _onSend = useCallback(
|
||||
(): void => {
|
||||
let extrinsic: any;
|
||||
|
||||
if (propsExtrinsic) {
|
||||
extrinsic = propsExtrinsic;
|
||||
} else {
|
||||
const [section, method] = (tx || '').split('.');
|
||||
if (propsExtrinsic) {
|
||||
extrinsic = propsExtrinsic;
|
||||
} else {
|
||||
const [section, method] = (tx || '').split('.');
|
||||
|
||||
assert(api.tx[section] && api.tx[section][method], `Unable to find api.tx.${section}.${method}`);
|
||||
assert(api.tx[section] && api.tx[section][method], `Unable to find api.tx.${section}.${method}`);
|
||||
|
||||
extrinsic = api.tx[section][method](...(
|
||||
isFunction(params)
|
||||
? params()
|
||||
: (params || [])
|
||||
));
|
||||
}
|
||||
extrinsic = api.tx[section][method](...(
|
||||
isFunction(params)
|
||||
? params()
|
||||
: (params || [])
|
||||
));
|
||||
}
|
||||
|
||||
assert(extrinsic, 'Expected generated extrinsic passed to TxButton');
|
||||
assert(extrinsic, 'Expected generated extrinsic passed to TxButton');
|
||||
|
||||
if (withSpinner) {
|
||||
setIsSending(true);
|
||||
}
|
||||
if (withSpinner) {
|
||||
setIsSending(true);
|
||||
}
|
||||
|
||||
queueExtrinsic({
|
||||
accountId: accountId && accountId.toString(),
|
||||
extrinsic,
|
||||
isUnsigned,
|
||||
txFailedCb: withSpinner ? _onFailed : onFailed,
|
||||
txStartCb: onStart,
|
||||
txSuccessCb: withSpinner ? _onSuccess : onSuccess,
|
||||
txUpdateCb: onUpdate
|
||||
});
|
||||
queueExtrinsic({
|
||||
accountId: accountId && accountId.toString(),
|
||||
extrinsic,
|
||||
isUnsigned,
|
||||
txFailedCb: withSpinner ? _onFailed : onFailed,
|
||||
txStartCb: onStart,
|
||||
txSuccessCb: withSpinner ? _onSuccess : onSuccess,
|
||||
txUpdateCb: onUpdate
|
||||
});
|
||||
|
||||
onClick && onClick();
|
||||
};
|
||||
onClick && onClick();
|
||||
},
|
||||
[_onFailed, _onSuccess, accountId, api.tx, isUnsigned, onClick, onFailed, onStart, onSuccess, onUpdate, params, propsExtrinsic, queueExtrinsic, setIsSending, tx, withSpinner]
|
||||
);
|
||||
|
||||
if (onSendRef) {
|
||||
onSendRef.current = _onSend;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user