Pārlūkot izejas kodu

AddressMenu popup when clicking on address name (#2563)

* AddressMenu

* use menu

* refactor

* draft

* cleanup

* Sidebar

* Sidebar

* improvements

* redesign

* bump

* Small adjustments

* Layout adjustments, useCallbacks (not perfect...)

* Remove _setName with extra effect, re-add badges (edit missing)

* Missing useCallback in Address

* no withSidebar on AddressToggle

* useAccountInfo CPU spikes (split effects, no useCallback deps)

* Alway have underline (even on address)

* secondary button color

* Add name callback

* Make identity appear with displayname

* Adjust judgement button position

* UI adjustments

Co-authored-by: Jaco Greeff <jacogr@gmail.com>
Keith Ingram 4 gadi atpakaļ
vecāks
revīzija
9703811f8b
34 mainītis faili ar 1397 papildinājumiem un 514 dzēšanām
  1. 6 3
      packages/apps/src/index.tsx
  2. 17 140
      packages/page-accounts/src/Accounts/Account.tsx
  3. 48 117
      packages/page-accounts/src/Contacts/Address.tsx
  4. 575 0
      packages/page-accounts/src/Sidebar/Sidebar.tsx
  5. 43 0
      packages/page-accounts/src/Sidebar/index.tsx
  6. 3 1
      packages/page-democracy/src/Overview/External.tsx
  7. 3 1
      packages/page-explorer/src/BlockHeader.tsx
  8. 5 1
      packages/page-explorer/src/BlockInfo/ByHash.tsx
  9. 18 78
      packages/page-parachains/src/ParachainInfo.tsx
  10. 38 74
      packages/react-components/src/AccountName.tsx
  11. 1 0
      packages/react-components/src/AccountNameJudgement.tsx
  12. 3 1
      packages/react-components/src/AddressMini.tsx
  13. 23 13
      packages/react-components/src/AddressSmall.tsx
  14. 1 0
      packages/react-components/src/AddressToggle.tsx
  15. 100 0
      packages/react-components/src/AvatarItem.tsx
  16. 5 1
      packages/react-components/src/Button/Button.tsx
  17. 19 0
      packages/react-components/src/Button/Content.tsx
  18. 2 0
      packages/react-components/src/Button/index.tsx
  19. 15 6
      packages/react-components/src/Button/types.ts
  20. 7 1
      packages/react-components/src/IconLink.tsx
  21. 2 1
      packages/react-components/src/IdentityIcon.tsx
  22. 4 4
      packages/react-components/src/Input.tsx
  23. 1 1
      packages/react-components/src/InputBalance.tsx
  24. 6 3
      packages/react-components/src/Popup.tsx
  25. 128 0
      packages/react-components/src/Transfer.tsx
  26. 3 0
      packages/react-components/src/index.tsx
  27. 5 5
      packages/react-components/src/styles/theme.ts
  28. 4 3
      packages/react-components/src/types.ts
  29. 1 0
      packages/react-hooks/src/index.ts
  30. 40 1
      packages/react-hooks/src/types.ts
  31. 206 0
      packages/react-hooks/src/useAccountInfo.ts
  32. 5 2
      packages/react-hooks/src/useAccounts.ts
  33. 5 2
      packages/react-hooks/src/useAddresses.ts
  34. 55 55
      yarn.lock

+ 6 - 3
packages/apps/src/index.tsx

@@ -11,6 +11,7 @@ import React, { Suspense } from 'react';
 import ReactDOM from 'react-dom';
 import { HashRouter } from 'react-router-dom';
 import { ThemeProvider } from 'styled-components';
+import AccountSidebar from '@polkadot/app-accounts/Sidebar';
 import { Api } from '@polkadot/react-api';
 import Queue from '@polkadot/react-components/Status/Queue';
 import { BlockAuthors, Events } from '@polkadot/react-query';
@@ -33,9 +34,11 @@ ReactDOM.render(
         <Api url={settings.apiUrl}>
           <BlockAuthors>
             <Events>
-              <HashRouter>
-                <Apps />
-              </HashRouter>
+              <AccountSidebar>
+                <HashRouter>
+                  <Apps />
+                </HashRouter>
+              </AccountSidebar>
             </Events>
           </BlockAuthors>
         </Api>

+ 17 - 140
packages/page-accounts/src/Accounts/Account.tsx

@@ -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 { DeriveAccountInfo, DeriveBalancesAll } from '@polkadot/api-derive/types';
+import { DeriveBalancesAll } from '@polkadot/api-derive/types';
 import { ActionStatus } from '@polkadot/react-components/Status/types';
 import { RecoveryConfig } from '@polkadot/types/interfaces';
 import { SortedAccount } from './types';
@@ -10,8 +10,8 @@ import { SortedAccount } from './types';
 import BN from 'bn.js';
 import React, { useCallback, useState, useEffect } from 'react';
 import styled from 'styled-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 { AddressInfo, AddressMini, AddressSmall, Badge, Button, ChainLock, CryptoType, Forget, Icon, IdentityIcon, LinkExternal, Menu, Popup, Tag } from '@polkadot/react-components';
+import { useAccountInfo, useApi, useCall, useToggle } from '@polkadot/react-hooks';
 import { Option } from '@polkadot/types';
 import keyring from '@polkadot/ui-keyring';
 import { formatBalance, formatNumber } from '@polkadot/util';
@@ -47,18 +47,12 @@ function calcVisible (filter: string, name: string, tags: string[]): boolean {
 function Account ({ account: { address, meta }, className, filter, isFavorite, setBalance, toggleFavorite }: Props): React.ReactElement<Props> | null {
   const { t } = useTranslation();
   const api = useApi();
-  const info = useCall<DeriveAccountInfo>(api.api.derive.accounts.info, [address]);
   const balancesAll = useCall<DeriveBalancesAll>(api.api.derive.balances.all, [address]);
   const recoveryInfo = useCall<RecoveryConfig | null>(api.api.query.recovery?.recoverable, [address], {
     transform: (opt: Option<RecoveryConfig>) => opt.unwrapOr(null)
   });
-  const [tags, setTags] = useState<string[]>([]);
-  const [accName, setAccName] = useState('');
-  const [genesisHash, setGenesisHash] = useState<string | null>(null);
-  const [{ isDevelopment, isEditable, isExternal }, setFlags] = useState({ isDevelopment: false, isEditable: false, isExternal: false });
+  const { genesisHash, isDevelopment, isEditable, isExternal, name: accName, onSaveGenesisHash, tags } = useAccountInfo(address);
   const [isVisible, setIsVisible] = useState(true);
-  const [isEditingName, toggleEditName] = useToggle();
-  const [isEditingTags, toggleEditTags] = useToggle();
   const [isBackupOpen, toggleBackup] = useToggle();
   const [isDeriveOpen, toggleDerive] = useToggle();
   const [isForgetOpen, toggleForget] = useToggle();
@@ -69,40 +63,10 @@ function Account ({ account: { address, meta }, className, filter, isFavorite, s
   const [isSettingsOpen, toggleSettings] = useToggle();
   const [isTransferOpen, toggleTransfer] = useToggle();
 
-  const _setTags = useCallback(
-    (tags: string[]): void => setTags(tags.sort()),
-    []
-  );
-
   useEffect((): void => {
     balancesAll && setBalance(address, balancesAll.freeBalance);
   }, [address, balancesAll, setBalance]);
 
-  useEffect((): void => {
-    const { identity, nickname } = info || {};
-
-    if (api.api.query.identity && api.api.query.identity.identityOf) {
-      if (identity?.display) {
-        setAccName(identity.display);
-      }
-    } else if (nickname) {
-      setAccName(nickname);
-    }
-  }, [api, info]);
-
-  useEffect((): void => {
-    const account = keyring.getAccount(address);
-
-    setGenesisHash(account?.meta.genesisHash || null);
-    setFlags({
-      isDevelopment: account?.meta.isTesting || false,
-      isEditable: (account && !(account.meta.isInjected || account.meta.isHardware)) || false,
-      isExternal: account?.meta.isExternal || false
-    });
-    _setTags(account?.meta.tags || []);
-    setAccName(account?.meta.name || '');
-  }, [address, _setTags]);
-
   useEffect((): void => {
     setIsVisible(
       calcVisible(filter, accName, tags)
@@ -114,55 +78,6 @@ function Account ({ account: { address, meta }, className, filter, isFavorite, s
     [address, toggleFavorite]
   );
 
-  const _onGenesisChange = useCallback(
-    (genesisHash: string | null): void => {
-      const account = keyring.getPair(address);
-
-      account && keyring.saveAccountMeta(account, { ...account.meta, genesisHash });
-
-      setGenesisHash(genesisHash);
-    },
-    [address]
-  );
-
-  const _saveName = useCallback(
-    (): void => {
-      toggleEditName();
-
-      const meta = { name: accName, whenEdited: Date.now() };
-
-      if (address) {
-        try {
-          const currentKeyring = keyring.getPair(address);
-
-          currentKeyring && keyring.saveAccountMeta(currentKeyring, meta);
-        } catch (error) {
-          keyring.saveAddress(address, meta);
-        }
-      }
-    },
-    [accName, address, toggleEditName]
-  );
-
-  const _saveTags = useCallback(
-    (): void => {
-      toggleEditTags();
-
-      const meta = { tags, whenEdited: Date.now() };
-
-      if (address) {
-        try {
-          const currentKeyring = keyring.getPair(address);
-
-          currentKeyring && keyring.saveAccountMeta(currentKeyring, meta);
-        } catch (error) {
-          keyring.saveAddress(address, meta);
-        }
-      }
-    },
-    [address, tags, toggleEditTags]
-  );
-
   const _onForget = useCallback(
     (): void => {
       if (!address) {
@@ -240,26 +155,7 @@ function Account ({ account: { address, meta }, className, filter, isFavorite, s
         )}
       </td>
       <td className='address'>
-        <AddressSmall
-          onClickName={toggleEditName}
-          overrideName={
-            isEditingName
-              ? (
-                <Input
-                  autoFocus
-                  className='name--input'
-                  defaultValue={accName}
-                  onBlur={_saveName}
-                  onChange={setAccName}
-                  onEnter={_saveName}
-                  withLabel={false}
-                />
-              )
-              : undefined
-          }
-          toggle={isEditingName}
-          value={address}
-        />
+        <AddressSmall value={address} />
         {isBackupOpen && (
           <Backup
             address={address}
@@ -327,36 +223,17 @@ function Account ({ account: { address, meta }, className, filter, isFavorite, s
         <CryptoType accountId={address} />
       </td>
       <td className='all'>
-        {isEditingTags
-          ? (
-            <InputTags
-              defaultValue={tags}
-              onBlur={_saveTags}
-              onChange={_setTags}
-              onClose={_saveTags}
-              openOnFocus
-              searchInput={{ autoFocus: true }}
-              value={tags}
-              withLabel={false}
-            />
-          )
-          : (
-            <div
-              className='tags--toggle'
-              onClick={toggleEditTags}
-            >
-              {tags.length
-                ? tags.map((tag): React.ReactNode => (
-                  <Tag
-                    key={tag}
-                    label={tag}
-                  />
-                ))
-                : <label>{t('no tags')}</label>
-              }
-            </div>
-          )
-        }
+        <div className='tags--toggle'>
+          {tags.length
+            ? tags.map((tag): React.ReactNode => (
+              <Tag
+                key={tag}
+                label={tag}
+              />
+            ))
+            : <label>{t('no tags')}</label>
+          }
+        </div>
       </td>
       <td className='number'>
         {balancesAll && formatNumber(balancesAll.accountNonce)}
@@ -443,7 +320,7 @@ function Account ({ account: { address, meta }, className, filter, isFavorite, s
                 <ChainLock
                   className='accounts--network-toggle'
                   genesisHash={genesisHash}
-                  onChange={_onGenesisChange}
+                  onChange={onSaveGenesisHash}
                   preventDefault
                 />
               </>

+ 48 - 117
packages/page-accounts/src/Contacts/Address.tsx

@@ -8,12 +8,11 @@ import { ActionStatus } from '@polkadot/react-components/Status/types';
 
 import React, { useCallback, useEffect, useState } from 'react';
 import styled from 'styled-components';
-import { AddressSmall, AddressInfo, Button, ChainLock, Icon, InputTags, Input, LinkExternal, Forget, Menu, Popup, Tag } from '@polkadot/react-components';
-import { useApi, useCall, useToggle } from '@polkadot/react-hooks';
+import { AddressSmall, AddressInfo, Button, ChainLock, Icon, LinkExternal, Forget, Menu, Popup, Tag, Transfer } from '@polkadot/react-components';
+import { useApi, useCall } from '@polkadot/react-hooks';
 import keyring from '@polkadot/ui-keyring';
 import { formatNumber } from '@polkadot/util';
 
-import Transfer from '../Accounts/modals/Transfer';
 import { useTranslation } from '../translate';
 
 interface Props {
@@ -37,11 +36,9 @@ function Address ({ address, className, filter, isFavorite, toggleFavorite }: Pr
   const [accName, setAccName] = useState('');
   const [current, setCurrent] = useState<KeyringAddress | null>(null);
   const [genesisHash, setGenesisHash] = useState<string | null>(null);
-  const [isEditingName, toggleEditName] = useToggle();
-  const [isEditingTags, toggleEditTags] = useToggle();
-  const [isForgetOpen, toggleForget] = useToggle();
-  const [isSettingPopupOpen, toggleTransfer] = useToggle();
-  const [isTransferOpen, toggleSettingPopup] = useToggle();
+  const [isForgetOpen, setIsForgetOpen] = useState(false);
+  const [isSettingPopupOpen, setIsSettingPopupOpen] = useState(false);
+  const [isTransferOpen, setIsTransferOpen] = useState(false);
   const [isVisible, setIsVisible] = useState(true);
 
   const _setTags = useCallback(
@@ -90,27 +87,6 @@ function Address ({ address, className, filter, isFavorite, toggleFavorite }: Pr
     }
   }, [accName, filter, tags]);
 
-  const _onForget = useCallback(
-    (): void => {
-      if (address) {
-        const status: Partial<ActionStatus> = {
-          account: address,
-          action: 'forget'
-        };
-
-        try {
-          keyring.forgetAddress(address);
-          status.status = 'success';
-          status.message = t('address forgotten');
-        } catch (error) {
-          status.status = 'error';
-          status.message = error.message;
-        }
-      }
-    },
-    [address, t]
-  );
-
   const _onGenesisChange = useCallback(
     (genesisHash: string | null): void => {
       setGenesisHash(genesisHash);
@@ -129,42 +105,40 @@ function Address ({ address, className, filter, isFavorite, toggleFavorite }: Pr
     [address, toggleFavorite]
   );
 
-  const _saveName = useCallback(
-    (): void => {
-      toggleEditName();
-
-      const meta = { name: accName, whenEdited: Date.now() };
+  const _toggleForget = useCallback(
+    (): void => setIsForgetOpen(!isForgetOpen),
+    [isForgetOpen]
+  );
 
-      if (address) {
-        try {
-          const currentKeyring = keyring.getPair(address);
+  const _toggleSettingPopup = useCallback(
+    (): void => setIsSettingPopupOpen(!isSettingPopupOpen),
+    [isSettingPopupOpen]
+  );
 
-          currentKeyring && keyring.saveAccountMeta(currentKeyring, meta);
-        } catch (error) {
-          keyring.saveAddress(address, meta);
-        }
-      }
-    },
-    [accName, address, toggleEditName]
+  const _toggleTransfer = useCallback(
+    (): void => setIsTransferOpen(!isTransferOpen),
+    [isTransferOpen]
   );
 
-  const _saveTags = useCallback(
+  const _onForget = useCallback(
     (): void => {
-      toggleEditTags();
-
-      const meta = { tags, whenEdited: Date.now() };
-
       if (address) {
-        try {
-          const currentKeyring = keyring.getPair(address);
+        const status: Partial<ActionStatus> = {
+          account: address,
+          action: 'forget'
+        };
 
-          currentKeyring && keyring.saveAccountMeta(currentKeyring, meta);
+        try {
+          keyring.forgetAddress(address);
+          status.status = 'success';
+          status.message = t('address forgotten');
         } catch (error) {
-          keyring.saveAddress(address, meta);
+          status.status = 'error';
+          status.message = error.message;
         }
       }
     },
-    [address, tags, toggleEditTags]
+    [address, t]
   );
 
   if (!isVisible) {
@@ -181,26 +155,7 @@ function Address ({ address, className, filter, isFavorite, toggleFavorite }: Pr
         />
       </td>
       <td className='address'>
-        <AddressSmall
-          onClickName={toggleEditName}
-          overrideName={
-            isEditingName
-              ? (
-                <Input
-                  autoFocus
-                  className='name--input'
-                  defaultValue={accName}
-                  onBlur={_saveName}
-                  onChange={setAccName}
-                  onEnter={_saveName}
-                  withLabel={false}
-                />
-              )
-              : undefined
-          }
-          toggle={isEditingName}
-          value={address}
-        />
+        <AddressSmall value={address} />
         {address && current && (
           <>
             {isForgetOpen && (
@@ -208,14 +163,14 @@ function Address ({ address, className, filter, isFavorite, toggleFavorite }: Pr
                 address={current.address}
                 key='modal-forget-account'
                 mode='address'
-                onClose={toggleForget}
+                onClose={_toggleForget}
                 onForget={_onForget}
               />
             )}
             {isTransferOpen && (
               <Transfer
                 key='modal-transfer'
-                onClose={toggleTransfer}
+                onClose={_toggleTransfer}
                 recipientId={address}
               />
             )}
@@ -223,36 +178,17 @@ function Address ({ address, className, filter, isFavorite, toggleFavorite }: Pr
         )}
       </td>
       <td className='all'>
-        {isEditingTags
-          ? (
-            <InputTags
-              defaultValue={tags}
-              onBlur={_saveTags}
-              onChange={_setTags}
-              onClose={_saveTags}
-              openOnFocus
-              searchInput={{ autoFocus: true }}
-              value={tags}
-              withLabel={false}
-            />
-          )
-          : (
-            <div
-              className='tags--toggle'
-              onClick={toggleEditTags}
-            >
-              {tags.length
-                ? tags.map((tag): React.ReactNode => (
-                  <Tag
-                    key={tag}
-                    label={tag}
-                  />
-                ))
-                : <label>{t('no tags')}</label>
-              }
-            </div>
-          )
-        }
+        <div className='tags'>
+          {tags.length
+            ? tags.map((tag): React.ReactNode => (
+              <Tag
+                key={tag}
+                label={tag}
+              />
+            ))
+            : <label>{t('no tags')}</label>
+          }
+        </div>
       </td>
       <td className='number'>
         {balancesAll && formatNumber(balancesAll.accountNonce)}
@@ -270,30 +206,30 @@ function Address ({ address, className, filter, isFavorite, toggleFavorite }: Pr
           icon='paper plane'
           key='deposit'
           label={t('deposit')}
-          onClick={toggleTransfer}
+          onClick={_toggleTransfer}
           size='small'
           tooltip={t('Send funds to this address')}
         />
         <Popup
           className='theme--default'
           isOpen={isSettingPopupOpen}
-          onClose={toggleSettingPopup}
+          onClose={_toggleSettingPopup}
           trigger={
             <Button
               icon='setting'
-              onClick={toggleSettingPopup}
+              onClick={_toggleSettingPopup}
               size='small'
             />
           }
         >
           <Menu
-            onClick={toggleSettingPopup}
+            onClick={_toggleSettingPopup}
             text
             vertical
           >
             <Menu.Item
               disabled={!isEditable}
-              onClick={toggleForget}
+              onClick={_toggleForget}
             >
               {t('Forget this address')}
             </Menu.Item>
@@ -329,14 +265,9 @@ export default React.memo(styled(Address)`
     text-align: right;
   }
 
-  .tags--toggle {
-    cursor: pointer;
+  .tags {
     width: 100%;
     min-height: 1.5rem;
-
-    label {
-      cursor: pointer;
-    }
   }
 
   .name--input {

+ 575 - 0
packages/page-accounts/src/Sidebar/Sidebar.tsx

@@ -0,0 +1,575 @@
+// Copyright 2017-2020 @polkadot/app-accounts 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 '@polkadot/react-components/types';
+import { AccountId, Address } from '@polkadot/types/interfaces';
+
+import React, { useCallback } from 'react';
+import { Label } from 'semantic-ui-react';
+import styled from 'styled-components';
+import { useAccountInfo, useApi, useRegistrars, useToggle } from '@polkadot/react-hooks';
+
+import { classes } from '@polkadot/react-components/util';
+import { colorLink } from '@polkadot/react-components/styles/theme';
+import { AccountNameJudgement, AccountName, AddressMini, AvatarItem, Button, Icon, IconLink, IdentityIcon, Input, InputTags, LinkExternal, Transfer } from '@polkadot/react-components';
+
+import { useTranslation } from '../translate';
+
+interface Props extends BareProps {
+  address: AccountId | Address | string | Uint8Array;
+  onClose: () => void;
+  onUpdateName: () => void;
+}
+
+function Sidebar ({ address, className, onClose, onUpdateName, style }: Props): React.ReactElement<Props> {
+  const { t } = useTranslation();
+  const { api } = useApi();
+  const { isRegistrar, registrars } = useRegistrars();
+  const { identity, isCouncil, isDevelopment, isEditable, isEditingName, isEditingTags, isExternal, isInContacts, isOwned, isSociety, isSudo, isTechCommittee, name, onForgetAddress, onSaveName, onSaveTags, setName, setTags, tags, toggleIsEditingName, toggleIsEditingTags } = useAccountInfo(address);
+  const [isHoveringButton, toggleIsHoveringButton] = useToggle();
+  const [isTransferOpen, toggleIsTransferOpen] = useToggle();
+  const [isJudgementOpen, toggleIsJudgementOpen] = useToggle();
+
+  const _onForgetAddress = useCallback(
+    (): void => {
+      onForgetAddress();
+      onUpdateName && onUpdateName();
+    },
+    [onForgetAddress, onUpdateName]
+  );
+
+  const _onUpdateName = useCallback(
+    (): void => {
+      onSaveName();
+      onUpdateName && onUpdateName();
+    },
+    [onSaveName, onUpdateName]
+  );
+
+  const useIdentity = !!api.query.identity?.identityOf;
+  const hasFlags = isDevelopment || isExternal || isSociety || isCouncil || isTechCommittee || isSudo;
+
+  return (
+    <>
+      <div
+        className={classes('ui--AddressMenu', className)}
+        style={style}
+      >
+        <Button
+          className='ui--AddressMenu-close'
+          icon='close'
+          isBasic
+          isCircular
+          onClick={onClose}
+        />
+        <div className='ui--AddressMenu-header'>
+          <IdentityIcon
+            size={80}
+            value={address.toString()}
+          />
+          <div className='ui--AddressMenu-addr'>
+            {address.toString()}
+          </div>
+          <AccountName
+            onClick={(isEditable && !isEditingName) ? toggleIsEditingName : undefined}
+            override={
+              isEditingName
+                ? (
+                  <Input
+                    autoFocus
+                    className='name--input'
+                    defaultValue={name}
+                    onBlur={(isInContacts || isOwned) ? _onUpdateName : undefined}
+                    onChange={setName}
+                    withLabel={false}
+                  />
+                )
+                : isEditable
+                  ? (name.toUpperCase() || t('<unknown>'))
+                  : undefined
+            }
+            value={address}
+            withSidebar={false}
+          >
+            {(!isEditingName && isEditable) && (
+              <Icon
+                className='inline-icon'
+                name='edit'
+              />
+            )}
+          </AccountName>
+          <div className='ui--AddressMenu-tags'>
+            {isEditingTags
+              ? (
+                <InputTags
+                  defaultValue={tags}
+                  onBlur={onSaveTags}
+                  onChange={setTags}
+                  onClose={onSaveTags}
+                  openOnFocus
+                  searchInput={{ autoFocus: true }}
+                  value={tags}
+                  withLabel={false}
+                />
+              )
+              : (
+                <div
+                  className='tags--toggle'
+                  onClick={toggleIsEditingTags}
+                >
+                  {tags.length
+                    ? tags.map((tag): React.ReactNode => (
+                      <Label
+                        color='grey'
+                        key={tag}
+                        size='tiny'
+                      >
+                        {tag}
+                      </Label>
+                    ))
+                    : <label>{t('no tags')}</label>
+                  }
+                </div>
+              )
+            }
+            {(!isEditingTags && (isInContacts || isOwned)) && (
+              <Icon
+                className='inline-icon'
+                name='edit'
+                onClick={toggleIsEditingTags}
+              />
+            )}
+          </div>
+          {hasFlags && (
+            <div className='ui--AddressMenu-flags'>
+              {isExternal && (
+                <Label
+                  color='grey'
+                  size='tiny'
+                  tag
+                >
+                  {t('Injected')}
+                </Label>
+              )}
+              {isDevelopment && (
+                <Label
+                  color='grey'
+                  size='tiny'
+                  tag
+                >
+                  {t('Test account')}
+                </Label>
+              )}
+              {isCouncil && (
+                <Label
+                  color='blue'
+                  size='tiny'
+                  tag
+                >
+                  {t('Council')}
+                </Label>
+              )}
+              {isSociety && (
+                <Label
+                  color='green'
+                  size='tiny'
+                  tag
+                >
+                  {t('Society')}
+                </Label>
+              )}
+              {isTechCommittee && (
+                <Label
+                  color='orange'
+                  size='tiny'
+                  tag
+                >
+                  {t('Technical committee')}
+                </Label>
+              )}
+              {isSudo && (
+                <Label
+                  color='pink'
+                  size='tiny'
+                  tag
+                >
+                  {t('Sudo key')}
+                </Label>
+              )}
+            </div>
+          )}
+          <div className='ui-AddressMenu--button'>
+            <Button.Group>
+              <Button
+                icon='send'
+                label={t('Deposit')}
+                onClick={toggleIsTransferOpen}
+              />
+              {isOwned && (
+                <Button
+                  className='basic'
+                  icon='check'
+                  isPrimary
+                  label={t('Owned')}
+                  onMouseEnter={toggleIsHoveringButton}
+                  onMouseLeave={toggleIsHoveringButton}
+                  size='tiny'
+                />
+              )}
+              {!isOwned && !isInContacts && (
+                <Button
+                  icon='add'
+                  isPositive
+                  label={t('Save')}
+                  onClick={_onUpdateName}
+                  onMouseEnter={toggleIsHoveringButton}
+                  onMouseLeave={toggleIsHoveringButton}
+                  size='tiny'
+                />
+              )}
+              {!isOwned && isInContacts && (
+                <Button
+                  className={`ui--AddressMenu-button icon ${isHoveringButton ? '' : 'basic'}`}
+                  isAnimated
+                  isNegative={isHoveringButton}
+                  isPositive={!isHoveringButton}
+                  onClick={_onForgetAddress}
+                  onMouseEnter={toggleIsHoveringButton}
+                  onMouseLeave={toggleIsHoveringButton}
+                  size='tiny'
+                >
+                  <Button.Content visible>
+                    <Icon name='check' />
+                    {' '}
+                    {t('Saved')}
+                  </Button.Content>
+                  <Button.Content hidden>
+                    <Icon name='ban' />
+                    {' '}
+                    {t('Remove')}
+                  </Button.Content>
+                </Button>
+              )}
+            </Button.Group>
+          </div>
+        </div>
+        {useIdentity && identity?.isExistent && (
+          <div className='ui--AddressMenu-section ui--AddressMenu-identity'>
+            <div className='ui--AddressMenu-sectionHeader'>
+              <div>
+                <Icon name='address card' />
+                {' '}
+                {t('identity')}
+              </div>
+              <Label
+                color={
+                  identity.isGood
+                    ? 'green'
+                    : identity.isBad
+                      ? 'red'
+                      : 'yellow'
+                }
+                size='tiny'
+              >
+                <b>{identity.judgements.length}</b>
+                <Label.Detail>
+                  {
+                    identity.judgements.length
+                      ? (identity.isGood
+                        ? (identity.isKnownGood ? t('Known good') : t('Reasonable'))
+                        : (identity.isErroneous ? t('Erroneous') : t('Low quality'))
+                      )
+                      : t('No judgments')
+                  }
+                </Label.Detail>
+              </Label>
+            </div>
+            <div>
+              <AvatarItem
+                icon={
+                  // This won't work - images are IPFS hashes
+                  // identity.image
+                  //   ? <img src={identity.image} />
+                  //   : <i className='icon user ui--AddressMenu-identityIcon' />
+                  //
+                  <i className='icon user ui--AddressMenu-identityIcon' />
+                }
+                subtitle={identity.legal}
+                title={identity.display}
+              />
+              <div className='ui--AddressMenu-identityTable'>
+                {identity.parent && (
+                  <div className='tr parent'>
+                    <div className='th'>{t('parent')}</div>
+                    <div className='td'>
+                      <AddressMini
+                        className='parent'
+                        isPadded={false}
+                        value={identity.parent}
+                      />
+                    </div>
+                  </div>
+                )}
+                {identity.email && (
+                  <div className='tr'>
+                    <div className='th'>{t('email')}</div>
+                    <div className='td'>
+                      <a
+                        href={`mailto:${identity.email}`}
+                        rel='noopener noreferrer'
+                        target='_blank'
+                      >
+                        {identity.email}
+                      </a>
+                    </div>
+                  </div>
+                )}
+                {identity.web && (
+                  <div className='tr'>
+                    <div className='th'>{t('website')}</div>
+                    <div className='td'>
+                      <a
+                        href={identity.web.replace(/^(https?:\/\/)?/g, 'https://')}
+                        rel='noopener noreferrer'
+                        target='_blank'
+                      >
+                        {identity.web}
+                      </a>
+                    </div>
+                  </div>
+                )}
+                {identity.twitter && (
+                  <div className='tr'>
+                    <div className='th'>{t('twitter')}</div>
+                    <div className='td'>
+                      <a
+                        href={`https://twitter.com/${identity.twitter}`}
+                        rel='noopener noreferrer'
+                        target='_blank'
+                      >
+                        {identity.twitter}
+                      </a>
+                    </div>
+                  </div>
+                )}
+                {identity.riot && (
+                  <div className='tr'>
+                    <div className='th'>{t('riot')}</div>
+                    <div className='td'>
+                      {identity.riot}
+                    </div>
+                  </div>
+                )}
+              </div>
+            </div>
+          </div>
+        )}
+        {address && identity?.isExistent && isRegistrar && (
+          <div className='ui--AddressMenu-section'>
+            <div className='ui--AddressMenu-actions'>
+              <ul>
+                <li>
+                  <IconLink
+                    icon='address card'
+                    label={t('Add identity judgment')}
+                    onClick={toggleIsJudgementOpen}
+                  />
+                </li>
+              </ul>
+            </div>
+          </div>
+        )}
+        <div className='ui--AddressMenu-section'>
+          <LinkExternal
+            data={address}
+            type='address'
+          />
+        </div>
+      </div>
+      {isTransferOpen && (
+        <Transfer
+          key='modal-transfer'
+          onClose={toggleIsTransferOpen}
+          recipientId={address}
+        />
+      )}
+      {(!!address && isJudgementOpen && isRegistrar && useIdentity) && (
+        <AccountNameJudgement
+          address={address.toString()}
+          key='modal-judgement'
+          registrars={registrars}
+          toggleJudgement={toggleIsJudgementOpen}
+        />
+      )}
+    </>
+  );
+}
+
+export default React.memo(styled(Sidebar)`
+  bottom: 0;
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  max-width: 24rem;
+  background: #f5f5f5;
+  padding: 1rem;
+  box-shadow: -6px 0px 20px 0px rgba(0,0,0,0.2);
+  z-index: 999;
+
+  input {
+    width: auto !important;
+  }
+
+  .ui--AddressMenu-close {
+    position: absolute;
+    right: 0.5rem;
+    top: 0.5rem;
+    font-size: 1.2rem;
+    padding: 0.6rem !important;
+  }
+
+  .ui--AddressMenu-header {
+    align-items: center;
+    background: white;
+    border-bottom: 1px solid #e6e6e6;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    margin: -1rem -1rem 1rem -1rem;
+    padding: 1rem;
+
+    .ui.button {
+      transition: 0.5s all;
+
+      &.secondary {
+        background-color: #666;
+      }
+    }
+
+    .ui.button+.ui.button {
+      margin-left: 0.25rem !important;
+    }
+  }
+
+  .ui--AddressMenu-addr {
+    font-family: monospace;
+    margin: 0.5rem 0;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    width: 100%;
+  }
+
+  .ui--AddressMenu-section {
+    &:not(:last-child) {
+      margin-bottom: 1.4rem;
+    }
+
+    .ui--AddressMenu-sectionHeader {
+      display: inline-flex;
+      color: #aaa;
+      margin-bottom: 0.4rem;
+      width: 100%;
+
+      & > :first-child {
+        flex: 1;
+      }
+    }
+  }
+
+  .ui--AddressMenu-identity {
+    .ui--AddressMenu-identityTable {
+      font-size: 13px;
+      margin-top: 0.3rem;
+
+      .tr {
+        display: inline-flex;
+        align-items: center;
+        width: 100%;
+
+        .th {
+          font-weight: bold;
+          text-align: right;
+          flex-basis: 20%;
+        }
+
+        .td {
+          padding-left: 0.6rem;
+          flex: 1;
+        }
+      }
+    }
+
+    .parent {
+      padding: 0 !important;
+    }
+  }
+
+  .ui--AddressMenu-tags,
+  .ui--AddressMenu-flags {
+    margin-bottom: 0.75rem;
+  }
+
+  .ui--AddressMenu-flags {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: flex-end;
+
+    > * {
+      margin-bottom: 0.4rem;
+
+      &:not(:first-child) {
+        margin-left: 1rem;
+        margin-right: 0;
+      }
+    }
+  }
+
+  .ui--AddressMenu-identityIcon {
+    background: ${colorLink}66;
+  }
+
+  .ui--AddressMenu-actions {
+    ul {
+      list-style-type: none;
+      margin-block-start: 0;
+      margin-block-end: 0;
+      padding-inline-start: 1rem;
+
+      li {
+        margin: 0.2rem 0;
+      }
+    }
+  }
+
+  .tags--toggle {
+    display: inline-block;
+  }
+
+  .inline-icon {
+    cursor: pointer;
+    margin: 0 0 0 0.6rem;
+    color: rgba(200, 200, 200, 0.8);
+  }
+
+  &:hover {
+    .inline-icon {
+      color: ${colorLink}
+    }
+  }
+
+  .name--input {
+    .ui.input {
+      margin: 0 !important;
+
+      > input {
+        padding: 0 !important;
+        background: rgba(230, 230, 230, 0.8) !important;
+        border: 0 !important;
+        border-radius: 0 !important;
+        box-shadow: 0 3px 3px rgba(0,0,0,.2);
+      }
+    }
+  }
+`);

+ 43 - 0
packages/page-accounts/src/Sidebar/index.tsx

@@ -0,0 +1,43 @@
+// Copyright 2017-2020 @polkadot/app-accounts 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 { StringOrNull, VoidFn } from '@polkadot/react-components/types';
+
+import React, { useCallback, useState } from 'react';
+
+import Sidebar from './Sidebar';
+
+type ToggleContext = undefined | (([address, onUpdateName]: [StringOrNull, VoidFn | null]) => void);
+
+interface Props {
+  children: React.ReactNode;
+}
+
+const AccountSidebarToggle: React.Context<ToggleContext> = React.createContext<ToggleContext>(undefined);
+
+function AccountSidebar ({ children }: Props): React.ReactElement<Props> {
+  const [[address, onUpdateName], setAddress] = useState<[StringOrNull, VoidFn | null]>([null, null]);
+
+  const onClose = useCallback(
+    () => setAddress([null, null]),
+    []
+  );
+
+  return (
+    <AccountSidebarToggle.Provider value={setAddress}>
+      {children}
+      {address && (
+        <Sidebar
+          address={address}
+          onClose={onClose}
+          onUpdateName={onUpdateName}
+        />
+      )}
+    </AccountSidebarToggle.Provider>
+  );
+}
+
+export { AccountSidebarToggle };
+
+export default React.memo(AccountSidebar);

+ 3 - 1
packages/page-democracy/src/Overview/External.tsx

@@ -25,7 +25,9 @@ function External ({ className, value: { image, imageHash, threshold } }: Props)
         proposal={image?.proposal}
       />
       <td className='address'>
-        {image && <AddressMini value={image.proposer} />}
+        {image && (
+          <AddressMini value={image.proposer} />
+        )}
       </td>
       <td className='number'>
         {image && <FormatBalance value={image.balance} />}

+ 3 - 1
packages/page-explorer/src/BlockHeader.tsx

@@ -29,7 +29,9 @@ function BlockHeader ({ value }: Props): React.ReactElement<Props> | null {
       </td>
       <td className='all hash overflow'>{hashHex}</td>
       <td className='address'>
-        {value.author && <AddressMini value={value.author} />}
+        {value.author && (
+          <AddressMini value={value.author} />
+        )}
       </td>
     </tr>
   );

+ 5 - 1
packages/page-explorer/src/BlockInfo/ByHash.tsx

@@ -50,7 +50,11 @@ function BlockByHash ({ className, value }: Props): React.ReactElement<Props> {
       >
         {getBlock && !getBlock.isEmpty && getHeader && !getHeader.isEmpty && (
           <tr>
-            <td className='address'>{getHeader.author && <AddressMini value={getHeader.author} />}</td>
+            <td className='address'>
+              {getHeader.author && (
+                <AddressMini value={getHeader.author} />
+              )}
+            </td>
             <td className='hash overflow'>{getHeader.hash.toHex()}</td>
             <td className='hash overflow'><Link to={`/explorer/query/${parentHash}`}>{parentHash}</Link></td>
             <td className='hash overflow'>{getHeader.extrinsicsRoot.toHex()}</td>

+ 18 - 78
packages/page-parachains/src/ParachainInfo.tsx

@@ -6,6 +6,7 @@ import { DeriveParachainInfo } from '@polkadot/api-derive/types';
 
 import React from 'react';
 import styled from 'styled-components';
+import { AvatarItem } from '@polkadot/react-components';
 
 import { useTranslation } from './translate';
 import { parachainName, parachainOwner } from './util';
@@ -21,91 +22,30 @@ function ParachainInfo ({ children, className, info, isBig }: Props): React.Reac
   const { t } = useTranslation();
 
   return (
-    <div className={[className, isBig ? 'big' : ''].join(' ')}>
-      <div className='chain-icon'>
-        {
-          info?.icon
-            ? (
-              <img src={info.icon} />
-            )
-            : (
-              <i className='icon chain fitted' />
-            )
-        }
-      </div>
-      <div className='details'>
-        <div className='name'>
-          {parachainName(t, info)}
-        </div>
-        <div className='owner'>
-          {parachainOwner(t, info)}
-        </div>
-      </div>
+    <AvatarItem
+      className={className}
+      icon={
+        info?.icon
+          ? (
+            <img src={info.icon} />
+          )
+          : (
+            <i className='icon chain' />
+          )
+      }
+      isBig={isBig}
+      subtitle={parachainOwner(t, info)}
+      title={parachainName(t, info)}
+    >
       {children}
-    </div>
+    </AvatarItem>
   );
 }
 
 export default React.memo(styled(ParachainInfo)`
   & {
-    display: flex;
-    align-items: center;
-
-    .chain-icon {
-      width: 2.4rem;
-      height: 2.4rem;
+    .icon.chain {
       background: #e03997;
-      border-radius: 50%;
-      color: white;
-      margin-right: 0.5rem;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-
-      > * {
-        width:100%;
-      }
-
-      > i.icon {
-        height: auto !important;
-      }
-
-      > img {
-        height: 100%;
-      }
-    }
-
-    .details {
-      flex: 1;
-
-      .name {
-        font-weight: bold;
-        font-size: 1rem;
-      }
-
-      .owner {
-        color: rgba(100, 100, 100, 0.6);
-        font-size: 1rem;
-      }
-    }
-
-    &.big {
-      .chain-icon {
-        width: 3.4rem;
-        height: 3.4rem;
-        margin-right: 0.6rem;
-
-        > i.icon {
-          font-size: 1.6rem;
-        }
-      }
-
-      .details {
-        .name {
-          font-size: 1.4rem;
-          line-height: 1.4rem;
-        }
-      }
     }
   }
 `);

+ 38 - 74
packages/react-components/src/AccountName.tsx

@@ -6,16 +6,14 @@ import { DeriveAccountInfo, DeriveAccountRegistration } from '@polkadot/api-deri
 import { BareProps } from '@polkadot/react-api/types';
 import { AccountId, AccountIndex, Address } from '@polkadot/types/interfaces';
 
-import React, { useEffect, useState } from 'react';
+import React, { useCallback, useContext, useEffect, useState } from 'react';
 import styled from 'styled-components';
 import registry from '@polkadot/react-api/typeRegistry';
-import { useCall, useApi, useRegistrars, useToggle } from '@polkadot/react-hooks';
+import { AccountSidebarToggle } from '@polkadot/app-accounts/Sidebar';
+import { useCall, useApi } from '@polkadot/react-hooks';
 import { stringToU8a } from '@polkadot/util';
 
-import { useTranslation } from './translate';
 import { getAddressName } from './util';
-import AccountNameJudgement from './AccountNameJudgement';
-import AddressMini from './AddressMini';
 import Badge from './Badge';
 import Icon from './Icon';
 
@@ -29,9 +27,9 @@ interface Props extends BareProps {
   // this is used by app-account/addresses to toggle editing
   toggle?: boolean;
   value: AccountId | AccountIndex | Address | string | Uint8Array | null | undefined;
+  withSidebar?: boolean;
 }
 
-const DISPLAY_KEYS = ['display', 'legal', 'email', 'web', 'twitter', 'riot'];
 const KNOWN: [AccountId, string][] = [
   [registry.createType('AccountId', stringToU8a('modlpy/socie'.padEnd(32, '\0'))), 'Society'],
   [registry.createType('AccountId', stringToU8a('modlpy/trsry'.padEnd(32, '\0'))), 'Treasury']
@@ -98,16 +96,14 @@ function extractName (address: string, accountIndex?: AccountIndex, defaultName?
   );
 }
 
-function createIdElem (badgeType: 'green' | 'brown' | 'gray', nameElem: React.ReactNode, infoElem: React.ReactNode, hoverElem?: React.ReactNode, onJudge?: undefined | (() => void)): React.ReactNode {
+function createIdElem (badgeType: 'green' | 'brown' | 'gray', nameElem: React.ReactNode, infoElem: React.ReactNode): React.ReactNode {
   return (
     <div className='via-identity'>
       <Badge
-        hover={hoverElem}
         info={infoElem}
         isInline
         isSmall
         isTooltip
-        onClick={onJudge}
         type={badgeType}
       />
       {nameElem}
@@ -115,11 +111,10 @@ function createIdElem (badgeType: 'green' | 'brown' | 'gray', nameElem: React.Re
   );
 }
 
-function extractIdentity (address: string, identity: DeriveAccountRegistration, onJudge: undefined | (() => void), t: (key: string, opts?: object) => string): React.ReactNode {
+function extractIdentity (address: string, identity: DeriveAccountRegistration): React.ReactNode {
   const judgements = identity.judgements.filter(([, judgement]): boolean => !judgement.isFeePaid);
   const isGood = judgements.some(([, judgement]): boolean => judgement.isKnownGood || judgement.isReasonable);
   const isBad = judgements.some(([, judgement]): boolean => judgement.isErroneous || judgement.isLowQuality);
-  const waitCount = identity.judgements.length - judgements.length;
   const displayName = isGood
     ? identity.display
     : (identity.display || '').replace(/[^\x20-\x7E]/g, '');
@@ -130,43 +125,6 @@ function extractIdentity (address: string, identity: DeriveAccountRegistration,
         : identity.displayParent.replace(/[^\x20-\x7E]/g, '')
     )
     : undefined;
-  const hoverElem = (
-    <div>
-      <div>
-        {
-          judgements.length
-            ? (judgements.length === 1
-              ? t('1 judgement')
-              : t('{{count}} judgements', { replace: { count: judgements.length } })
-            )
-            : t('no judgements')
-        }{judgements.length ? ': ' : ''}{judgements.map(([, judgement]): string => judgement.toString()).join(', ')}{
-          waitCount
-            ? t(' ({{count}} waiting)', { replace: { count: waitCount } })
-            : ''
-        }
-      </div>
-      <table>
-        <tbody>
-          {identity.parent && (
-            <tr>
-              <td>{t('parent')}</td>
-              <td><AddressMini value={identity.parent} /></td>
-            </tr>
-          )}
-          {DISPLAY_KEYS
-            .filter((key): boolean => !!identity[key as 'web'])
-            .map((key): React.ReactNode => (
-              <tr key={key}>
-                <td>{t(key)}</td>
-                <td>{identity[key as 'web']}</td>
-              </tr>
-            ))
-          }
-        </tbody>
-      </table>
-    </div>
-  );
   const nameElem = displayParent
     ? <span className={`name ${isGood && 'isGood'}`}><span className='top'>{displayParent}</span><span className='sub'>/{displayName}</span></span>
     : <span className={`name ${isGood && 'isGood'}`}>{displayName}</span>;
@@ -176,16 +134,14 @@ function extractIdentity (address: string, identity: DeriveAccountRegistration,
   nameCache.set(address, [false, displayParent ? [displayParent, displayName] : [displayName, null]]);
   displayCache.set(address, createIdElem(badgeType, nameElem, infoElem));
 
-  return createIdElem(badgeType, nameElem, infoElem, hoverElem, onJudge);
+  return createIdElem(badgeType, nameElem, infoElem);
 }
 
-function AccountName ({ children, className, defaultName, label, noLookup, onClick, override, toggle, value }: Props): React.ReactElement<Props> {
-  const { t } = useTranslation();
+function AccountName ({ children, className, defaultName, label, noLookup, onClick, override, toggle, value, withSidebar }: Props): React.ReactElement<Props> {
   const { api } = useApi();
-  const { isRegistrar, registrars } = useRegistrars(noLookup);
-  const [isJudgementOpen, toggleJudgement] = useToggle();
   const info = useCall<DeriveAccountInfo>(!noLookup && api.derive.accounts.info, [value]);
   const [name, setName] = useState<React.ReactNode>(() => extractName((value || '').toString(), undefined, defaultName));
+  const toggleSidebar = useContext(AccountSidebarToggle);
 
   // set the actual nickname, local name, accountIndex, accountId
   useEffect((): void => {
@@ -195,7 +151,7 @@ function AccountName ({ children, className, defaultName, label, noLookup, onCli
     if (api.query.identity?.identityOf) {
       setName(() =>
         identity?.display
-          ? extractIdentity(cacheAddr, identity, isRegistrar ? toggleJudgement : undefined, t)
+          ? extractIdentity(cacheAddr, identity)
           : extractName(cacheAddr, accountIndex)
       );
     } else if (nickname) {
@@ -205,32 +161,40 @@ function AccountName ({ children, className, defaultName, label, noLookup, onCli
     } else {
       setName(defaultOrAddr(defaultName, cacheAddr, accountIndex));
     }
-  }, [api, defaultName, info, isRegistrar, t, toggle, toggleJudgement, value]);
+  }, [api, defaultName, info, toggle, value]);
+
+  const _onNameEdit = useCallback(
+    () => setName(defaultOrAddr(defaultName, (value || '').toString())),
+    [defaultName, value]
+  );
+
+  const _onToggleSidebar = useCallback(
+    () => toggleSidebar && value && toggleSidebar([value.toString(), _onNameEdit]),
+    [_onNameEdit, toggleSidebar, value]
+  );
 
   return (
-    <>
-      {isJudgementOpen && (
-        <AccountNameJudgement
-          address={(value || '').toString()}
-          registrars={registrars}
-          toggleJudgement={toggleJudgement}
-        />
-      )}
-      <div
-        className={`ui--AccountName ${className}`}
-        onClick={
-          override
-            ? undefined
-            : onClick
-        }
-      >
-        {label || ''}{override || name}{children}
-      </div>
-    </>
+    <div
+      className={`ui--AccountName ${withSidebar && 'withSidebar'} ${className}`}
+      onClick={
+        withSidebar
+          ? _onToggleSidebar
+          : onClick
+      }
+    >
+      {label || ''}{override || name}{children}
+    </div>
   );
 }
 
 export default React.memo(styled(AccountName)`
+  border: 1px dotted transparent;
+
+  &.withSidebar:hover {
+    border-bottom-color: #333;
+    cursor: help !important;
+  }
+
   .via-identity {
     display: inline-block;
     overflow: hidden;

+ 1 - 0
packages/react-components/src/AccountNameJudgement.tsx

@@ -48,6 +48,7 @@ function AccountNameJudgement ({ address, registrars, toggleJudgement }: Props):
   return (
     <Modal
       header={t('Provide judgement')}
+      onClose={toggleJudgement}
       size='small'
     >
       <Modal.Content>

+ 3 - 1
packages/react-components/src/AddressMini.tsx

@@ -34,11 +34,12 @@ interface Props extends BareProps {
   withBalance?: boolean;
   withBonded?: boolean;
   withLockedVote?: boolean;
+  withSidebar?: boolean;
   withName?: boolean;
   withShrink?: boolean;
 }
 
-function AddressMini ({ balance, bonded, children, className, iconInfo, isPadded = true, label, labelBalance, noLookup, summary, value, withAddress = true, withBalance = false, withBonded = false, withLockedVote = false, withName = true, withShrink = false }: Props): React.ReactElement<Props> | null {
+function AddressMini ({ balance, bonded, children, className, iconInfo, isPadded = true, label, labelBalance, noLookup, summary, value, withAddress = true, withBalance = false, withBonded = false, withLockedVote = false, withName = true, withShrink = false, withSidebar = true }: Props): React.ReactElement<Props> | null {
   if (!value) {
     return null;
   }
@@ -67,6 +68,7 @@ function AddressMini ({ balance, bonded, children, className, iconInfo, isPadded
                 <AccountName
                   noLookup={noLookup}
                   value={value}
+                  withSidebar={withSidebar}
                 />
               )
               : toShortAddress(value)

+ 23 - 13
packages/react-components/src/AddressSmall.tsx

@@ -7,26 +7,31 @@ import { Address, AccountId } from '@polkadot/types/interfaces';
 import React from 'react';
 import styled from 'styled-components';
 
+import { classes } from './util';
+import AccountIndex from './AccountIndex';
 import AccountName from './AccountName';
 import IdentityIcon from './IdentityIcon';
 
 interface Props {
+  children?: React.ReactNode;
   className?: string;
   defaultName?: string;
   onClickName?: () => void;
   overrideName?: React.ReactNode;
-  toggle?: boolean;
+  withIndex?: boolean;
+  withSidebar?: boolean;
+  toggle?: any;
   value?: string | Address | AccountId | null | Uint8Array;
 }
 
-function AddressSmall ({ className, defaultName, onClickName, overrideName, toggle, value }: Props): React.ReactElement<Props> {
+function AddressSmall ({ children, className, defaultName, onClickName, overrideName, toggle, value, withIndex, withSidebar = true }: Props): React.ReactElement<Props> {
   return (
     <div className={`ui--AddressSmall ${className}`}>
       <IdentityIcon
         size={32}
         value={value as Uint8Array}
       />
-      <div className='nameInfo'>
+      <div className={classes('nameInfo', withSidebar && 'withSidebar')}>
         <AccountName
           className={(overrideName || !onClickName) ? '' : 'name--clickable'}
           defaultName={defaultName}
@@ -34,24 +39,27 @@ function AddressSmall ({ className, defaultName, onClickName, overrideName, togg
           override={overrideName}
           toggle={toggle}
           value={value}
-        />
+          withSidebar={withSidebar}
+        >
+          {children}
+        </AccountName>
+        {withIndex && (
+          <AccountIndex value={value} />
+        )}
       </div>
     </div>
   );
 }
 
 export default React.memo(styled(AddressSmall)`
-  vertical-align: middle;
-  white-space: nowrap;
-
-  .name--clickable {
-    cursor: pointer;
-  }
+  display: flex;
+  align-items: center;
 
   .ui--IdentityIcon,
   .nameInfo {
     display: inline-block;
     vertical-align: middle;
+    white-space: nowrap;
   }
 
   .ui--IdentityIcon {
@@ -59,10 +67,12 @@ export default React.memo(styled(AddressSmall)`
   }
 
   .nameInfo {
+    &.withSidebar {
+      cursor: help;
+    }
+
     > div {
-      max-width: 16rem;
-      overflow: hidden;
-      text-overflow: ellipsis;
+      max-width: 12rem;
     }
   }
 `);

+ 1 - 0
packages/react-components/src/AddressToggle.tsx

@@ -69,6 +69,7 @@ function AddressToggle ({ address, className, filter, isHidden, noLookup, noTogg
         className='ui--AddressToggle-address'
         noLookup={noLookup}
         value={address}
+        withSidebar={false}
       />
       {!noToggle && (
         <div className='ui--AddressToggle-toggle'>

+ 100 - 0
packages/react-components/src/AvatarItem.tsx

@@ -0,0 +1,100 @@
+// Copyright 2017-2020 @polkadot/app-parachains 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 React from 'react';
+import styled from 'styled-components';
+
+interface Props {
+  children?: React.ReactNode;
+  className?: string;
+  color?: string;
+  icon: React.ReactNode;
+  isBig?: boolean;
+  title: React.ReactNode;
+  subtitle: React.ReactNode;
+}
+
+function AvatarItem ({ children, className, icon, isBig, subtitle, title }: Props): React.ReactElement<Props> {
+  return (
+    <div className={['ui--AvatarItem', className, isBig ? 'big' : ''].join(' ')}>
+      <div className='ui--AvatarItem-icon'>
+        {icon}
+      </div>
+      <div className='ui--AvatarItem-details'>
+        <div className='ui--AvatarItem-title'>
+          {title}
+        </div>
+        <div className='ui--AvatarItem-subtitle'>
+          {subtitle}
+        </div>
+      </div>
+      {children}
+    </div>
+  );
+}
+
+export default React.memo(styled(AvatarItem)`
+  & {
+    display: flex;
+    align-items: center;
+
+    .ui--AvatarItem-icon {
+      width: 2.4rem;
+      height: 2.4rem;
+      margin-right: 0.5rem;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      > * {
+        border-radius: 50%;
+        width: 100%;
+        height: 100%;
+      }
+
+      > i.icon {
+        color: white;
+        line-height: 2.4rem;
+        margin-right: 0 !important;
+      }
+
+      > img {
+      }
+    }
+
+    .ui--AvatarItem-details {
+      flex: 1;
+
+      .ui--AvatarItem-title {
+        font-weight: bold;
+        font-size: 1rem;
+      }
+
+      .ui--AvatarItem-subtitle {
+        color: rgba(100, 100, 100, 0.6);
+        font-size: 1rem;
+      }
+    }
+
+    &.big {
+      .ui--AvatarItem-icon {
+        width: 3.4rem;
+        height: 3.4rem;
+        margin-right: 0.6rem;
+
+        > i.icon {
+          font-size: 1.6rem;
+          line-height: 3.4rem;
+        }
+      }
+
+      .ui--AvatarItem-details {
+        .ui--AvatarItem-name {
+          font-size: 1.4rem;
+          line-height: 1.4rem;
+        }
+      }
+    }
+  }
+`);

+ 5 - 1
packages/react-components/src/Button/Button.tsx

@@ -14,9 +14,11 @@ import Tooltip from '../Tooltip';
 
 let idCounter = 0;
 
-function Button ({ children, className, floated, icon, isBasic = false, isCircular = false, isDisabled = false, isFluid = false, isIcon, isLoading = false, isNegative = false, isPositive = false, isPrimary = false, label, labelPosition, onClick, size, style, tabIndex, tooltip }: ButtonProps): React.ReactElement<ButtonProps> {
+function Button ({ children, className, floated, icon, isAnimated, isBasic = false, isCircular = false, isDisabled = false, isFluid = false, isIcon, isLoading = false, isNegative = false, isPositive = false, isPrimary = false, label, labelPosition, onClick, onMouseEnter, onMouseLeave, size, style, tabIndex, tooltip }: ButtonProps): React.ReactElement<ButtonProps> {
   const [triggerId] = useState(`button-${++idCounter}`);
   const props = {
+    animate: 'fade',
+    animated: isAnimated,
     basic: isBasic,
     circular: isCircular,
     className: `${className} ${isIcon && 'isIcon'}`,
@@ -29,6 +31,8 @@ function Button ({ children, className, floated, icon, isBasic = false, isCircul
     loading: isLoading,
     negative: isNegative,
     onClick,
+    onMouseEnter,
+    onMouseLeave,
     positive: isPositive,
     primary: isPrimary,
     secondary: !isBasic && !(isPositive || isPrimary || isNegative),

+ 19 - 0
packages/react-components/src/Button/Content.tsx

@@ -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 React from 'react';
+import SUIButton from 'semantic-ui-react/dist/commonjs/elements/Button/Button';
+
+import { ContentProps as Props } from './types';
+
+export default function ButtonContent ({ children, hidden, visible }: Props): React.ReactElement<Props> {
+  return (
+    <SUIButton.Content
+      hidden={hidden}
+      visible={visible}
+    >
+      {children}
+    </SUIButton.Content>
+  );
+}

+ 2 - 0
packages/react-components/src/Button/index.tsx

@@ -5,12 +5,14 @@
 import { ButtonType } from './types';
 
 import IButton from './Button';
+import Content from './Content';
 import Divider from './Divider';
 import Group from './Group';
 import Or from './Or';
 
 const Button = IButton as unknown as ButtonType;
 
+Button.Content = Content;
 Button.Divider = Divider;
 Button.Group = Group;
 Button.Or = Or;

+ 15 - 6
packages/react-components/src/Button/types.ts

@@ -2,16 +2,16 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
+import { ButtonProps as SUIButtonProps } from 'semantic-ui-react/dist/commonjs/elements/Button/Button';
 import { BareProps } from '../types';
 
-export type Button$Sizes = 'mini' | 'tiny' | 'small' | 'medium' | 'large' | 'big' | 'huge' | 'massive';
-
-export type Button$OnClick = () => void | Promise<void>;
+export type Button$Callback = () => void | Promise<void>;
 
 export interface ButtonProps extends BareProps {
   children?: React.ReactNode;
   floated?: 'left' | 'right';
-  icon: string;
+  icon?: string;
+  isAnimated?: SUIButtonProps['animated'];
   isBasic?: boolean;
   isCircular?: boolean;
   isDisabled?: boolean;
@@ -23,15 +23,23 @@ export interface ButtonProps extends BareProps {
   isPrimary?: boolean;
   label?: React.ReactNode;
   labelPosition?: 'left' | 'right';
-  onClick?: Button$OnClick;
+  onClick?: Button$Callback;
+  onMouseEnter?: Button$Callback;
+  onMouseLeave?: Button$Callback;
   ref?: any;
-  size?: Button$Sizes;
+  size?: SUIButtonProps['size'];
   tabIndex?: number;
   tooltip?: React.ReactNode;
 }
 
 export type DividerProps = BareProps;
 
+export interface ContentProps extends BareProps {
+  children?: React.ReactNode;
+  hidden?: boolean;
+  visible?: boolean;
+}
+
 export interface GroupProps extends BareProps {
   children?: React.ReactNode;
   isBasic?: boolean;
@@ -43,6 +51,7 @@ export type GroupType = React.ComponentType<GroupProps> & {
 };
 
 export type ButtonType = React.ComponentType<ButtonProps> & {
+  Content: React.ComponentType<ContentProps>;
   Divider: React.ComponentType<DividerProps>;
   Group: GroupType;
   Or: React.ComponentType<BareProps>;

+ 7 - 1
packages/react-components/src/IconLink.tsx

@@ -10,16 +10,22 @@ import styled from 'styled-components';
 import Icon from './Icon';
 
 interface Props extends BareProps {
+  href?: string;
   icon?: string;
   label?: React.ReactNode;
+  rel?: string;
+  target?: string;
   onClick: () => void;
 }
 
-function IconLink ({ className, icon, label, onClick }: Props): React.ReactElement<Props> {
+function IconLink ({ className, href, icon, label, onClick, rel, target }: Props): React.ReactElement<Props> {
   return (
     <a
       className={className}
+      href={href}
       onClick={onClick}
+      rel={rel}
+      target={target}
     >
       {icon && <Icon className={icon} />}
       {label}

+ 2 - 1
packages/react-components/src/IdentityIcon.tsx

@@ -25,13 +25,14 @@ function IdentityIcon ({ className, onCopy, prefix, size, theme, value }: Props)
   const { queueAction } = useContext(StatusContext);
   const validators = useContext(ValidatorsContext);
   const [isValidator, setIsValidator] = useState(false);
-  const [address] = useState(value?.toString());
+  const [address, setAddress] = useState(value?.toString());
   const thisTheme = theme || getIdentityTheme(systemName);
 
   useEffect((): void => {
     value && setIsValidator(
       validators.includes(value.toString())
     );
+    value && setAddress(value.toString());
   }, [value, validators]);
 
   const _onCopy = useCallback(

+ 4 - 4
packages/react-components/src/Input.tsx

@@ -2,11 +2,11 @@
 // 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 { BareProps, VoidFn } from './types';
 
 import React, { useCallback, useState } from 'react';
 import SUIInput from 'semantic-ui-react/dist/commonjs/elements/Input/Input';
-import { isUndefined } from '@polkadot/util';
+import { isFunction, isUndefined } from '@polkadot/util';
 
 import Labelled from './Labelled';
 
@@ -33,7 +33,7 @@ interface Props extends BareProps {
   maxLength?: number;
   min?: any;
   name?: string;
-  onEnter?: () => void;
+  onEnter?: boolean | VoidFn;
   onEscape?: () => void;
   onChange?: (value: string) => void;
   onBlur?: () => void;
@@ -116,7 +116,7 @@ function Input ({ autoFocus = false, children, className, defaultValue, help, ic
 
       if (onEnter && event.keyCode === 13) {
         (event.target as any).blur();
-        onEnter();
+        isFunction(onEnter) && onEnter();
       }
 
       if (onEscape && event.keyCode === 27) {

+ 1 - 1
packages/react-components/src/InputBalance.tsx

@@ -8,8 +8,8 @@ import BN from 'bn.js';
 import React from 'react';
 import styled from 'styled-components';
 import { BitLengthOption } from '@polkadot/react-components/constants';
-import { InputNumber } from '@polkadot/react-components';
 import { formatBalance } from '@polkadot/util';
+import InputNumber from './InputNumber';
 
 interface Props extends BareProps {
   autoFocus?: boolean;

+ 6 - 3
packages/react-components/src/Popup.tsx

@@ -3,23 +3,26 @@
 // of the Apache-2.0 license. See the LICENSE file for details.
 
 import React from 'react';
-import SUIPopup from 'semantic-ui-react/dist/commonjs/modules/Popup/Popup';
+import SUIPopup, { PopupProps } from 'semantic-ui-react/dist/commonjs/modules/Popup/Popup';
 
 interface Props {
   children: React.ReactNode;
   className?: string;
   isOpen?: boolean;
+  on?: PopupProps['on'];
   onClose?: () => void;
+  position?: PopupProps['position'];
   trigger?: React.ReactNode;
 }
 
-function Popup ({ children, className, isOpen, onClose, trigger }: Props): React.ReactElement<Props> {
+function Popup ({ children, className, isOpen, on = 'click', onClose, position = 'bottom right', trigger }: Props): React.ReactElement<Props> {
   return (
     <SUIPopup
       className={className}
+      on={on}
       onClose={onClose}
       open={isOpen}
-      position='bottom right'
+      position={position}
       trigger={trigger}
     >
       {children}

+ 128 - 0
packages/react-components/src/Transfer.tsx

@@ -0,0 +1,128 @@
+// Copyright 2017-2020 @polkadot/app-accounts 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 { SubmittableExtrinsic } from '@polkadot/api/promise/types';
+
+import BN from 'bn.js';
+import React, { useEffect, useState } from 'react';
+import styled from 'styled-components';
+import { useApi } from '@polkadot/react-hooks';
+import { Available } from '@polkadot/react-query';
+
+import InputAddress from './InputAddress';
+import InputBalance from './InputBalance';
+import Modal from './Modal';
+import TxButton from './TxButton';
+
+import { useTranslation } from './translate';
+
+interface Props {
+  className?: string;
+  onClose: () => void;
+  recipientId?: string;
+  senderId?: string;
+}
+
+const ZERO = new BN(0);
+
+function Transfer ({ className, onClose, recipientId: propRecipientId, senderId: propSenderId }: Props): React.ReactElement<Props> {
+  const { t } = useTranslation();
+  const { api } = useApi();
+  const [amount, setAmount] = useState<BN | undefined>(new BN(0));
+  const [extrinsic, setExtrinsic] = useState<SubmittableExtrinsic | null>(null);
+  const [hasAvailable] = useState(true);
+  const [maxBalance] = useState(new BN(0));
+  const [recipientId, setRecipientId] = useState<string | null>(propRecipientId || null);
+  const [senderId, setSenderId] = useState<string | null>(propSenderId || null);
+
+  useEffect((): void => {
+    senderId && recipientId && setExtrinsic(
+      () => api.tx.balances.transfer(recipientId, amount || ZERO)
+    );
+  }, [api, amount, recipientId, senderId]);
+
+  const transferrable = <span className='label'>{t('transferrable')}</span>;
+
+  return (
+    <Modal
+      className='app--accounts-Modal'
+      header={t('Send funds')}
+    >
+      <Modal.Content>
+        <div className={className}>
+          <InputAddress
+            defaultValue={propSenderId}
+            help={t('The account you will send funds from.')}
+            isDisabled={!!propSenderId}
+            label={t('send from account')}
+            labelExtra={
+              <Available
+                label={transferrable}
+                params={senderId}
+              />
+            }
+            onChange={setSenderId}
+            type='account'
+          />
+          <InputAddress
+            defaultValue={propRecipientId}
+            help={t('Select a contact or paste the address you want to send funds to.')}
+            isDisabled={!!propRecipientId}
+            label={t('send to address')}
+            labelExtra={
+              <Available
+                label={transferrable}
+                params={recipientId}
+              />
+            }
+            onChange={setRecipientId}
+            type='allPlus'
+          />
+          <InputBalance
+            autoFocus
+            help={t('Type the amount you want to transfer. Note that you can select the unit on the right e.g sending 1 milli is equivalent to sending 0.001.')}
+            isError={!hasAvailable}
+            isZeroable
+            label={t('amount')}
+            maxValue={maxBalance}
+            onChange={setAmount}
+            withMax
+          />
+        </div>
+      </Modal.Content>
+      <Modal.Actions onCancel={onClose}>
+        <TxButton
+          accountId={senderId}
+          extrinsic={extrinsic}
+          icon='send'
+          isDisabled={!hasAvailable}
+          isPrimary
+          label={t('Make Transfer')}
+          onStart={onClose}
+        />
+      </Modal.Actions>
+    </Modal>
+  );
+}
+
+export default React.memo(styled(Transfer)`
+  article.padded {
+    box-shadow: none;
+    margin-left: 2rem;
+  }
+
+  .balance {
+    margin-bottom: 0.5rem;
+    text-align: right;
+    padding-right: 1rem;
+
+    .label {
+      opacity: 0.7;
+    }
+  }
+
+  label.with-help {
+    flex-basis: 10rem;
+  }
+`);

+ 3 - 0
packages/react-components/src/index.tsx

@@ -4,6 +4,7 @@
 
 export { default as AccountIndex } from './AccountIndex';
 export { default as AccountName } from './AccountName';
+export { default as AccountNameJudgement } from './AccountNameJudgement';
 export { default as ActionItem } from './ActionItem';
 export { default as AddressInfo } from './AddressInfo';
 export { default as AddressMini } from './AddressMini';
@@ -11,6 +12,7 @@ export { default as AddressRow } from './AddressRow';
 export { default as AddressSmall } from './AddressSmall';
 export { default as AddressToggle } from './AddressToggle';
 export { default as Available } from './Available';
+export { default as AvatarItem } from './AvatarItem';
 export { default as Badge } from './Badge';
 export { default as Balance } from './Balance';
 export { default as Bonded } from './Bonded';
@@ -83,6 +85,7 @@ export { default as Tabs } from './Tabs';
 export { default as Tag } from './Tag';
 export { default as Toggle } from './Toggle';
 export { default as Tooltip } from './Tooltip';
+export { default as Transfer } from './Transfer';
 export { default as TxButton } from './TxButton';
 export { default as TxComponent } from './TxComponent';
 export { default as TxModal } from './TxModal';

+ 5 - 5
packages/react-components/src/styles/theme.ts

@@ -5,18 +5,18 @@
 import { css } from 'styled-components';
 
 /* default buttons, dark gray */
-const colorBtnDefault = '#666';
+export const colorBtnDefault = '#666';
 
 /* highlighted buttons, orange */
-const colorBtnHighlight = '#f19135';
+export const colorBtnHighlight = '#f19135';
 
 /* primary buttons, blue */
-const colorBtnPrimary = colorBtnDefault; // '#2e86ab';
+export const colorBtnPrimary = colorBtnDefault; // '#2e86ab';
 
 /* button text color */
-const colorBtnText = '#f9f9f9';
+export const colorBtnText = '#f9f9f9';
 
-const colorLink = '#2e86ab';
+export const colorLink = '#2e86ab';
 
 export default css`
   .theme--default {

+ 4 - 3
packages/react-components/src/types.ts

@@ -3,13 +3,14 @@
 // of the Apache-2.0 license. See the LICENSE file for details.
 
 import { WithTranslation } from 'react-i18next';
+import { ButtonProps as SUIButtonProps } from 'semantic-ui-react/dist/commonjs/elements/Button/Button';
 import { SubmittableExtrinsic } from '@polkadot/api/promise/types';
 import { Abi } from '@polkadot/api-contract';
 import { ActionStatus } from '@polkadot/react-components/Status/types';
 import { TxState } from '@polkadot/react-hooks/types';
 import { IExtrinsic } from '@polkadot/types/types';
 import { AccountId, Index } from '@polkadot/types/interfaces';
-import { ButtonProps, Button$Sizes } from './Button/types';
+import { ButtonProps } from './Button/types';
 import { InputAddressProps } from './InputAddress/types';
 import { TxCallback, TxFailedCallback } from './Status/types';
 
@@ -55,7 +56,7 @@ export interface TxButtonProps extends TxProps {
   accountNonce?: Index;
   className?: string;
   icon?: string;
-  iconSize?: Button$Sizes;
+  iconSize?: SUIButtonProps['size'];
   isBasic?: boolean;
   isDisabled?: boolean;
   isIcon?: boolean;
@@ -69,7 +70,7 @@ export interface TxButtonProps extends TxProps {
   onStart?: VoidFn;
   onSuccess?: TxCallback;
   onUpdate?: TxCallback;
-  size?: Button$Sizes;
+  size?: SUIButtonProps['size'];
   tooltip?: string;
   withSpinner?: boolean;
 }

+ 1 - 0
packages/react-hooks/src/index.ts

@@ -3,6 +3,7 @@
 // of the Apache-2.0 license. See the LICENSE file for details.
 
 export { default as useAccountId } from './useAccountId';
+export { default as useAccountInfo } from './useAccountInfo';
 export { default as useAccounts } from './useAccounts';
 export { default as useAddresses } from './useAddresses';
 export { default as useApi } from './useApi';

+ 40 - 1
packages/react-hooks/src/types.ts

@@ -2,7 +2,8 @@
 // This software may be modified and distributed under the terms
 // of the Apache-2.0 license. See the LICENSE file for details.
 
-import { ConstructTxFn, StringOrNull } from '@polkadot/react-components/types';
+import { DeriveAccountFlags, DeriveAccountRegistration } from '@polkadot/api-derive/types';
+import { ConstructTxFn, StringOrNull, VoidFn } from '@polkadot/react-components/types';
 import { AccountId, Balance, BlockNumber, Call, Hash, SessionIndex } from '@polkadot/types/interfaces';
 import { IExtrinsic } from '@polkadot/types/types';
 import { SubmittableExtrinsic } from '@polkadot/api/promise/types';
@@ -72,3 +73,41 @@ export interface UseSudo {
   sudoKey?: string;
   isMine: boolean;
 }
+
+export interface AddressFlags extends DeriveAccountFlags {
+  isDevelopment: boolean;
+  isEditable: boolean;
+  isExternal: boolean;
+  isFavorite: boolean;
+  isInContacts: boolean;
+  isOwned: boolean;
+}
+
+export interface AddressIdentity extends DeriveAccountRegistration {
+  isGood: boolean;
+  isBad: boolean;
+  isKnownGood: boolean;
+  isReasonable: boolean;
+  isErroneous: boolean;
+  isLowQuality: boolean;
+  isExistent: boolean;
+  waitCount: number;
+}
+
+export interface UseAccountInfo extends AddressFlags {
+  name: string;
+  setName: React.Dispatch<string>;
+  tags: string[];
+  setTags: React.Dispatch<string[]>;
+  genesisHash: StringOrNull;
+  setGenesisHash: React.Dispatch<StringOrNull>;
+  identity?: AddressIdentity;
+  isEditingName: boolean;
+  toggleIsEditingName: VoidFn;
+  isEditingTags: boolean;
+  toggleIsEditingTags: VoidFn;
+  onSaveName: VoidFn;
+  onSaveTags: VoidFn;
+  onSaveGenesisHash: VoidFn;
+  onForgetAddress: VoidFn;
+}

+ 206 - 0
packages/react-hooks/src/useAccountInfo.ts

@@ -0,0 +1,206 @@
+// Copyright 2017-2020 @polkadot/react-hooks 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 { DeriveAccountFlags, DeriveAccountInfo } from '@polkadot/api-derive/types';
+import { StringOrNull } from '@polkadot/react-components/types';
+import { Address, AccountId } from '@polkadot/types/interfaces';
+import { AddressFlags, AddressIdentity, UseAccountInfo } from './types';
+
+import { useCallback, useEffect, useState } from 'react';
+import keyring from '@polkadot/ui-keyring';
+
+import useAccounts from './useAccounts';
+import useAddresses from './useAddresses';
+import useApi from './useApi';
+import useCall from './useCall';
+import useToggle from './useToggle';
+
+const IS_NONE = {
+  isCouncil: false,
+  isDevelopment: false,
+  isEditable: false,
+  isExternal: false,
+  isFavorite: false,
+  isInContacts: false,
+  isOwned: false,
+  isSociety: false,
+  isSudo: false,
+  isTechCommittee: false
+};
+
+export default function useAccountInfo (_value: AccountId | Address | string | Uint8Array): UseAccountInfo {
+  const value = _value.toString();
+  const { api } = useApi();
+  const accountInfo = useCall<DeriveAccountInfo>(api.derive.accounts.info as any, [value]);
+  const accountFlags = useCall<DeriveAccountFlags>(api.derive.accounts.flags as any, [value]);
+  const { isAccount } = useAccounts();
+  const { isAddress } = useAddresses();
+
+  const [tags, setSortedTags] = useState<string[]>([]);
+  const [name, setName] = useState('');
+  const [genesisHash, setGenesisHash] = useState<StringOrNull>(null);
+  const [identity, setIdentity] = useState<AddressIdentity | undefined>();
+
+  const [flags, setFlags] = useState<AddressFlags>(IS_NONE);
+  const [isEditingName, toggleIsEditingName] = useToggle(false);
+  const [isEditingTags, toggleIsEditingTags] = useToggle(false);
+
+  useEffect((): void => {
+    accountFlags && setFlags((flags) => ({
+      ...flags,
+      ...accountFlags
+    }));
+  }, [accountFlags]);
+
+  useEffect((): void => {
+    const { identity, nickname } = accountInfo || {};
+
+    if (api.query.identity && api.query.identity.identityOf) {
+      if (identity?.display) {
+        setName(identity.display);
+      }
+    } else if (nickname) {
+      setName(nickname);
+    } else {
+      setName('');
+    }
+
+    if (identity) {
+      const judgements = identity.judgements.filter(([, judgement]) => !judgement.isFeePaid);
+      const isKnownGood = judgements.some(([, judgement]) => judgement.isKnownGood);
+      const isReasonable = judgements.some(([, judgement]) => judgement.isReasonable);
+      const isErroneous = judgements.some(([, judgement]) => judgement.isErroneous);
+      const isLowQuality = judgements.some(([, judgement]) => judgement.isLowQuality);
+
+      setIdentity({
+        ...identity,
+        isBad: isErroneous || isLowQuality,
+        isErroneous,
+        isExistent: !!identity.display,
+        isGood: isKnownGood || isReasonable,
+        isKnownGood,
+        isLowQuality,
+        isReasonable,
+        judgements,
+        waitCount: identity.judgements.length - judgements.length
+      });
+    } else {
+      setIdentity(undefined);
+    }
+  }, [accountInfo, api]);
+
+  useEffect((): void => {
+    const accountOrAddress = keyring.getAccount(value) || keyring.getAddress(value);
+    const isOwned = isAccount(value);
+    const isInContacts = isAddress(value);
+
+    setGenesisHash(accountOrAddress?.meta.genesisHash || null);
+    setFlags((flags) => ({
+      ...flags,
+      isDevelopment: accountOrAddress?.meta.isTesting || false,
+      isEditable: (!identity?.display && (isInContacts || (accountOrAddress && !(accountOrAddress.meta.isInjected || accountOrAddress.meta.isHardware)))) || false,
+      isExternal: accountOrAddress?.meta.isExternal || false,
+      isInContacts,
+      isOwned
+    }));
+    setName(accountOrAddress?.meta.name || '');
+    setSortedTags(accountOrAddress?.meta.tags ? accountOrAddress.meta.tags.sort() : []);
+  }, [identity, isAccount, isAddress, value]);
+
+  const onSaveName = useCallback(
+    (): void => {
+      if (isEditingName) {
+        toggleIsEditingName();
+      }
+
+      const meta = { name, whenEdited: Date.now() };
+
+      if (value) {
+        try {
+          const currentKeyring = keyring.getPair(value);
+
+          currentKeyring && keyring.saveAccountMeta(currentKeyring, meta);
+        } catch (error) {
+          keyring.saveAddress(value, meta);
+        }
+      }
+    },
+    [isEditingName, name, toggleIsEditingName, value]
+  );
+
+  const onSaveTags = useCallback(
+    (): void => {
+      if (isEditingTags) {
+        toggleIsEditingTags();
+      }
+
+      const meta = { tags, whenEdited: Date.now() };
+
+      if (value) {
+        try {
+          const currentKeyring = keyring.getPair(value);
+
+          currentKeyring && keyring.saveAccountMeta(currentKeyring, meta);
+        } catch (error) {
+          keyring.saveAddress(value, meta);
+        }
+      }
+    },
+    [isEditingTags, tags, toggleIsEditingTags, value]
+  );
+
+  const onForgetAddress = useCallback(
+    (): void => {
+      if (isEditingName) {
+        toggleIsEditingName();
+      }
+
+      if (isEditingTags) {
+        toggleIsEditingTags();
+      }
+
+      try {
+        keyring.forgetAddress(value);
+      } catch (e) {
+        console.error(e);
+      }
+    },
+    [isEditingName, isEditingTags, toggleIsEditingName, toggleIsEditingTags, value]
+  );
+
+  const onSaveGenesisHash = useCallback(
+    (): void => {
+      const account = keyring.getPair(value);
+
+      account && keyring.saveAccountMeta(account, { ...account.meta, genesisHash });
+
+      setGenesisHash(genesisHash);
+    },
+    [genesisHash, value]
+  );
+
+  const setTags = useCallback(
+    (tags: string[]): void => setSortedTags(tags.sort()),
+    []
+  );
+
+  return {
+    genesisHash,
+    identity,
+    isEditingName,
+    isEditingTags,
+    name,
+    onForgetAddress,
+    onSaveGenesisHash,
+    onSaveName,
+    onSaveTags,
+    setGenesisHash,
+    setName,
+    setTags,
+    tags,
+    toggleIsEditingName,
+    toggleIsEditingTags,
+    ...flags
+  };
+}

+ 5 - 2
packages/react-hooks/src/useAccounts.ts

@@ -3,6 +3,7 @@
 // of the Apache-2.0 license. See the LICENSE file for details.
 
 import { useEffect, useState } from 'react';
+import { AccountId, Address } from '@polkadot/types/interfaces';
 import accountObservable from '@polkadot/ui-keyring/observable/accounts';
 
 import useIsMountedRef from './useIsMountedRef';
@@ -10,19 +11,21 @@ import useIsMountedRef from './useIsMountedRef';
 interface UseAccounts {
   allAccounts: string[];
   hasAccounts: boolean;
+  isAccount: (_: string | AccountId | Address) => boolean;
 }
 
 export default function useAccounts (): UseAccounts {
   const mountedRef = useIsMountedRef();
-  const [state, setState] = useState<UseAccounts>({ allAccounts: [], hasAccounts: false });
+  const [state, setState] = useState<UseAccounts>({ allAccounts: [], hasAccounts: false, isAccount: () => false });
 
   useEffect((): () => void => {
     const subscription = accountObservable.subject.subscribe((accounts): void => {
       if (mountedRef.current) {
         const allAccounts = accounts ? Object.keys(accounts) : [];
         const hasAccounts = allAccounts.length !== 0;
+        const isAccount = (accountId: string | AccountId | Address): boolean => allAccounts.includes(accountId.toString());
 
-        setState({ allAccounts, hasAccounts });
+        setState({ allAccounts, hasAccounts, isAccount });
       }
     });
 

+ 5 - 2
packages/react-hooks/src/useAddresses.ts

@@ -3,6 +3,7 @@
 // of the Apache-2.0 license. See the LICENSE file for details.
 
 import { useEffect, useState } from 'react';
+import { AccountId, Address } from '@polkadot/types/interfaces';
 import addressObservable from '@polkadot/ui-keyring/observable/addresses';
 
 import useIsMountedRef from './useIsMountedRef';
@@ -10,19 +11,21 @@ import useIsMountedRef from './useIsMountedRef';
 interface UseAccounts {
   allAddresses: string[];
   hasAddresses: boolean;
+  isAddress: (_: string | AccountId | Address) => boolean;
 }
 
 export default function useAccounts (): UseAccounts {
   const mountedRef = useIsMountedRef();
-  const [state, setState] = useState<UseAccounts>({ allAddresses: [], hasAddresses: false });
+  const [state, setState] = useState<UseAccounts>({ allAddresses: [], hasAddresses: false, isAddress: () => false });
 
   useEffect((): () => void => {
     const subscription = addressObservable.subject.subscribe((addresses): void => {
       if (mountedRef.current) {
         const allAddresses = addresses ? Object.keys(addresses) : [];
         const hasAddresses = allAddresses.length !== 0;
+        const isAddress = (address: string | AccountId | Address): boolean => allAddresses.includes(address.toString());
 
-        setState({ allAddresses, hasAddresses });
+        setState({ allAddresses, hasAddresses, isAddress });
       }
     });
 

+ 55 - 55
yarn.lock

@@ -3893,9 +3893,9 @@ __metadata:
   linkType: hard
 
 "@types/node@npm:*, @types/node@npm:>= 8":
-  version: 13.13.0
-  resolution: "@types/node@npm:13.13.0"
-  checksum: 2/30bc5a1ade71f18de9a4324b273d140a131310e92245a449b39c3aaa1271ca03d6a59b79cb12154bbedd8960f99d184e0d757e53fe704d6d1736d9567696468e
+  version: 13.13.2
+  resolution: "@types/node@npm:13.13.2"
+  checksum: 2/de03e2f34b92655ae6f89abfed3362559751d0ae4f35905b6a931dca03e1e9cb4f51b104f509661b1df03baafdaab5b44d6a142620c67cb907af821f5481df32
   languageName: node
   linkType: hard
 
@@ -4776,11 +4776,11 @@ __metadata:
   linkType: hard
 
 "acorn-import-meta@npm:^1.0.0":
-  version: 1.0.0
-  resolution: "acorn-import-meta@npm:1.0.0"
+  version: 1.1.0
+  resolution: "acorn-import-meta@npm:1.1.0"
   peerDependencies:
-    acorn: ^6.0.0
-  checksum: 2/c39d4fc960653bdb5e61ed2d0982afd84e185b2f5ba2e4139a60e456305df558d10d3a969508bf4c1f621bdf064c7d82db4f33ac3eda2e19f39b28b9d728b532
+    acorn: ^6.0.0 || ^7.0.0
+  checksum: 2/16facde86452de81c367bc31dcc90778b7b762cd6f666b1ddd7fec375e0261aa7a758a5f118cad0bc94b1acfbf5036c4bf657788693832b2db4ad3bc5b49732f
   languageName: node
   linkType: hard
 
@@ -4859,7 +4859,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"acorn@npm:^6.0.0, acorn@npm:^6.0.1, acorn@npm:^6.2.1":
+"acorn@npm:^6.0.0, acorn@npm:^6.0.1, acorn@npm:^6.4.1":
   version: 6.4.1
   resolution: "acorn@npm:6.4.1"
   bin:
@@ -5511,11 +5511,11 @@ __metadata:
   linkType: hard
 
 "babel-plugin-dynamic-import-node@npm:^2.3.0":
-  version: 2.3.0
-  resolution: "babel-plugin-dynamic-import-node@npm:2.3.0"
+  version: 2.3.3
+  resolution: "babel-plugin-dynamic-import-node@npm:2.3.3"
   dependencies:
     object.assign: ^4.1.0
-  checksum: 2/2987dc15b8d07b2527aa1019d96e56d65eeb2fa596f67231fc39cc739d68ccd7f477dc3f9c40245de25a90ea76e8619e6ecd1569b65bcfcf15db097d81aa6cd2
+  checksum: 2/6745b8edca96f6c8bc34ab65935b5676358d2e55323e8e823b8de7aa353e3e6398a495ce434c9c36ad5fb1609467a1b1a0028946e1490bf7de8f97df3ae7f3b1
   languageName: node
   linkType: hard
 
@@ -5987,16 +5987,16 @@ __metadata:
   linkType: hard
 
 "browserslist@npm:^4.0.0, browserslist@npm:^4.11.1, browserslist@npm:^4.8.5, browserslist@npm:^4.9.1":
-  version: 4.11.1
-  resolution: "browserslist@npm:4.11.1"
+  version: 4.12.0
+  resolution: "browserslist@npm:4.12.0"
   dependencies:
-    caniuse-lite: ^1.0.30001038
-    electron-to-chromium: ^1.3.390
+    caniuse-lite: ^1.0.30001043
+    electron-to-chromium: ^1.3.413
     node-releases: ^1.1.53
     pkg-up: ^2.0.0
   bin:
     browserslist: cli.js
-  checksum: 2/fdf7a5ab90fac4dd9ffbcc5c219afbe30a8deb16b2d6fe4277cf4271e12b59f1b464ace29fb4ff24896a11e14a02f4cc27a6a560575d2767b77a629d073deea4
+  checksum: 2/564af87b3300321d885c22b6fb010a4d702b1cc77591d684d8f79411d5df65b9290a043348bcb5e4ca96b423c703aeb83fa25979ee3b140b28c48fc965dea0ed
   languageName: node
   linkType: hard
 
@@ -6351,10 +6351,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001038, caniuse-lite@npm:^1.0.30001039":
-  version: 1.0.30001043
-  resolution: "caniuse-lite@npm:1.0.30001043"
-  checksum: 2/142c04989837e6332a1d1ac451df98ef9ab08097df4e1c54b4d8a57dd496d0014edbc749834921e7d1cd5306a1cd6264daec5d5743214ea3d28019b5e70fe74c
+"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001039, caniuse-lite@npm:^1.0.30001043":
+  version: 1.0.30001045
+  resolution: "caniuse-lite@npm:1.0.30001045"
+  checksum: 2/b9b393d506d4c771dff786d040eb8b5e988311f691c59df2c24644604e2b86a06d00b4b0499453e67feee0e4a8d02ead6f115d47a548a075095ecb7c56c72ebb
   languageName: node
   linkType: hard
 
@@ -7371,8 +7371,8 @@ __metadata:
   linkType: hard
 
 "coveralls@npm:^3.0.11":
-  version: 3.0.11
-  resolution: "coveralls@npm:3.0.11"
+  version: 3.0.13
+  resolution: "coveralls@npm:3.0.13"
   dependencies:
     js-yaml: ^3.13.1
     lcov-parse: ^1.0.0
@@ -7381,7 +7381,7 @@ __metadata:
     request: ^2.88.0
   bin:
     coveralls: ./bin/coveralls.js
-  checksum: 2/8bf464950dda177a07f930109ddc8cc3779ddb1d0d3b666789883d6234f4094922dbd93e00ed5917c2535a1fea378c328e8fde4be336b5c5ac8b4509ef797e4e
+  checksum: 2/d573474ef85762d7859ffe494daef5fb32d9b92a27779e34adacb4ea60aa74b59df72b34b12a1d3a3805e9715407daa74e5f7c7f976960f7e3971c57aa000cbb
   languageName: node
   linkType: hard
 
@@ -8693,10 +8693,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"electron-to-chromium@npm:^1.3.390":
-  version: 1.3.413
-  resolution: "electron-to-chromium@npm:1.3.413"
-  checksum: 2/5647038890ac4dc6e657b2951e1cbbe0e1cd66ccfaa27ca7677f6eb2b5df46e838e7f38c70c947f19f96432924f4ba64f8bb92b986919e03494acd0986ea6827
+"electron-to-chromium@npm:^1.3.413":
+  version: 1.3.414
+  resolution: "electron-to-chromium@npm:1.3.414"
+  checksum: 2/e5c162e58042617f4c333bd4e8defca84d07fd251589a4e8aeaa790ebcbeed1175da6b9bf4fc9544422333d85831698a26003c5cb34d72a3162e14fc493335c1
   languageName: node
   linkType: hard
 
@@ -11540,11 +11540,11 @@ __metadata:
   linkType: hard
 
 "i18next@npm:*, i18next@npm:^19.4.2":
-  version: 19.4.2
-  resolution: "i18next@npm:19.4.2"
+  version: 19.4.3
+  resolution: "i18next@npm:19.4.3"
   dependencies:
     "@babel/runtime": ^7.3.1
-  checksum: 2/122cc93821947f427a66ef1a595bb278ad1b16210552772a903f64a5af3726dd190220ddda7bd1d9de05494058264a4df9244180a18f9c69f723a86c7cfe5412
+  checksum: 2/a9f2527abdbfcfa1b117d84b18653b6aee90fd6778676daf7b8c205630b4c06a9d18b83241b862b18f7f916f95883de58f00d9fbc18cfa1d83036a7a7ae132b8
   languageName: node
   linkType: hard
 
@@ -15106,11 +15106,11 @@ __metadata:
   linkType: hard
 
 "nan@npm:^2.12.1, nan@npm:^2.14.0":
-  version: 2.14.0
-  resolution: "nan@npm:2.14.0"
+  version: 2.14.1
+  resolution: "nan@npm:2.14.1"
   dependencies:
     node-gyp: latest
-  checksum: 2/988248a5f141b9ff728d00927607af857564707fb480de7dca775126af3ea5d7fe231958139fb866931742525ef0c0ca9c8d161188df81e1fa5fd79de1e2adc6
+  checksum: 2/eeab7cf260362a578f0b8622716a76d19bc009722049c7274748644ce03b2aa38ca01b3ac730a0497fd2c1ec882a21a0592e800a903994ed4d32acd06bf7eba7
   languageName: node
   linkType: hard
 
@@ -15943,8 +15943,8 @@ __metadata:
   linkType: hard
 
 "ora@npm:^4.0.2":
-  version: 4.0.3
-  resolution: "ora@npm:4.0.3"
+  version: 4.0.4
+  resolution: "ora@npm:4.0.4"
   dependencies:
     chalk: ^3.0.0
     cli-cursor: ^3.1.0
@@ -15954,7 +15954,7 @@ __metadata:
     mute-stream: 0.0.8
     strip-ansi: ^6.0.0
     wcwidth: ^1.0.1
-  checksum: 2/cd84ed2c776b595af9abdef274e17d74ada10bfee88ba919219902a547a0dc853ba24f46f4ec30e589cbb92bae574e406c4169d6f197cf15c279e12a403763ff
+  checksum: 2/120595347066b1749b099015cbee3bda201904b5eff41f72ada2d06a0ebbd54bb2d765d3bfe8da09c61bc1894364e689cb085f65f71d95768acaf4f3ae274a91
   languageName: node
   linkType: hard
 
@@ -19611,12 +19611,12 @@ __metadata:
   linkType: hard
 
 "source-map-support@npm:^0.5.16, source-map-support@npm:^0.5.6, source-map-support@npm:~0.5.12":
-  version: 0.5.17
-  resolution: "source-map-support@npm:0.5.17"
+  version: 0.5.18
+  resolution: "source-map-support@npm:0.5.18"
   dependencies:
     buffer-from: ^1.0.0
     source-map: ^0.6.0
-  checksum: 2/d2fd6d9a9e924c992fe9c026c9a24fd6ea18adee82462075c09aeb9e78202f2c89a7d9f8b22772c19bffce0bfa1d21a95adee3607bb828b81b5439c98a4eb411
+  checksum: 2/f3146ec8ab3c099a875eece31f2c7144d1ac3d220af3d624c3a63c82d7273047fb21622225a5eaac5a6cc9be9cc8dba62104b40089450185de25743ae277e9d5
   languageName: node
   linkType: hard
 
@@ -19675,9 +19675,9 @@ __metadata:
   linkType: hard
 
 "spdx-exceptions@npm:^2.1.0":
-  version: 2.2.0
-  resolution: "spdx-exceptions@npm:2.2.0"
-  checksum: 2/748c042fb1928b5ece6b5ae939ef091207e0c45066419d6c6a4944e21219c038a2a1a6df60d6b1adf3a600cd1da846d72adb246b09d9b8ef56c12b5d92bbfc01
+  version: 2.3.0
+  resolution: "spdx-exceptions@npm:2.3.0"
+  checksum: 2/3cbd2498897dc384158666a9dd7435e3b42ece5da42fd967b218b790e248381d001ec77a676d13d1f4e8da317d97b7bc0ebf4fff37bfbb95923d49b024030c96
   languageName: node
   linkType: hard
 
@@ -20297,8 +20297,8 @@ __metadata:
   linkType: hard
 
 "stylelint@npm:^13.3.2":
-  version: 13.3.2
-  resolution: "stylelint@npm:13.3.2"
+  version: 13.3.3
+  resolution: "stylelint@npm:13.3.3"
   dependencies:
     "@stylelint/postcss-css-in-js": ^0.37.1
     "@stylelint/postcss-markdown": ^0.36.1
@@ -20350,7 +20350,7 @@ __metadata:
     write-file-atomic: ^3.0.3
   bin:
     stylelint: bin/stylelint.js
-  checksum: 2/3afc4f2c252b69f5b2fabe805bbbba1c8da585ff20db040e77992c31a900887a87b06a4eeeb015bf2d33e70e07f25adeafda6ad5a8fb3004ce3d99ecb57bf8d7
+  checksum: 2/b523f716580cb4490597f659e254f1c178f8da9351b2d0cb8482d291a802e32f14097153d65fb3f1ddc3a70dfc32edba64c3dae5b5d82acf046b21ee89b8e532
   languageName: node
   linkType: hard
 
@@ -22208,7 +22208,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"watchpack@npm:^1.6.0":
+"watchpack@npm:^1.6.1":
   version: 1.6.1
   resolution: "watchpack@npm:1.6.1"
   dependencies:
@@ -22427,15 +22427,15 @@ __metadata:
   languageName: node
   linkType: hard
 
-"webpack@npm:4.42.1, webpack@npm:^4.42.1, webpack@npm:^4.8.1":
-  version: 4.42.1
-  resolution: "webpack@npm:4.42.1"
+"webpack@npm:4.43.0, webpack@npm:^4.42.1, webpack@npm:^4.8.1":
+  version: 4.43.0
+  resolution: "webpack@npm:4.43.0"
   dependencies:
     "@webassemblyjs/ast": 1.9.0
     "@webassemblyjs/helper-module-context": 1.9.0
     "@webassemblyjs/wasm-edit": 1.9.0
     "@webassemblyjs/wasm-parser": 1.9.0
-    acorn: ^6.2.1
+    acorn: ^6.4.1
     ajv: ^6.10.2
     ajv-keywords: ^3.4.1
     chrome-trace-event: ^1.0.2
@@ -22452,11 +22452,11 @@ __metadata:
     schema-utils: ^1.0.0
     tapable: ^1.1.3
     terser-webpack-plugin: ^1.4.3
-    watchpack: ^1.6.0
+    watchpack: ^1.6.1
     webpack-sources: ^1.4.1
   bin:
     webpack: bin/webpack.js
-  checksum: 2/c951f5b8728b1e7000b69322d41f643598aaab5ff0f5f9c30ec1eb7942b0454fa4ac80971c0cbfb3e5eeff6d50a88af5fc6632109b5a47aa666cc38684074fc8
+  checksum: 2/9f1e6375ba28368b4a0d02a0807aa4e148b101244d7ab698c99ac478e3043cdacf7d7c89aa7d4af9549509be7d23878ef863bd6a9540e89a18dd2c04e82e2b4f
   languageName: node
   linkType: hard
 
@@ -22862,11 +22862,11 @@ __metadata:
   linkType: hard
 
 "yaml@npm:^1.7.2":
-  version: 1.9.1
-  resolution: "yaml@npm:1.9.1"
+  version: 1.9.2
+  resolution: "yaml@npm:1.9.2"
   dependencies:
     "@babel/runtime": ^7.9.2
-  checksum: 2/56973fe2f959d5e36952c5c676439fa399d42c5a169b9f001574198fd654d3a6038e6429549f20df3f422af5130fcc22c05979fb95824be3fcf92833dd483884
+  checksum: 2/098baa974d33f13c3a93efba4e83c8992f533e78463010afc41a9c2ae574890fa670d91e8d18ffaf9af616c3a0683a4e93b69057bd143f53306570a117ebcb7f
   languageName: node
   linkType: hard