瀏覽代碼

Merge pull request #1156 from Lezek123/joy-members-upgrage

Pioneer: Apps adjustments + joy-members reactivation
Mokhtar Naamani 4 年之前
父節點
當前提交
7fa601b3e7
共有 73 個文件被更改,包括 1267 次插入534 次删除
  1. 3 0
      package.json
  2. 0 3
      pioneer/.eslintignore
  3. 8 2
      pioneer/.eslintrc.js
  4. 3 1
      pioneer/packages/apps-config/src/api/spec/index.ts
  5. 3 0
      pioneer/packages/apps-config/src/api/spec/joystream-node.ts
  6. 5 0
      pioneer/packages/apps-config/src/settings/endpoints.ts
  7. 3 1
      pioneer/packages/apps-config/src/ui/logos/index.ts
  8. 1 0
      pioneer/packages/apps-config/src/ui/logos/nodes/joystream-node.svg
  9. 21 41
      pioneer/packages/apps-routing/src/index.ts
  10. 15 0
      pioneer/packages/apps-routing/src/joy-members.ts
  11. 27 0
      pioneer/packages/apps-routing/src/joy-pages.ts
  12. 二進制
      pioneer/packages/apps/public/favicon.ico
  13. 11 0
      pioneer/packages/apps/public/index.html
  14. 14 1
      pioneer/packages/apps/src/Apps.tsx
  15. 1 1
      pioneer/packages/apps/src/Content/NotFound.tsx
  16. 2 6
      pioneer/packages/apps/src/Content/index.tsx
  17. 56 0
      pioneer/packages/apps/src/JoyTopBar/TopBar.tsx
  18. 1 1
      pioneer/packages/apps/src/SideBar/ChainInfo.tsx
  19. 3 24
      pioneer/packages/apps/src/SideBar/index.tsx
  20. 20 13
      pioneer/packages/apps/src/index.tsx
  21. 3 11
      pioneer/packages/apps/webpack.base.config.js
  22. 2 1
      pioneer/packages/apps/webpack.config.js
  23. 0 0
      pioneer/packages/joy-members/.skip-build
  24. 3 3
      pioneer/packages/joy-members/package.json
  25. 47 29
      pioneer/packages/joy-members/src/Dashboard.tsx
  26. 27 23
      pioneer/packages/joy-members/src/Details.tsx
  27. 3 1
      pioneer/packages/joy-members/src/DetailsByHandle.tsx
  28. 53 45
      pioneer/packages/joy-members/src/EditForm.tsx
  29. 11 6
      pioneer/packages/joy-members/src/List.tsx
  30. 8 8
      pioneer/packages/joy-members/src/MemberPreview.tsx
  31. 0 58
      pioneer/packages/joy-members/src/index.css
  32. 16 11
      pioneer/packages/joy-members/src/index.tsx
  33. 62 0
      pioneer/packages/joy-members/src/style.ts
  34. 3 2
      pioneer/packages/joy-members/src/utils.ts
  35. 0 0
      pioneer/packages/joy-pages/.skip-build
  36. 3 3
      pioneer/packages/joy-pages/package.json
  37. 4 2
      pioneer/packages/joy-pages/src/index.tsx
  38. 0 14
      pioneer/packages/joy-utils-old/src/functions/misc.ts
  39. 0 1
      pioneer/packages/joy-utils-old/src/react/components/index.tsx
  40. 0 1
      pioneer/packages/joy-utils-old/src/react/context/index.tsx
  41. 0 0
      pioneer/packages/joy-utils/README.md
  42. 3 3
      pioneer/packages/joy-utils/package.json
  43. 0 0
      pioneer/packages/joy-utils/src/functions/date.ts
  44. 2 2
      pioneer/packages/joy-utils/src/functions/format.ts
  45. 167 0
      pioneer/packages/joy-utils/src/functions/misc.ts
  46. 1 1
      pioneer/packages/joy-utils/src/react/components/FlexCenter.tsx
  47. 4 1
      pioneer/packages/joy-utils/src/react/components/MutedText.tsx
  48. 3 0
      pioneer/packages/joy-utils/src/react/components/Section.tsx
  49. 46 50
      pioneer/packages/joy-utils/src/react/components/TxButton.tsx
  50. 5 5
      pioneer/packages/joy-utils/src/react/components/forms.tsx
  51. 4 0
      pioneer/packages/joy-utils/src/react/components/index.tsx
  52. 30 10
      pioneer/packages/joy-utils/src/react/context/account.tsx
  53. 2 0
      pioneer/packages/joy-utils/src/react/context/index.tsx
  54. 2 6
      pioneer/packages/joy-utils/src/react/context/membership.tsx
  55. 0 0
      pioneer/packages/joy-utils/src/react/helpers/index.ts
  56. 124 0
      pioneer/packages/joy-utils/src/react/hocs/accounts.tsx
  57. 104 0
      pioneer/packages/joy-utils/src/react/hocs/guards.tsx
  58. 2 0
      pioneer/packages/joy-utils/src/react/hooks/index.ts
  59. 6 0
      pioneer/packages/joy-utils/src/react/hooks/useMyAccount.tsx
  60. 6 0
      pioneer/packages/joy-utils/src/react/hooks/useMyMembership.tsx
  61. 0 28
      pioneer/packages/old-apps/apps-routing/src/joy-pages.ts
  62. 0 17
      pioneer/packages/old-apps/apps/src/TopBar.css
  63. 0 47
      pioneer/packages/old-apps/apps/src/TopBar.tsx
  64. 0 11
      pioneer/packages/old-apps/react-components/src/styles/old-theme.ts-unused
  65. 1 1
      pioneer/packages/page-staking/src/Targets/Summary.tsx
  66. 8 0
      pioneer/packages/react-components/src/InputAddress/index.tsx
  67. 5 2
      pioneer/packages/react-components/src/Tabs/Tab.tsx
  68. 1 0
      pioneer/packages/react-components/src/Tabs/types.ts
  69. 3 1
      pioneer/packages/react-components/src/styles/index.ts
  70. 16 8
      pioneer/packages/react-components/src/styles/joystream.ts
  71. 1 1
      pioneer/packages/react-components/src/styles/theme.ts
  72. 7 9
      pioneer/tsconfig.json
  73. 272 18
      yarn.lock

+ 3 - 0
package.json

@@ -23,6 +23,9 @@
     "pioneer/packages/apps*",
     "pioneer/packages/page*",
     "pioneer/packages/react*",
+    "pioneer/packages/joy-utils",
+    "pioneer/packages/joy-members",
+    "pioneer/packages/joy-pages",
     "utils/api-examples"
   ],
   "resolutions": {

+ 0 - 3
pioneer/.eslintignore

@@ -2,16 +2,13 @@
 **/coverage/*
 **/node_modules/*
 packages/old-apps/*
-packages/joy-members/*
 packages/joy-election/*
 packages/joy-forum/*
 packages/joy-help/*
 packages/joy-media/*
-packages/joy-pages/*
 packages/joy-proposals/*
 packages/joy-roles/*
 packages/joy-settings/*
-packages/joy-utils/*
 packages/joy-utils-old/*
 .eslintrc.js
 i18next-scanner.config.js

+ 8 - 2
pioneer/.eslintrc.js

@@ -13,14 +13,20 @@ module.exports = {
   rules: {
     ...base.rules,
     '@typescript-eslint/no-explicit-any': 'off',
-    'react/prop-types': 'off',
     'new-cap': 'off',
     '@typescript-eslint/interface-name-prefix': 'off',
     '@typescript-eslint/ban-ts-comment': 'error',
     // why only required in VSCode!?!? is eslint plugin not working like eslint commandline?
     // Or are we having to add this because of new versions of eslint-config-* ?
     'no-console': 'off',
-    'header/header': 'off' // Temporary disable polkadot's rule
+    // Override some extended config rules:
+    'camelcase': 'off',
+    'header/header': 'off',
+    'sort-keys': 'off',
+    'react/jsx-sort-props': 'off',
+    'react/jsx-max-props-per-line': 'off',
+    'sort-destructure-keys/sort-destructure-keys': 'off',
+    '@typescript-eslint/unbound-method': 'warn', // Doesn't work well with our version of Formik, see: https://github.com/formium/formik/issues/2589
   },
   // isolate pioneer from monorepo eslint rules
   root: true

+ 3 - 1
pioneer/packages/apps-config/src/api/spec/index.ts

@@ -10,6 +10,7 @@ import encointerNodeTeeproxy from './encointer-node-teeproxy';
 import kulupu from './kulupu';
 import nodeTemplate from './node-template';
 import stablePoc from './stable-poc';
+import joystreamNode from './joystream-node';
 
 export default {
   acala,
@@ -21,5 +22,6 @@ export default {
   kulupu,
   'node-template': nodeTemplate,
   'stable-poc': stablePoc,
-  stable_poc: stablePoc
+  stable_poc: stablePoc,
+  'joystream-node': joystreamNode
 };

+ 3 - 0
pioneer/packages/apps-config/src/api/spec/joystream-node.ts

@@ -0,0 +1,3 @@
+import { types } from '@joystream/types';
+
+export default types;

+ 5 - 0
pioneer/packages/apps-config/src/settings/endpoints.ts

@@ -29,6 +29,11 @@ function createDev (t: TFunction): LinkOption[] {
 
 function createLive (t: TFunction): LinkOption[] {
   return [
+    {
+      info: 'joystream',
+      text: t<string>('rpc.joystream', 'Joystream (Current Testnet, hosted by Jsgenesis)', { ns: 'apps-config' }),
+      value: 'wss://rome-rpc-endpoint.joystream.org:9944'
+    },
     {
       dnslink: 'polkadot',
       info: 'polkadot',

+ 3 - 1
pioneer/packages/apps-config/src/ui/logos/index.ts

@@ -17,6 +17,7 @@ import nodeNodle from './nodes/nodle.svg';
 import nodePolkadot from './nodes/polkadot-circle.svg';
 import nodePolkadotJs from './nodes/polkadot-js.svg';
 import nodeSubstrate from './nodes/substrate-hexagon.svg';
+import nodeJoystream from './nodes/joystream-node.svg';
 
 // extensions
 import extensionPolkadotJs from './extensions/polkadot-js.svg';
@@ -48,7 +49,8 @@ const nodeLogos: Record<string, any> = [
   ['Nodle Chain Node', nodeNodle],
   ['parity-polkadot', nodePolkadot],
   ['polkadot-js', nodePolkadotJs],
-  ['substrate-node', nodeSubstrate]
+  ['substrate-node', nodeSubstrate],
+  ['joystream-node', nodeJoystream]
 ].reduce((logos, [node, logo]): Record<string, any> => ({
   ...logos,
   [(node as string).toLowerCase().replace(/-/g, ' ')]: logo

+ 1 - 0
pioneer/packages/apps-config/src/ui/logos/nodes/joystream-node.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240"><defs><style>.cls-1{fill:#4038ff;}.cls-2{fill:#fff;}</style></defs><title>Icon-mono-white-1bg-blue</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_14" data-name="Layer 14"><rect class="cls-1" width="240" height="240"/><path class="cls-2" d="M135.28,49.73l12.7,0-.15,59.67a57,57,0,0,1-14.49,37.86,67.76,67.76,0,0,0,1.72-15Z"/><path class="cls-2" d="M94.28,153.78v0a34.19,34.19,0,0,1-26.15,12.61L72,153.73Z"/><path class="cls-2" d="M102,130.94v1.28a34,34,0,0,1-2,11.41l-25-.06,3.83-12.69Z"/><path class="cls-2" d="M158.14,49.78l12.7,0-.09,36.84a57,57,0,0,1-14.49,37.86,67.76,67.76,0,0,0,1.72-15Z"/><path class="cls-2" d="M125.11,49.69l-.21,82.59a57.22,57.22,0,0,1-57.32,57H61.23l3.83-12.69h2.56a44.5,44.5,0,0,0,44.57-44.35l.22-82.58Z"/></g></g></svg>

+ 21 - 41
pioneer/packages/apps-routing/src/index.ts

@@ -9,69 +9,49 @@ import appSettings from '@polkadot/ui-settings';
 // When adding here, also ensure to add to Dummy.tsx
 
 import accounts from './accounts';
-import claims from './claims';
-import contracts from './contracts';
-import council from './council';
-// import dashboard from './dashboard';
-import democracy from './democracy';
 import explorer from './explorer';
 import extrinsics from './extrinsics';
-import genericAsset from './generic-asset';
 import js from './js';
-import parachains from './parachains';
-import poll from './poll';
 import settings from './settings';
-import society from './society';
 import staking from './staking';
 import storage from './storage';
 import sudo from './sudo';
-import techcomm from './techcomm';
 import toolbox from './toolbox';
 import transfer from './transfer';
-import treasury from './treasury';
+// Joy packages
+import members from './joy-members';
+import { terms, privacyPolicy } from './joy-pages';
 
 export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Routes {
   return appSettings.uiMode === 'light'
     ? [
-      // dashboard,
-      explorer(t),
-      accounts(t),
-      claims(t),
-      poll(t),
-      transfer(t),
-      genericAsset(t),
-      null,
+      members(t),
       staking(t),
-      democracy(t),
-      council(t),
-      // TODO Not sure about the inclusion of treasury, parachains & society here
       null,
-      settings(t)
+      transfer(t),
+      accounts(t),
+      settings(t),
+      // Those are hidden
+      terms(t),
+      privacyPolicy(t)
     ]
     : [
-      // dashboard(t),
-      explorer(t),
-      accounts(t),
-      claims(t),
-      poll(t),
-      transfer(t),
-      genericAsset(t),
-      null,
+      members(t),
       staking(t),
-      democracy(t),
-      council(t),
-      treasury(t),
-      techcomm(t),
-      parachains(t),
-      society(t),
       null,
-      contracts(t),
+      transfer(t),
+      accounts(t),
+      settings(t),
+      null,
+      explorer(t),
       storage(t),
       extrinsics(t),
+      js(t),
+      toolbox(t),
       sudo(t),
       null,
-      settings(t),
-      toolbox(t),
-      js(t)
+      // Those are hidden
+      terms(t),
+      privacyPolicy(t)
     ];
 }

+ 15 - 0
pioneer/packages/apps-routing/src/joy-members.ts

@@ -0,0 +1,15 @@
+import { Route } from './types';
+
+import Members from '@polkadot/joy-members/index';
+
+export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Route {
+  return {
+    Component: Members,
+    display: {
+      needsApi: ['query.members.nextMemberId']
+    },
+    icon: 'users',
+    name: 'members',
+    text: t<string>('nav.membership', 'Membership', { ns: 'apps-routing' })
+  };
+}

+ 27 - 0
pioneer/packages/apps-routing/src/joy-pages.ts

@@ -0,0 +1,27 @@
+import { Route } from './types';
+
+import { ToS, Privacy } from '@polkadot/joy-pages/index';
+
+export function terms (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Route {
+  return {
+    Component: ToS,
+    display: {
+      isHidden: true
+    },
+    text: t<string>('nav.terms', 'Terms of Service', { ns: 'apps-routing' }),
+    icon: 'file',
+    name: 'pages/tos'
+  };
+}
+
+export function privacyPolicy (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Route {
+  return {
+    Component: Privacy,
+    display: {
+      isHidden: true
+    },
+    text: t<string>('nav.privacy', 'Privacy Policy', { ns: 'apps-routing' }),
+    icon: 'file',
+    name: 'pages/privacy'
+  };
+}

二進制
pioneer/packages/apps/public/favicon.ico


+ 11 - 0
pioneer/packages/apps/public/index.html

@@ -6,6 +6,17 @@
     <link rel="manifest" href="manifest.json">
     <link rel="shortcut icon" href="favicon.ico">
     <title><%= htmlWebpackPlugin.options.PAGE_TITLE %></title>
+    <% if (htmlWebpackPlugin.options.IS_PROD) { %>
+      <!-- Global site tag (gtag.js) - Google Analytics -->
+      <script async src="https://www.googletagmanager.com/gtag/js?id=UA-133429788-6"></script>
+      <script>
+        window.dataLayer = window.dataLayer || [];
+        function gtag(){dataLayer.push(arguments);}
+        gtag('js', new Date());
+
+        gtag('config', 'UA-133429788-6', { 'anonymize_ip': true });
+      </script>
+    <% } %>
     <script type="text/javascript" src="/env-config.js"></script>
   </head>
   <body>

+ 14 - 1
pioneer/packages/apps/src/Apps.tsx

@@ -21,6 +21,9 @@ import SideBar from './SideBar';
 import WarmUp from './WarmUp';
 import { WindowDimensionsCtx } from './WindowDimensions';
 
+/* Joystream-specific */
+import TopBar from './JoyTopBar/TopBar';
+
 interface SidebarState {
   isCollapsed: boolean;
   isMenu: boolean;
@@ -96,7 +99,10 @@ function Apps ({ className = '' }: Props): React.ReactElement<Props> {
             toggleMenu={_toggleMenu}
           />
           <Signer>
-            <Content />
+            <div className='apps--Main'>
+              <TopBar />
+              <Content />
+            </div>
           </Signer>
           <ConnectingOverlay />
           <div id={PORTAL_ID} />
@@ -222,4 +228,11 @@ export default React.memo(styled(Apps)`
       opacity: 1;
     }
   }
+
+  .apps--Main {
+    flex-grow: 1;
+    min-height: 100vh;
+    overflow-x: hidden;
+    overflow-y: auto;
+  }
 `);

+ 1 - 1
pioneer/packages/apps/src/Content/NotFound.tsx

@@ -7,7 +7,7 @@ import { Redirect } from 'react-router';
 
 function NotFound (): React.ReactElement {
   return (
-    <Redirect to='/explorer' />
+    <Redirect to='/staking' />
   );
 }
 

+ 2 - 6
pioneer/packages/apps/src/Content/index.tsx

@@ -78,15 +78,11 @@ function Content ({ className }: Props): React.ReactElement<Props> {
 }
 
 export default React.memo(styled(Content)`
-  background: #f5f4f3;
-  flex-grow: 1;
-  height: 100%;
-  min-height: 100vh;
-  overflow-x: hidden;
-  overflow-y: auto;
+  background: rgba(250, 250, 250);
   padding: 0 1.5rem;
   position: relative;
   width: 100%;
+  height: 100%;
 
   @media(max-width: 768px) {
     padding: 0 0.5rem;

+ 56 - 0
pioneer/packages/apps/src/JoyTopBar/TopBar.tsx

@@ -0,0 +1,56 @@
+import React from 'react';
+import { useMyMembership } from '@polkadot/joy-utils/react/hooks';
+import { InputAddress } from '@polkadot/react-components';
+import { Available } from '@polkadot/react-query';
+import styled from 'styled-components';
+import { useApi } from '@polkadot/react-hooks';
+
+const StyledTopBar = styled.div`
+  padding: 0.75rem;
+  background-color: #3f3f3f;
+  border-bottom: 1px solid #d4d4d5;
+  text-align: right;
+  margin: 0;
+
+  &.NoMyAddress {
+    background-color: #ffeb83;
+    color: #000;
+    text-align: center;
+  }
+
+  .ui--InputAddress {
+    display: inline-block;
+  }
+`;
+
+function JoyTopBar () {
+  const {
+    allAccounts,
+    myAddress
+  } = useMyMembership();
+
+  const { isApiReady } = useApi();
+
+  if (!isApiReady) {
+    return null;
+  }
+
+  const balance = <span className='label'>Balance: </span>;
+  const labelExtra = myAddress
+    ? <Available label={balance} params={myAddress} />
+    : 'No key selected';
+
+  return Object.keys(allAccounts || {}).length ? (
+    <StyledTopBar>
+      <InputAddress
+        defaultValue={myAddress}
+        help='My current key that signs transactions'
+        label='My key'
+        labelExtra={labelExtra}
+        type='account'
+      />
+    </StyledTopBar>
+  ) : null;
+}
+
+export default JoyTopBar;

+ 1 - 1
pioneer/packages/apps/src/SideBar/ChainInfo.tsx

@@ -24,7 +24,7 @@ function ChainInfo ({ className = '', onClick }: Props): React.ReactElement<Prop
 
   return (
     <div
-      className={`apps--SideBar-logo ${className} ui--highlight--border`}
+      className={`apps--SideBar-logo ${className}`}
       onClick={onClick}
     >
       <div className='apps--SideBar-logo-inner'>

+ 3 - 24
pioneer/packages/apps/src/SideBar/index.tsx

@@ -7,7 +7,7 @@ import { Routes } from '@polkadot/apps-routing/types';
 import React, { useCallback, useMemo, useState } from 'react';
 import styled from 'styled-components';
 import createRoutes from '@polkadot/apps-routing';
-import { Button, ChainImg, Icon, Menu, media } from '@polkadot/react-components';
+import { Button, ChainImg, Menu, media } from '@polkadot/react-components';
 
 import { SIDEBAR_MENU_THRESHOLD } from '../constants';
 import NetworkModal from '../modals/Network';
@@ -101,27 +101,6 @@ function SideBar ({ className = '', collapse, handleResize, isCollapsed, isMenuO
                 )
             ))}
             <Menu.Divider hidden />
-            <Menu.Item className='apps--SideBar-Item'>
-              <a
-                className='apps--SideBar-Item-NavLink'
-                href='https://github.com/polkadot-js/apps'
-                rel='noopener noreferrer'
-                target='_blank'
-              >
-                <Icon icon='code-branch' /><span className='text'>{t<string>('nav.github', 'GitHub', { ns: 'apps-routing' })}</span>
-              </a>
-            </Menu.Item>
-            <Menu.Item className='apps--SideBar-Item'>
-              <a
-                className='apps--SideBar-Item-NavLink'
-                href='https://wiki.polkadot.network'
-                rel='noopener noreferrer'
-                target='_blank'
-              >
-                <Icon icon='book' /><span className='text'>{t<string>('nav.wiki', 'Wiki', { ns: 'apps-routing' })}</span>
-              </a>
-            </Menu.Item>
-            <Menu.Divider hidden />
             {!isCollapsed && <NodeInfo />}
           </div>
           <div className={`apps--SideBar-collapse ${isCollapsed ? 'collapsed' : 'expanded'}`}>
@@ -159,7 +138,7 @@ export default React.memo(styled(SideBar)`
 
   .apps--SideBar {
     align-items: center;
-    background: #4f5255;
+    background: #3f3f3f;
     box-sizing: border-box;
     display: flex;
     flex-flow: column;
@@ -221,7 +200,7 @@ export default React.memo(styled(SideBar)`
     }
 
     .apps--SideBar-collapse {
-      background: #4f5255;
+      background: #3f3f3f;
       bottom: 0;
       left: 0;
       padding: 0.75rem 0 .75rem 0.65rem;

+ 20 - 13
pioneer/packages/apps/src/index.tsx

@@ -20,6 +20,9 @@ import settings from '@polkadot/ui-settings';
 import Apps from './Apps';
 import WindowDimensions from './WindowDimensions';
 
+/* Joystream-specific */
+import { MyMembershipProvider, MyAccountProvider } from '@polkadot/joy-utils/react/context';
+
 const rootId = 'root';
 const rootElement = document.getElementById(rootId);
 const theme = { theme: settings.uiTheme };
@@ -38,19 +41,23 @@ store.each((_, key): void => {
 ReactDOM.render(
   <Suspense fallback='...'>
     <ThemeProvider theme={theme}>
-      <Queue>
-        <Api url={settings.apiUrl}>
-          <BlockAuthors>
-            <Events>
-              <HashRouter>
-                <WindowDimensions>
-                  <Apps />
-                </WindowDimensions>
-              </HashRouter>
-            </Events>
-          </BlockAuthors>
-        </Api>
-      </Queue>
+      <MyAccountProvider>
+        <Queue>
+          <Api url={settings.apiUrl}>
+            <MyMembershipProvider>
+              <BlockAuthors>
+                <Events>
+                  <HashRouter>
+                    <WindowDimensions>
+                      <Apps />
+                    </WindowDimensions>
+                  </HashRouter>
+                </Events>
+              </BlockAuthors>
+            </MyMembershipProvider>
+          </Api>
+        </Queue>
+      </MyAccountProvider>
     </ThemeProvider>
   </Suspense>,
   rootElement

+ 3 - 11
pioneer/packages/apps/webpack.base.config.js

@@ -98,7 +98,7 @@ function createWebpack (ENV, context) {
           ]
         },
         {
-          exclude: [/semantic-ui-css/],
+          // Original config had "exclude: [/semantic-ui-css/]"
           test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
           use: [
             {
@@ -112,7 +112,8 @@ function createWebpack (ENV, context) {
           ]
         },
         {
-          exclude: [/semantic-ui-css/],
+          // Original config had "exclude: [/semantic-ui-css/]", because Semantic UI Icons
+          // are not used in polkadot-js/apps repository, but they are used in ours
           test: [/\.eot$/, /\.ttf$/, /\.svg$/, /\.woff$/, /\.woff2$/],
           use: [
             {
@@ -123,15 +124,6 @@ function createWebpack (ENV, context) {
               }
             }
           ]
-        },
-        {
-          include: [/semantic-ui-css/],
-          test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/, /\.eot$/, /\.ttf$/, /\.svg$/, /\.woff$/, /\.woff2$/],
-          use: [
-            {
-              loader: require.resolve('null-loader')
-            }
-          ]
         }
       ]
     },

+ 2 - 1
pioneer/packages/apps/webpack.config.js

@@ -20,7 +20,8 @@ module.exports = merge(
     devtool: process.env.BUILD_ANALYZE ? 'source-map' : false,
     plugins: [
       new HtmlWebpackPlugin({
-        PAGE_TITLE: 'Polkadot/Substrate Portal',
+        IS_PROD: ENV === 'production',
+        PAGE_TITLE: 'Joystream Network Portal',
         inject: true,
         template: path.join(context, `${hasPublic ? 'public/' : ''}index.html`)
       })

+ 0 - 0
pioneer/packages/joy-members/.skip-build


+ 3 - 3
pioneer/packages/joy-members/package.json

@@ -7,9 +7,9 @@
   "author": "Joystream contributors",
   "maintainers": [],
   "dependencies": {
-    "@babel/runtime": "^7.7.1",
-    "@polkadot/react-components": "0.37.0-beta.63",
-    "@polkadot/react-query": "0.37.0-beta.63",
+    "@babel/runtime": "^7.10.5",
+    "@polkadot/react-components": "0.51.1",
+    "@polkadot/react-query": "0.51.1",
     "@polkadot/joy-utils": "^0.1.1"
   }
 }

+ 47 - 29
pioneer/packages/joy-members/src/Dashboard.tsx

@@ -3,12 +3,12 @@ import BN from 'bn.js';
 
 import { ApiProps } from '@polkadot/react-api/types';
 import { I18nProps } from '@polkadot/react-components/types';
-import { withCalls } from '@polkadot/react-api/with';
-import { Bubble } from '@polkadot/react-components/index';
+import { withCalls } from '@polkadot/react-api/hoc';
+import { Label } from 'semantic-ui-react';
 import { formatNumber } from '@polkadot/util';
 import { bool as Bool } from '@polkadot/types';
 
-import Section from '@polkadot/joy-utils/Section';
+import { Section } from '@polkadot/joy-utils/react/components';
 import translate from './translate';
 import { queryMembershipToProp } from './utils';
 
@@ -27,39 +27,57 @@ class Dashboard extends React.PureComponent<Props> {
   renderGeneral () {
     const p = this.props;
     const { newMembershipsAllowed: isAllowed } = p;
-    let isAllowedColor = '';
+    let isAllowedColor: 'grey' | 'green' | 'red' = 'grey';
+
     if (isAllowed) {
       isAllowedColor = isAllowed.eq(true) ? 'green' : 'red';
     }
-    return <Section title='General'>
-      <Bubble label='New memberships allowed?' className={isAllowedColor}>
-        {isAllowed && (isAllowed.eq(true) ? 'Yes' : 'No')}
-      </Bubble>
-      <Bubble label='Next member ID'>
-        {formatNumber(p.nextMemberId)}
-      </Bubble>
-      <Bubble label='First member ID'>
-        {formatNumber(FIRST_MEMBER_ID)}
-      </Bubble>
-    </Section>;
+
+    return (
+      <Section title='General'>
+        <Label.Group size='large'>
+          <Label color={isAllowedColor}>
+            New memberships allowed?
+            <Label.Detail>{isAllowed && (isAllowed.eq(true) ? 'Yes' : 'No')}</Label.Detail>
+          </Label>
+          <Label color='grey'>
+            Next member ID
+            <Label.Detail>{formatNumber(p.nextMemberId)}</Label.Detail>
+          </Label>
+          <Label color='grey'>
+            First member ID
+            <Label.Detail>{formatNumber(FIRST_MEMBER_ID)}</Label.Detail>
+          </Label>
+        </Label.Group>
+      </Section>
+    );
   }
 
   renderValidation () {
     const p = this.props;
-    return <Section title='Validation'>
-      <Bubble label='Min. length of handle'>
-        {formatNumber(p.minHandleLength)} chars
-      </Bubble>
-      <Bubble label='Max. length of handle'>
-        {formatNumber(p.maxHandleLength)} chars
-      </Bubble>
-      <Bubble label='Max. length of avatar URI'>
-        {formatNumber(p.maxAvatarUriLength)} chars
-      </Bubble>
-      <Bubble label='Max. length of about'>
-        {formatNumber(p.maxAboutTextLength)} chars
-      </Bubble>
-    </Section>;
+
+    return (
+      <Section title='Validation'>
+        <Label.Group color='grey' size='large'>
+          <Label>
+            Min. length of handle
+            <Label.Detail>{formatNumber(p.minHandleLength)} chars</Label.Detail>
+          </Label>
+          <Label>
+            Max. length of handle
+            <Label.Detail>{formatNumber(p.maxHandleLength)} chars</Label.Detail>
+          </Label>
+          <Label>
+            Max. length of avatar URI
+            <Label.Detail>{formatNumber(p.maxAvatarUriLength)} chars</Label.Detail>
+          </Label>
+          <Label>
+            Max. length of about
+            <Label.Detail>{formatNumber(p.maxAboutTextLength)} chars</Label.Detail>
+          </Label>
+        </Label.Group>
+      </Section>
+    );
   }
 
   render () {

+ 27 - 23
pioneer/packages/joy-members/src/Details.tsx

@@ -5,18 +5,18 @@ import ReactMarkdown from 'react-markdown';
 import { IdentityIcon } from '@polkadot/react-components';
 import { ApiProps } from '@polkadot/react-api/types';
 import { I18nProps } from '@polkadot/react-components/types';
-import { withCalls } from '@polkadot/react-api/with';
+import { withCalls } from '@polkadot/react-api/hoc';
 import { Option } from '@polkadot/types';
 import BalanceDisplay from '@polkadot/react-components/Balance';
-import AddressMini from '@polkadot/react-components/AddressMiniJoy';
+import AddressMini from '@polkadot/react-components/AddressMini';
 import { formatNumber } from '@polkadot/util';
 
 import translate from './translate';
 import { MemberId, Membership, EntryMethod, Paid, Screening, Genesis, SubscriptionId } from '@joystream/types/members';
 import { queryMembershipToProp } from './utils';
 import { Seat } from '@joystream/types/council';
-import { nonEmptyStr, queryToProp } from '@polkadot/joy-utils/index';
-import { MyAccountProps, withMyAccount } from '@polkadot/joy-utils/MyAccount';
+import { nonEmptyStr, queryToProp } from '@polkadot/joy-utils/functions/misc';
+import { MyAccountProps, withMyAccount } from '@polkadot/joy-utils/react/hocs/accounts';
 
 type Props = ApiProps & I18nProps & MyAccountProps & {
   preview?: boolean;
@@ -28,6 +28,7 @@ type Props = ApiProps & I18nProps & MyAccountProps & {
 class Component extends React.PureComponent<Props> {
   render () {
     const { membership } = this.props;
+
     return membership && !membership.handle.isEmpty
       ? this.renderProfile(membership)
       : (
@@ -54,34 +55,34 @@ class Component extends React.PureComponent<Props> {
     const hasAvatar = avatar_uri && nonEmptyStr(avatar_uri.toString());
     const isMyProfile = myAddress && (myAddress === root_account.toString() || myAddress === controller_account.toString());
     const isCouncilor: boolean = (
-      (activeCouncil.find(x => root_account.eq(x.member)) !== undefined) ||
-      (activeCouncil.find(x => controller_account.eq(x.member)) !== undefined)
+      (activeCouncil.find((x) => root_account.eq(x.member)) !== undefined) ||
+      (activeCouncil.find((x) => controller_account.eq(x.member)) !== undefined)
     );
 
     return (
       <>
-      <div className={`item ProfileDetails ${isMyProfile && 'MyProfile'}`}>
-        {hasAvatar
-          ? <img className='ui avatar image' src={avatar_uri.toString()} />
-          : <IdentityIcon className='image' value={root_account} size={40} />
-        }
-        <div className='content'>
-          <div className='header'>
-            <Link to={`/members/${handle.toString()}`} className='handle'>{handle.toString()}</Link>
-            {isMyProfile && <Link to={'/members/edit'} className='ui tiny button'>Edit my profile</Link>}
-          </div>
-          <div className='description'>
-            {isCouncilor &&
-              <b className='muted text' style={{ color: '#607d8b' }}>
+        <div className={`item ProfileDetails${isMyProfile ? ' MyProfile' : ''}`}>
+          {hasAvatar
+            ? <img className='ui avatar image' src={avatar_uri.toString()} />
+            : <IdentityIcon className='image' value={root_account} size={40} />
+          }
+          <div className='content'>
+            <div className='header'>
+              <Link to={`/members/${handle.toString()}`} className='handle'>{handle.toString()}</Link>
+              {isMyProfile && <Link to={'/members/edit'} className='ui tiny button'>Edit my profile</Link>}
+            </div>
+            <div className='description'>
+              {isCouncilor &&
+              <b className='muted text' style={{ color: '#607d8b', display: 'block' }}>
                 <i className='university icon'></i>
                 Council member
               </b>}
-            <BalanceDisplay label='Balance(root): ' params={root_account} />
-            <div>MemberId: {this.props.memberId.toString()}</div>
+              <BalanceDisplay label='Balance(root): ' params={root_account} />
+              <div>MemberId: {this.props.memberId.toString()}</div>
+            </div>
           </div>
         </div>
-      </div>
-      {!preview && this.renderDetails(membership, isCouncilor)}
+        {!preview && this.renderDetails(membership, isCouncilor)}
       </>
     );
   }
@@ -147,11 +148,14 @@ class Component extends React.PureComponent<Props> {
 
   private renderEntryMethod (entry: EntryMethod) {
     const etype = entry.type;
+
     if (etype === Paid.name) {
       const paid = entry.value as Paid;
+
       return <div>Paid, terms ID: {paid.toNumber()}</div>;
     } else if (etype === Screening.name) {
       const accountId = entry.value as Screening;
+
       return <div>Screened by <AddressMini value={accountId} isShort={false} isPadded={false} withBalance /></div>;
     } else if (etype === Genesis.name) {
       return <div>Created at Genesis</div>;

+ 3 - 1
pioneer/packages/joy-members/src/DetailsByHandle.tsx

@@ -1,7 +1,7 @@
 import React from 'react';
 
 import { I18nProps } from '@polkadot/react-components/types';
-import { withCalls } from '@polkadot/react-api/with';
+import { withCalls } from '@polkadot/react-api/hoc';
 import { stringToU8a, u8aToHex } from '@polkadot/util';
 
 import translate from './translate';
@@ -16,6 +16,7 @@ type DetailsByHandleProps = {
 
 function DetailsByHandleInner (p: DetailsByHandleProps) {
   const { memberIdByHandle: memberId } = p;
+
   return memberId !== undefined // here we can't make distinction value existing and loading
     ? <div className='ui massive relaxed middle aligned list FullProfile'>
       <Details memberId={memberId} />
@@ -39,6 +40,7 @@ class Component extends React.PureComponent<Props> {
   render () {
     const { match: { params: { handle } } } = this.props;
     const handleHex = u8aToHex(stringToU8a(handle));
+
     return (
       <DetailsByHandle handle={handleHex} />
     );

+ 53 - 45
pioneer/packages/joy-members/src/EditForm.tsx

@@ -1,19 +1,18 @@
 import BN from 'bn.js';
-import React from 'react';
+import React, { useContext } from 'react';
 import { Link } from 'react-router-dom';
 import { Form, Field, withFormik, FormikProps } from 'formik';
 import * as Yup from 'yup';
 
 import { Vec } from '@polkadot/types';
-import Section from '@polkadot/joy-utils/Section';
-import TxButton from '@polkadot/joy-utils/TxButton';
-import * as JoyForms from '@polkadot/joy-utils/forms';
+import { Section, TxButton } from '@polkadot/joy-utils/react/components';
+import * as JoyForms from '@polkadot/joy-utils/react/components/forms';
 import { SubmittableResult } from '@polkadot/api';
 import { MemberId, Membership, PaidTermId, PaidMembershipTerms } from '@joystream/types/members';
 import { OptionText } from '@joystream/types/common';
-import { MyAccountProps, withMyAccount } from '@polkadot/joy-utils/MyAccount';
+import { MyAccountProps, withMyAccount } from '@polkadot/joy-utils/react/hocs/accounts';
 import { queryMembershipToProp } from './utils';
-import { withCalls } from '@polkadot/react-api/index';
+import { withCalls, ApiContext } from '@polkadot/react-api/index';
 import { Button, Message } from 'semantic-ui-react';
 import { formatBalance } from '@polkadot/util';
 import { TxFailedCallback, TxCallback } from '@polkadot/react-components/Status/types';
@@ -79,12 +78,15 @@ const InnerForm = (props: FormProps) => {
     memberId
   } = props;
 
+  const { api } = useContext(ApiContext);
+
   const onSubmit = (sendTx: () => void) => {
     if (isValid) sendTx();
   };
 
   const onTxFailed: TxFailedCallback = (txResult: SubmittableResult | null) => {
     setSubmitting(false);
+
     if (txResult == null) {
       // Tx cancelled.
 
@@ -102,7 +104,9 @@ const InnerForm = (props: FormProps) => {
 
   // TODO extract to forms.tsx
   const fieldToTextOption = (field: FieldName): OptionText => {
-    return isFieldChanged(field) ? OptionText.some(values[field]) : OptionText.none();
+    return isFieldChanged(field)
+      ? api.createType('Option<Text>', values[field])
+      : api.createType('Option<Text>', null);
   };
 
   const buildTxParams = () => {
@@ -126,29 +130,29 @@ const InnerForm = (props: FormProps) => {
   // TODO show warning that you don't have enough balance to buy a membership
 
   return (
-    <Section title="My Membership Profile">
-      <Form className="ui form JoyForm">
+    <Section title='My Membership Profile'>
+      <Form className='ui form JoyForm'>
         <LabelledText
-          name="handle"
-          label="Handle/nickname"
+          name='handle'
+          label='Handle/nickname'
           placeholder={'You can use a-z, 0-9 and underscores.'}
           style={{ maxWidth: '30rem' }}
           {...props}
         />
         <LabelledText
-          name="avatar"
-          label="Avatar URL"
-          placeholder="Paste here an URL of your avatar image."
+          name='avatar'
+          label='Avatar URL'
+          placeholder='Paste here an URL of your avatar image.'
           {...props}
         />
-        <LabelledField name="about" label="About" {...props}>
+        <LabelledField name='about' label='About' {...props}>
           <Field
-            component="textarea"
-            id="about"
-            name="about"
+            component='textarea'
+            id='about'
+            name='about'
             disabled={isSubmitting}
             rows={3}
-            placeholder="Write here anything you would like to share about yourself with Joystream community."
+            placeholder='Write here anything you would like to share about yourself with Joystream community.'
           />
         </LabelledField>
         {!profile && paidTerms && (
@@ -165,24 +169,26 @@ const InnerForm = (props: FormProps) => {
           </Message>
         )}
         <LabelledField invisibleLabel {...props}>
-          <TxButton
-            type="submit"
-            size="large"
-            label={profile ? 'Update my profile' : 'Register'}
-            isDisabled={!dirty || isSubmitting}
-            params={buildTxParams()}
-            tx={profile ? 'members.updateMembership' : 'members.buyMembership'}
-            onClick={onSubmit}
-            txFailedCb={onTxFailed}
-            txSuccessCb={onTxSuccess}
-          />
-          <Button
-            type="button"
-            size="large"
-            disabled={!dirty || isSubmitting}
-            onClick={() => resetForm()}
-            content="Reset form"
-          />
+          <div style={{ display: 'flex' }}>
+            <TxButton
+              type='submit'
+              size='large'
+              label={profile ? 'Update my profile' : 'Register'}
+              isDisabled={!dirty || isSubmitting}
+              params={buildTxParams()}
+              tx={profile ? 'members.updateMembership' : 'members.buyMembership'}
+              onClick={onSubmit}
+              txFailedCb={onTxFailed}
+              txSuccessCb={onTxSuccess}
+            />
+            <Button
+              type='button'
+              size='large'
+              disabled={!dirty || isSubmitting}
+              onClick={() => resetForm()}
+              content='Reset form'
+            />
+          </div>
         </LabelledField>
       </Form>
     </Section>
@@ -191,8 +197,9 @@ const InnerForm = (props: FormProps) => {
 
 const EditForm = withFormik<OuterProps, FormValues>({
   // Transform outer props into form values
-  mapPropsToValues: props => {
+  mapPropsToValues: (props) => {
     const { profile: p } = props;
+
     return {
       handle: p ? p.handle.toString() : '',
       avatar: p ? p.avatar_uri.toString() : '',
@@ -202,7 +209,7 @@ const EditForm = withFormik<OuterProps, FormValues>({
 
   validationSchema: buildSchema,
 
-  handleSubmit: values => {
+  handleSubmit: (values) => {
     // do submitting things
   }
 })(InnerForm);
@@ -220,6 +227,7 @@ type WithMembershipDataProps = {
 
 function WithMembershipDataInner (p: WithMembershipDataProps) {
   const triedToFindProfile = !p.memberId || p.membership;
+
   if (
     triedToFindProfile &&
     p.paidTerms &&
@@ -228,7 +236,9 @@ function WithMembershipDataInner (p: WithMembershipDataProps) {
     p.maxAvatarUriLength &&
     p.maxAboutTextLength
   ) {
-    const membership = p.membership && !p.membership.handle.isEmpty ? p.membership : undefined;
+    const membership = (p.memberId && p.membership && !p.membership.handle.isEmpty)
+      ? p.membership
+      : undefined;
 
     if (!membership && p.paidTerms.isEmpty) {
       console.error('Could not find active paid membership terms');
@@ -267,10 +277,10 @@ type WithMembershipDataWrapperProps = MyAccountProps & {
 function WithMembershipDataWrapperInner (p: WithMembershipDataWrapperProps) {
   if (p.allAccounts && !Object.keys(p.allAccounts).length) {
     return (
-      <Message warning className="JoyMainStatus">
+      <Message warning className='JoyMainStatus'>
         <Message.Header>Please create a key to get started.</Message.Header>
         <div style={{ marginTop: '1rem' }}>
-          <Link to={'/accounts'} className="ui button orange">
+          <Link to={'/accounts'} className='ui button orange'>
             Create key
           </Link>
         </div>
@@ -280,9 +290,7 @@ function WithMembershipDataWrapperInner (p: WithMembershipDataWrapperProps) {
 
   if (p.memberIdsByRootAccountId && p.memberIdsByControllerAccountId && p.paidTermsIds) {
     if (p.paidTermsIds.length) {
-      // let member_ids = p.memberIdsByRootAccountId.slice(); // u8a.subarray is not a function!!
-      p.memberIdsByRootAccountId.concat(p.memberIdsByControllerAccountId);
-      const memberId = p.memberIdsByRootAccountId.length ? p.memberIdsByRootAccountId[0] : undefined;
+      const [memberId] = p.memberIdsByRootAccountId.toArray().concat(p.memberIdsByControllerAccountId.toArray());
 
       return <WithMembershipData memberId={memberId} paidTermsId={p.paidTermsIds[0]} />;
     } else {

+ 11 - 6
pioneer/packages/joy-members/src/List.tsx

@@ -4,13 +4,14 @@ import React from 'react';
 import { ApiProps } from '@polkadot/react-api/types';
 import { I18nProps } from '@polkadot/react-components/types';
 
-import Section from '@polkadot/joy-utils/Section';
+import { Section } from '@polkadot/joy-utils/react/components';
 import translate from './translate';
 import Details from './Details';
 import { MemberId } from '@joystream/types/members';
 import { RouteComponentProps, Redirect } from 'react-router-dom';
 import { Pagination, Icon, PaginationProps } from 'semantic-ui-react';
 import styled from 'styled-components';
+import { withApi } from '@polkadot/react-api';
 
 const StyledPagination = styled(Pagination)`
   border-bottom: 1px solid #ddd !important;
@@ -22,7 +23,7 @@ type Props = ApiProps & I18nProps & RouteComponentProps & {
   match: { params: { page?: string } };
 };
 
-type State = {};
+type State = Record<any, never>;
 
 const MEMBERS_PER_PAGE = 20;
 
@@ -31,7 +32,8 @@ class Component extends React.PureComponent<Props, State> {
 
   onPageChange = (e: React.MouseEvent, data: PaginationProps) => {
     const { history } = this.props;
-    history.push(`/members/list/${data.activePage}`);
+
+    history.push(`/members/list/${data.activePage || 1}`);
   }
 
   renderPagination (currentPage: number, pagesCount: number) {
@@ -55,7 +57,8 @@ class Component extends React.PureComponent<Props, State> {
     const {
       firstMemberId,
       membersCreated,
-      match: { params: { page } }
+      match: { params: { page } },
+      api
     } = this.props;
 
     const membersCount = membersCreated.toNumber();
@@ -67,11 +70,13 @@ class Component extends React.PureComponent<Props, State> {
     }
 
     const ids: MemberId[] = [];
+
     if (membersCount > 0) {
       const firstId = firstMemberId.toNumber() + (currentPage - 1) * MEMBERS_PER_PAGE;
       const lastId = Math.min(firstId + MEMBERS_PER_PAGE, membersCount) - 1;
+
       for (let i = firstId; i <= lastId; i++) {
-        ids.push(new MemberId(i));
+        ids.push(api.createType('MemberId', i));
       }
     }
 
@@ -95,4 +100,4 @@ class Component extends React.PureComponent<Props, State> {
   }
 }
 
-export default translate(Component);
+export default translate(withApi(Component));

+ 8 - 8
pioneer/packages/joy-members/src/MemberPreview.tsx

@@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
 
 import { ApiProps } from '@polkadot/react-api/types';
 import { I18nProps } from '@polkadot/react-components/types';
-import { withCalls, withMulti } from '@polkadot/react-api/with';
+import { withCalls, withMulti } from '@polkadot/react-api/hoc';
 import { Vec } from '@polkadot/types';
 import { AccountId } from '@polkadot/types/interfaces';
 import IdentityIcon from '@polkadot/react-components/IdentityIcon';
@@ -12,9 +12,8 @@ import translate from './translate';
 import { MemberId, Membership } from '@joystream/types/members';
 import { queryMembershipToProp } from './utils';
 import { Seat } from '@joystream/types/council';
-import { nonEmptyStr, queryToProp } from '@polkadot/joy-utils/index';
-import { FlexCenter } from '@polkadot/joy-utils/FlexCenter';
-import { MutedSpan } from '@polkadot/joy-utils/MutedText';
+import { nonEmptyStr, queryToProp } from '@polkadot/joy-utils/functions/misc';
+import { FlexCenter, MutedSpan } from '@polkadot/joy-utils/react/components';
 
 const AvatarSizePx = 36;
 const InlineAvatarSizePx = 24;
@@ -33,6 +32,7 @@ type MemberPreviewProps = ApiProps & I18nProps & {
 class InnerMemberPreview extends React.PureComponent<MemberPreviewProps> {
   render () {
     const { membership } = this.props;
+
     return membership && !membership.handle.isEmpty
       ? this.renderProfile(membership)
       : null;
@@ -43,19 +43,19 @@ class InnerMemberPreview extends React.PureComponent<MemberPreviewProps> {
     const { handle, avatar_uri } = membership;
 
     const hasAvatar = avatar_uri && nonEmptyStr(avatar_uri.toString());
-    const isCouncilor: boolean = accountId !== undefined && activeCouncil.find(x => accountId.eq(x.member)) !== undefined;
+    const isCouncilor: boolean = accountId !== undefined && activeCouncil.find((x) => accountId.eq(x.member)) !== undefined;
 
     const avatarSize = inline ? InlineAvatarSizePx : AvatarSizePx;
 
-    return <div className={`JoyMemberPreview ${className}`} style={style}>
+    return <div className={`JoyMemberPreview ${className || ''}`} style={style}>
       <FlexCenter>
         {prefixLabel &&
           <MutedSpan className='PrefixLabel'>{prefixLabel}</MutedSpan>
         }
         {hasAvatar ? (
-          <img className="Avatar" src={avatar_uri.toString()} width={avatarSize} height={avatarSize} />
+          <img className='Avatar' src={avatar_uri.toString()} width={avatarSize} height={avatarSize} />
         ) : (
-          <IdentityIcon className="Avatar" value={accountId} size={avatarSize} />
+          <IdentityIcon className='Avatar' value={accountId} size={avatarSize} />
         )
         }
         <div className='Content'>

+ 0 - 58
pioneer/packages/joy-members/src/index.css

@@ -1,58 +0,0 @@
-.ProfilePreviews,
-.FullProfile {
-  .item {
-    .image {
-      padding: 0 !important;
-    }
-    .description {
-      font-size: 1rem;
-    }
-  }
-}
-.ProfilePreviews {
-  &.ui.list>.item:first-child {
-    padding-top: .75rem;
-  }
-  &.ui.list>.item:last-child {
-    padding-bottom: .75rem;
-  }
-  .MyProfile {
-    background-color: #FFF8E1;
-  }
-}
-.ProfileDetails {
-  padding-left: 1rem !important;
-  .handle {
-    margin-right: 1rem;
-    .button {
-      padding: .5rem .75rem;
-    }
-  }
-}
-.ProfileDetailsTable {
-  font-size: 1rem !important;
-  tr td:first-child {
-    width: 1%;
-    white-space: nowrap;
-  }
-}
-
-.JoyMemberPreview {
-  margin-right: .5rem;
-  .PrefixLabel {
-    margin-right: .5rem;
-  }
-  .Avatar {
-    margin-right: .5rem;
-    border-radius: 100%;
-  }
-  .Content {
-    .Username {
-      font-weight: bold;
-    }
-    .Details {
-      font-weight: 100;
-      opacity: .75;
-    }
-  }
-}

+ 16 - 11
pioneer/packages/joy-members/src/index.tsx

@@ -5,10 +5,9 @@ import { Route, Switch } from 'react-router';
 
 import { AppProps, I18nProps } from '@polkadot/react-components/types';
 import { ApiProps } from '@polkadot/react-api/types';
-import { withCalls, withMulti } from '@polkadot/react-api/with';
-import Tabs, { TabItem } from '@polkadot/react-components/Tabs';
-
-import './index.css';
+import { withCalls, withMulti } from '@polkadot/react-api/hoc';
+import Tabs from '@polkadot/react-components/Tabs';
+import { TabItem } from '@polkadot/react-components/Tabs/types';
 
 import { queryMembershipToProp } from './utils';
 import translate from './translate';
@@ -16,10 +15,15 @@ import Dashboard from './Dashboard';
 import List from './List';
 import DetailsByHandle from './DetailsByHandle';
 import EditForm from './EditForm';
-import { withMyAccount, MyAccountProps } from '@polkadot/joy-utils/MyAccount';
+import { withMyAccount, MyAccountProps } from '@polkadot/joy-utils/react/hocs/accounts';
 import { FIRST_MEMBER_ID } from './constants';
 import { RouteComponentProps } from 'react-router-dom';
 
+import styled from 'styled-components';
+import style from './style';
+
+const MembersMain = styled.main`${style}`;
+
 // define out internal types
 type Props = AppProps & ApiProps & I18nProps & MyAccountProps & {
   nextMemberId?: BN;
@@ -32,8 +36,8 @@ class App extends React.PureComponent<Props> {
     return [
       {
         name: 'list',
-        text: t('All members') + ` (${memberCount})`,
-        forcedExact: false
+        text: t('All members') + ` (${memberCount?.toString() || '-'})`,
+        forceMatchParams: true
       },
       {
         name: 'edit',
@@ -48,6 +52,7 @@ class App extends React.PureComponent<Props> {
 
   private renderList (routeProps: RouteComponentProps) {
     const { nextMemberId, ...otherProps } = this.props;
+
     return nextMemberId
       ? <List firstMemberId={FIRST_MEMBER_ID} membersCreated={nextMemberId} {...otherProps} {...routeProps}/>
       : <em>Loading...</em>;
@@ -58,18 +63,18 @@ class App extends React.PureComponent<Props> {
     const tabs = this.buildTabs();
 
     return (
-      <main className='members--App'>
+      <MembersMain className='members--App'>
         <header>
           <Tabs basePath={basePath} items={tabs} />
         </header>
         <Switch>
           <Route path={`${basePath}/edit`} component={EditForm} />
           <Route path={`${basePath}/dashboard`} component={Dashboard} />
-          <Route path={`${basePath}/list/:page([0-9]+)?`} render={ props => this.renderList(props) } />
+          <Route path={`${basePath}/list/:page([0-9]+)?`} render={ (props) => this.renderList(props) } />
           <Route exact={true} path={`${basePath}/:handle`} component={DetailsByHandle} />
-          <Route render={ props => this.renderList(props) } />
+          <Route render={ (props) => this.renderList(props) } />
         </Switch>
-      </main>
+      </MembersMain>
     );
   }
 }

+ 62 - 0
pioneer/packages/joy-members/src/style.ts

@@ -0,0 +1,62 @@
+import { css } from 'styled-components';
+
+export default css`
+  .ProfilePreviews,
+  .FullProfile {
+    .item {
+      .image {
+        padding: 0 !important;
+      }
+      .description {
+        font-size: 1rem;
+      }
+    }
+  }
+  .ProfilePreviews {
+    &.ui.list>.item:first-child {
+      padding-top: .75rem;
+    }
+    &.ui.list>.item:last-child {
+      padding-bottom: .75rem;
+    }
+    .MyProfile {
+      background-color: #FFF8E1;
+    }
+  }
+  .ProfileDetails {
+    padding-left: 1rem !important;
+    .handle {
+      margin-right: 1rem;
+      .button {
+        padding: .5rem .75rem;
+      }
+    }
+  }
+  .ProfileDetailsTable {
+    font-size: 1rem !important;
+    tr td:first-child {
+      width: 1%;
+      white-space: nowrap;
+    }
+  }
+
+  .JoyMemberPreview {
+    margin-right: .5rem;
+    .PrefixLabel {
+      margin-right: .5rem;
+    }
+    .Avatar {
+      margin-right: .5rem;
+      border-radius: 100%;
+    }
+    .Content {
+      .Username {
+        font-weight: bold;
+      }
+      .Details {
+        font-weight: 100;
+        opacity: .75;
+      }
+    }
+  }
+`;

+ 3 - 2
pioneer/packages/joy-members/src/utils.ts

@@ -1,5 +1,6 @@
-import { queryToProp } from '@polkadot/joy-utils/index';
-import { Options as QueryOptions } from '@polkadot/react-api/with/types';
+// TODO: Move to joy-utils?
+import { queryToProp } from '@polkadot/joy-utils/functions/misc';
+import { Options as QueryOptions } from '@polkadot/react-api/hoc/types';
 
 export const queryMembershipToProp = (storageItem: string, paramNameOrOpts?: string | QueryOptions) => {
   return queryToProp(`query.members.${storageItem}`, paramNameOrOpts);

+ 0 - 0
pioneer/packages/joy-pages/.skip-build


+ 3 - 3
pioneer/packages/joy-pages/package.json

@@ -7,9 +7,9 @@
   "author": "Joystream contributors",
   "maintainers": [],
   "dependencies": {
-    "@babel/runtime": "^7.7.1",
-    "@polkadot/react-components": "0.37.0-beta.63",
-    "@polkadot/react-query": "0.37.0-beta.63",
+    "@babel/runtime": "^7.10.5",
+    "@polkadot/react-components": "0.51.1",
+    "@polkadot/react-query": "0.51.1",
     "@polkadot/joy-utils": "^0.1.1"
   }
 }

+ 4 - 2
pioneer/packages/joy-pages/src/index.tsx

@@ -4,9 +4,11 @@ import Page from './Page';
 import ToS_md from './md/ToS.md';
 
 import Privacy_md from './md/Privacy.md';
+
 export function ToS () {
-  return <Page md={ToS_md} />;
+  return <Page md={ToS_md as string} />;
 }
+
 export function Privacy () {
-  return <Page md={Privacy_md} />;
+  return <Page md={Privacy_md as string} />;
 }

+ 0 - 14
pioneer/packages/joy-utils-old/src/functions/misc.ts

@@ -1,14 +0,0 @@
-import { Bytes } from '@polkadot/types/primitive';
-
-export function includeKeys<T extends { [k: string]: any }> (obj: T, ...allowedKeys: string[]) {
-  return Object.keys(obj).filter(objKey => {
-    return allowedKeys.reduce(
-      (hasAllowed: boolean, allowedKey: string) => hasAllowed || objKey.includes(allowedKey),
-      false
-    );
-  });
-}
-
-export function bytesToString (bytes: Bytes) {
-  return Buffer.from(bytes.toString().substr(2), 'hex').toString();
-}

+ 0 - 1
pioneer/packages/joy-utils-old/src/react/components/index.tsx

@@ -1 +0,0 @@
-export { default as PromiseComponent } from './PromiseComponent';

+ 0 - 1
pioneer/packages/joy-utils-old/src/react/context/index.tsx

@@ -1 +0,0 @@
-export { TransportContext, TransportProvider } from './transport';

+ 0 - 0
pioneer/packages/joy-utils-old/README.md → pioneer/packages/joy-utils/README.md


+ 3 - 3
pioneer/packages/joy-utils-old/package.json → pioneer/packages/joy-utils/package.json

@@ -7,9 +7,9 @@
   "author": "Joystream contributors",
   "maintainers": [],
   "dependencies": {
-    "@babel/runtime": "^7.7.1",
-    "@polkadot/react-components": "0.37.0-beta.63",
-    "@polkadot/react-query": "0.37.0-beta.63",
+    "@babel/runtime": "^7.10.5",
+    "@polkadot/react-components": "0.51.1",
+    "@polkadot/react-query": "0.51.1",
     "@types/query-string": "^6.2.0",
     "@types/uuid": "^3.4.4",
     "@types/yup": "^0.26.10",

+ 0 - 0
pioneer/packages/joy-utils-old/src/functions/date.ts → pioneer/packages/joy-utils/src/functions/date.ts


+ 2 - 2
pioneer/packages/joy-utils-old/src/functions/format.ts → pioneer/packages/joy-utils/src/functions/format.ts

@@ -16,7 +16,7 @@ export const formatReward = (
     : next_payment_at_block;
 
   return (
-    `${formatBalance(amount)}${interval.isSome ? ` / ${interval.unwrap()} block(s)` : ''}` +
-    ((showNextPaymentBlock && nextPaymentBlock) ? ` (Next payment: #${nextPaymentBlock})` : '')
+    `${formatBalance(amount)}${interval.isSome ? ` / ${interval.unwrap().toString()} block(s)` : ''}` +
+    ((showNextPaymentBlock && nextPaymentBlock) ? ` (Next payment: #${nextPaymentBlock.toString()})` : '')
   );
 };

+ 167 - 0
pioneer/packages/joy-utils/src/functions/misc.ts

@@ -0,0 +1,167 @@
+import { Bytes } from '@polkadot/types/primitive';
+import BN from 'bn.js';
+import keyring from '@polkadot/ui-keyring';
+import { ElectionStake, Backer } from '@joystream/types/council';
+import { Options as QueryOptions } from '@polkadot/react-api/hoc/types';
+import queryString from 'query-string';
+import { SubmittableResult } from '@polkadot/api';
+import { Codec } from '@polkadot/types/types';
+
+export const ZERO = new BN(0);
+
+export function bnToStr (bn?: BN, dflt = ''): string {
+  return bn ? bn.toString() : dflt;
+}
+
+export const notDefined = (x: any): boolean =>
+  x === null || typeof x === 'undefined';
+
+export const isDefined = (x: any): boolean =>
+  !notDefined(x);
+
+export const isDef = isDefined;
+
+export const notDef = notDefined;
+
+export const isObj = (x: any): boolean =>
+  x !== null && typeof x === 'object';
+
+export const isStr = (x: any): boolean =>
+  typeof x === 'string';
+
+export const isNum = (x: any): boolean =>
+  typeof x === 'number';
+
+export const isEmptyStr = (x: any): boolean =>
+  notDefined(x) || (isStr(x) && (x as string).trim().length === 0);
+
+export const nonEmptyStr = (x?: any) =>
+  isStr(x) && (x as string).trim().length > 0;
+
+export const parseNumStr = (num: string): number | undefined => {
+  try {
+    return parseInt(num, undefined);
+  } catch (err) {
+    return undefined;
+  }
+};
+
+export const nonEmptyArr = (x: any): boolean =>
+  Array.isArray(x) && x.length > 0;
+
+export const isEmptyArr = (x: any): boolean =>
+  !nonEmptyArr(x);
+
+export function findNameByAddress (address: string): string | undefined {
+  let keyring_address;
+
+  try {
+    keyring_address = keyring.getAccount(address);
+  } catch (error) {
+    try {
+      keyring_address = keyring.getAddress(address);
+    } catch (error) {
+      // do nothing
+    }
+  }
+
+  return keyring_address ? keyring_address.meta.name : undefined;
+}
+
+export function isKnownAddress (address: string): boolean {
+  return isDefined(findNameByAddress(address));
+}
+
+export function calcTotalStake (stakes: ElectionStake | ElectionStake[] | undefined): BN {
+  if (typeof stakes === 'undefined') {
+    return ZERO;
+  }
+
+  const total = (stake: ElectionStake) => stake.new.add(stake.transferred);
+
+  try {
+    if (Array.isArray(stakes)) {
+      return stakes.reduce((accum, stake) => {
+        return accum.add(total(stake));
+      }, ZERO);
+    } else {
+      return total(stakes);
+    }
+  } catch (err) {
+    console.log('Failed to calculate a total stake', stakes, err);
+
+    return ZERO;
+  }
+}
+
+export function calcBackersStake (backers: Backer[]): BN {
+  return backers.map((b) => b.stake).reduce((accum, stake) => {
+    return accum.add(stake);
+  }, ZERO);
+}
+
+/** Example of apiQuery: 'query.councilElection.round' */
+export function queryToProp (
+  apiQuery: string,
+  paramNameOrOpts?: string | QueryOptions
+): [string, QueryOptions] {
+  let paramName: string | undefined;
+  let propName: string | undefined;
+
+  if (typeof paramNameOrOpts === 'string') {
+    paramName = paramNameOrOpts;
+  } else if (paramNameOrOpts) {
+    paramName = paramNameOrOpts.paramName;
+    propName = paramNameOrOpts.propName;
+  }
+
+  // If prop name is still undefined, derive it from the name of storage item:
+  if (!propName) {
+    propName = apiQuery.split('.').slice(-1)[0];
+  }
+
+  return [apiQuery, { paramName, propName }];
+}
+
+export function getUrlParam (location: Location, paramName: string, deflt: string | null = null): string | null {
+  const params = queryString.parse(location.search);
+
+  return params[paramName] ? params[paramName] as string : deflt;
+}
+
+export function filterSubstrateEventsAndExtractData (txResult: SubmittableResult, eventName: string): Codec[][] {
+  const res: Codec[][] = [];
+
+  txResult.events.forEach((event) => {
+    const { event: { method, data } } = event;
+
+    if (method === eventName) {
+      res.push(data.toArray());
+    }
+  });
+
+  return res;
+}
+
+export function findFirstParamOfSubstrateEvent<T extends Codec> (txResult: SubmittableResult, eventName: string): T | undefined {
+  const data = filterSubstrateEventsAndExtractData(txResult, eventName);
+
+  if (data && data.length) {
+    return data[0][0] as T;
+  }
+
+  return undefined;
+}
+
+export function includeKeys<T extends { [k: string]: any }> (obj: T, ...allowedKeys: string[]) {
+  return Object.keys(obj).filter((objKey) => {
+    return allowedKeys.reduce(
+      (hasAllowed: boolean, allowedKey: string) => hasAllowed || objKey.includes(allowedKey),
+      false
+    );
+  });
+}
+
+export function bytesToString (bytes: Bytes) {
+  return Buffer.from(bytes.toString().substr(2), 'hex').toString();
+}

+ 1 - 1
pioneer/packages/joy-utils-old/src/FlexCenter.tsx → pioneer/packages/joy-utils/src/react/components/FlexCenter.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
 
-export function FlexCenter (props: React.PropsWithChildren<{}>) {
+export function FlexCenter (props: React.PropsWithChildren<unknown>) {
   return <div className='FlexCenter'>{props.children}</div>;
 }

+ 4 - 1
pioneer/packages/joy-utils-old/src/MutedText.tsx → pioneer/packages/joy-utils/src/react/components/MutedText.tsx

@@ -8,15 +8,18 @@ type Props = React.PropsWithChildren<{
 
 function getClassNames (props: Props): string {
   const { smaller = false, className } = props;
-  return `grey text ${smaller ? 'smaller' : ''} ${className}`;
+
+  return `grey text ${smaller ? 'smaller' : ''} ${className || ''}`;
 }
 
 export const MutedSpan = (props: Props) => {
   const { style, children } = props;
+
   return <span className={getClassNames(props)} style={style}>{children}</span>;
 };
 
 export const MutedDiv = (props: Props) => {
   const { style, children } = props;
+
   return <div className={getClassNames(props)} style={style}>{children}</div>;
 };

+ 3 - 0
pioneer/packages/joy-utils-old/src/Section.tsx → pioneer/packages/joy-utils/src/react/components/Section.tsx

@@ -46,6 +46,7 @@ type Props = BareProps & {
 export default class Section extends React.PureComponent<Props> {
   render () {
     let { className, children, pagination } = this.props;
+
     className = (className || '') + ' JoySection';
 
     return (
@@ -62,10 +63,12 @@ export default class Section extends React.PureComponent<Props> {
 
   private renderTitle = () => {
     const { title, level = 2, pagination } = this.props;
+
     if (!title) return null;
 
     const className = 'JoySection-title';
     const style = pagination ? { margin: '0' } : {};
+
     return React.createElement(
       `h${level}`,
       { className, style },

+ 46 - 50
pioneer/packages/joy-utils-old/src/TxButton.tsx → pioneer/packages/joy-utils/src/react/components/TxButton.tsx

@@ -5,11 +5,8 @@ import { Button } from '@polkadot/react-components/index';
 import { QueueConsumer } from '@polkadot/react-components/Status/Context';
 import { withApi } from '@polkadot/react-api/index';
 import { assert } from '@polkadot/util';
-import { withMyAccount, MyAccountProps } from '@polkadot/joy-utils/MyAccount';
-import { useTransportContext } from '@polkadot/joy-media/TransportContext';
-import { MockTransport } from '@polkadot/joy-media/transport.mock';
-import { Button$Sizes } from '@polkadot/react-components/Button/types';
-import { SemanticShorthandItem, IconProps } from 'semantic-ui-react';
+import { withMyAccount, MyAccountProps } from '../hocs/accounts';
+import { IconName } from '@fortawesome/fontawesome-svg-core';
 
 type InjectedProps = {
   queueExtrinsic: QueueTxExtrinsicAdd;
@@ -20,9 +17,7 @@ export type OnTxButtonClick = (sendTx: () => void) => void;
 type BasicButtonProps = {
   accountId?: string;
   type?: 'submit' | 'button';
-  size?: Button$Sizes;
   isBasic?: boolean;
-  isPrimary?: boolean;
   isDisabled?: boolean;
   label?: React.ReactNode;
   params: Array<any>;
@@ -32,7 +27,7 @@ type BasicButtonProps = {
   style?: Record<string, string | number>;
   children?: React.ReactNode;
   compact?: boolean;
-  icon?: boolean | SemanticShorthandItem<IconProps>;
+  icon?: IconName;
 
   onClick?: OnTxButtonClick;
   txFailedCb?: TxFailedCallback;
@@ -45,15 +40,14 @@ type PropsWithApi = BareProps & ApiProps & MyAccountProps & PartialQueueTxExtrin
 
 class TxButtonInner extends React.PureComponent<PropsWithApi & InjectedProps> {
   render () {
-    const { myAddress, accountId, isPrimary = true, isDisabled, icon = '', onClick } = this.props;
+    const { myAddress, accountId, isDisabled, icon = 'check', onClick } = this.props;
     const origin = accountId || myAddress;
 
     return (
       <Button
         {...this.props}
         isDisabled={isDisabled || !origin}
-        isPrimary={isPrimary}
-        icon={icon as string}
+        icon={icon}
         onClick={() => {
           if (onClick) onClick(this.send);
           else this.send();
@@ -74,7 +68,7 @@ class TxButtonInner extends React.PureComponent<PropsWithApi & InjectedProps> {
 
     queueExtrinsic({
       accountId: origin,
-      extrinsic: api.tx[section][method](...params) as any, // ???
+      extrinsic: api.tx[section][method](...params),
       txFailedCb,
       txSuccessCb,
       txStartCb,
@@ -98,41 +92,43 @@ class TxButton extends React.PureComponent<PropsWithApi> {
   }
 }
 
-const SubstrateTxButton = withApi(withMyAccount(TxButton));
-
-const mockSendTx = () => {
-  const msg = 'Cannot send a Substrate tx in a mock mode';
-  if (typeof window !== 'undefined') {
-    window.alert(`WARN: ${msg}`);
-  } else if (typeof console.warn === 'function') {
-    console.warn(msg);
-  } else {
-    console.log(`WARN: ${msg}`);
-  }
-};
-
-function MockTxButton (props: BasicButtonProps) {
-  const { isPrimary = true, icon = '', onClick } = props;
-
-  return (
-    <Button
-      {...props}
-      isPrimary={isPrimary}
-      icon={icon as string}
-      onClick={() => {
-        if (onClick) onClick(mockSendTx);
-        else mockSendTx();
-      }}
-    />
-  );
-}
-
-function ResolvedButton (props: BasicButtonProps) {
-  const isMock = useTransportContext() instanceof MockTransport;
-
-  return isMock
-    ? <MockTxButton {...props} />
-    : <SubstrateTxButton {...props} />;
-}
-
-export default ResolvedButton;
+export default withApi(withMyAccount(TxButton));
+
+// const SubstrateTxButton = withApi(withMyAccount(TxButton));
+
+// const mockSendTx = () => {
+//   const msg = 'Cannot send a Substrate tx in a mock mode';
+//   if (typeof window !== 'undefined') {
+//     window.alert(`WARN: ${msg}`);
+//   } else if (typeof console.warn === 'function') {
+//     console.warn(msg);
+//   } else {
+//     console.log(`WARN: ${msg}`);
+//   }
+// };
+
+// function MockTxButton (props: BasicButtonProps) {
+//   const { isPrimary = true, icon = '', onClick } = props;
+
+//   return (
+//     <Button
+//       {...props}
+//       isPrimary={isPrimary}
+//       icon={icon as string}
+//       onClick={() => {
+//         if (onClick) onClick(mockSendTx);
+//         else mockSendTx();
+//       }}
+//     />
+//   );
+// }
+
+// function ResolvedButton (props: BasicButtonProps) {
+//   const isMock = useTransportContext() instanceof MockTransport;
+
+//   return isMock
+//     ? <MockTxButton {...props} />
+//     : <SubstrateTxButton {...props} />;
+// }
+
+// export default ResolvedButton;

+ 5 - 5
pioneer/packages/joy-utils-old/src/forms.tsx → pioneer/packages/joy-utils/src/react/components/forms.tsx

@@ -1,7 +1,6 @@
 import React from 'react';
 import { Field, FormikErrors, FormikTouched } from 'formik';
-
-import { nonEmptyStr } from '@polkadot/joy-utils/index';
+import { nonEmptyStr } from '../../functions/misc';
 import { Popup, Icon } from 'semantic-ui-react';
 
 export type LabelledProps<FormValues> = {
@@ -38,9 +37,9 @@ export function LabelledField<FormValues> () {
       const renderLabel = () =>
         nonEmptyStr(label)
           ? <>
-              {required && <b style={{ color: 'red' }} title='This field is required'>* </b>}
-              {label}
-            </>
+            {required && <b style={{ color: 'red' }} title='This field is required'>* </b>}
+            {label}
+          </>
           : null;
 
       return (label || invisibleLabel)
@@ -57,6 +56,7 @@ export function LabelledField<FormValues> () {
           {fieldWithError}
         </div>;
     };
+
   return LabelledFieldInner;
 }
 

+ 4 - 0
pioneer/packages/joy-utils/src/react/components/index.tsx

@@ -0,0 +1,4 @@
+export { default as Section } from './Section';
+export { default as TxButton } from './TxButton';
+export { MutedSpan, MutedDiv } from './MutedText';
+export { FlexCenter } from './FlexCenter';

+ 30 - 10
pioneer/packages/joy-utils-old/src/MyAccountContext.tsx → pioneer/packages/joy-utils/src/react/context/account.tsx

@@ -1,11 +1,14 @@
-import React, { useReducer, createContext, useContext, useEffect } from 'react';
+import React, { useReducer, createContext, useEffect } from 'react';
 import store from 'store';
 
-export const MY_ADDRESS = 'joy.myAddress';
+export const ACCOUNT_CHANGED_EVENT_NAME = 'account-changed';
+export const MY_ADDRESS_STORAGE_KEY = 'joy.myAddress';
 
 function readMyAddress (): string | undefined {
-  const myAddress: string | undefined = store.get(MY_ADDRESS);
+  const myAddress = store.get(MY_ADDRESS_STORAGE_KEY) as string | undefined;
+
   console.log('Read my address from the local storage:', myAddress);
+
   return myAddress;
 }
 
@@ -22,7 +25,8 @@ type MyAccountAction = {
 function reducer (state: MyAccountState, action: MyAccountAction): MyAccountState {
   function forget () {
     console.log('Forget my address');
-    store.remove(MY_ADDRESS);
+    store.remove(MY_ADDRESS_STORAGE_KEY);
+
     return { ...state, address: undefined };
   }
 
@@ -32,29 +36,35 @@ function reducer (state: MyAccountState, action: MyAccountAction): MyAccountStat
     case 'reload': {
       address = readMyAddress();
       console.log('Reload my address:', address);
+
       return { ...state, address, inited: true };
     }
 
     case 'set': {
       address = action.address;
+
       if (address !== state.address) {
         if (address) {
           console.log('Set my new address:', address);
-          store.set(MY_ADDRESS, address);
+          store.set(MY_ADDRESS_STORAGE_KEY, address);
+
           return { ...state, address, inited: true };
         } else {
           return forget();
         }
       }
+
       return state;
     }
 
     case 'forget': {
       address = action.address;
       const isMyAddress = address && address === readMyAddress();
+
       if (!address || isMyAddress) {
         return forget();
       }
+
       return state;
     }
 
@@ -88,9 +98,23 @@ const contextStub: MyAccountContextProps = {
 
 export const MyAccountContext = createContext<MyAccountContextProps>(contextStub);
 
-export function MyAccountProvider (props: React.PropsWithChildren<{}>) {
+export function MyAccountProvider (props: React.PropsWithChildren<unknown>) {
   const [state, dispatch] = useReducer(reducer, initialState);
 
+  const handleAccountChangeEvent = (e: Event) => {
+    const { detail: address } = e as CustomEvent<string>;
+
+    dispatch({ type: 'set', address });
+  };
+
+  useEffect(() => {
+    window.addEventListener(ACCOUNT_CHANGED_EVENT_NAME, handleAccountChangeEvent);
+
+    return () => {
+      window.removeEventListener(ACCOUNT_CHANGED_EVENT_NAME, handleAccountChangeEvent);
+    };
+  });
+
   useEffect(() => {
     if (!state.inited) {
       dispatch({ type: 'reload' });
@@ -110,7 +134,3 @@ export function MyAccountProvider (props: React.PropsWithChildren<{}>) {
     </MyAccountContext.Provider>
   );
 }
-
-export function useMyAccount () {
-  return useContext(MyAccountContext);
-}

+ 2 - 0
pioneer/packages/joy-utils/src/react/context/index.tsx

@@ -0,0 +1,2 @@
+export { MyAccountContext, MyAccountProvider } from './account';
+export { MyMembershipContext, MyMembershipProvider } from './membership';

+ 2 - 6
pioneer/packages/joy-utils-old/src/MyMembershipContext.tsx → pioneer/packages/joy-utils/src/react/context/membership.tsx

@@ -1,5 +1,5 @@
-import React, { createContext, useContext } from 'react';
-import { MyAccountProps, withMyAccount } from './MyAccount';
+import React, { createContext } from 'react';
+import { MyAccountProps, withMyAccount } from '../hocs/accounts';
 
 export const MyMembershipContext = createContext<MyAccountProps>({});
 
@@ -12,7 +12,3 @@ function InnerMyMembershipProvider (props: React.PropsWithChildren<MyAccountProp
 }
 
 export const MyMembershipProvider = withMyAccount(InnerMyMembershipProvider);
-
-export function useMyMembership () {
-  return useContext(MyMembershipContext);
-}

+ 0 - 0
pioneer/packages/joy-utils-old/src/react/helpers/index.ts → pioneer/packages/joy-utils/src/react/helpers/index.ts


+ 124 - 0
pioneer/packages/joy-utils/src/react/hocs/accounts.tsx

@@ -0,0 +1,124 @@
+import React, { useContext } from 'react';
+
+import { AccountId } from '@polkadot/types/interfaces';
+import { Vec, Option } from '@polkadot/types';
+import accountObservable from '@polkadot/ui-keyring/observable/accounts';
+import { withCalls, withMulti, withObservable, ApiContext } from '@polkadot/react-api/index';
+import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
+
+import { MemberId, Membership } from '@joystream/types/members';
+import { LeadId } from '@joystream/types/content-working-group';
+
+import { queryMembershipToProp } from '@polkadot/joy-members/utils';
+import useMyAccount from '../hooks/useMyAccount';
+import { componentName } from '../helpers';
+
+export type MyAddressProps = {
+  myAddress?: string;
+};
+
+export type MyAccountProps = MyAddressProps & {
+  myAccountId?: AccountId;
+  myMemberId?: MemberId;
+  memberIdsByRootAccountId?: Vec<MemberId>;
+  memberIdsByControllerAccountId?: Vec<MemberId>;
+  myMemberIdChecked?: boolean;
+  iAmMember?: boolean;
+  myMembership?: Membership | null;
+
+  // Content Working Group
+  curatorEntries?: any; // entire linked_map: CuratorId => Curator
+  isLeadSet?: Option<LeadId>;
+  contentLeadId?: LeadId;
+  contentLeadEntry?: any; // linked_map value
+
+  curationActor?: any;
+  allAccounts?: SubjectInfo;
+};
+
+function withMyAddress<P extends MyAccountProps> (Component: React.ComponentType<P>) {
+  const ResultComponent: React.FunctionComponent<P> = (props: P) => {
+    const {
+      state: { address }
+    } = useMyAccount();
+    const { api } = useContext(ApiContext);
+    const myAccountId = (address && api.isReady)
+      ? api.createType('AccountId', address)
+      : undefined;
+
+    return <Component myAddress={address} myAccountId={myAccountId} {...props} />;
+  };
+
+  ResultComponent.displayName = `withMyAddress(${componentName(Component)})`;
+
+  return ResultComponent;
+}
+
+const withMyMemberIds = withCalls<MyAccountProps>(
+  queryMembershipToProp('memberIdsByRootAccountId', 'myAddress'),
+  queryMembershipToProp('memberIdsByControllerAccountId', 'myAddress')
+);
+
+function withMyMembership<P extends MyAccountProps> (Component: React.ComponentType<P>) {
+  const ResultComponent: React.FunctionComponent<P> = (props: P) => {
+    const { memberIdsByRootAccountId, memberIdsByControllerAccountId } = props;
+
+    const myMemberIdChecked = memberIdsByRootAccountId && memberIdsByControllerAccountId;
+
+    let myMemberId: MemberId | undefined;
+
+    if (memberIdsByRootAccountId && memberIdsByControllerAccountId) {
+      const [memberIdByAccount] = memberIdsByRootAccountId.toArray().concat(memberIdsByControllerAccountId.toArray());
+
+      myMemberId = memberIdByAccount;
+    }
+
+    const iAmMember = myMemberId !== undefined;
+
+    const newProps = {
+      myMemberIdChecked,
+      myMemberId,
+      iAmMember
+    };
+
+    return <Component {...props} {...newProps} />;
+  };
+
+  ResultComponent.displayName = `withMyMembership(${componentName(Component)})`;
+
+  return ResultComponent;
+}
+
+function resolveMyProfile<P extends { myMembership?: Membership | null }> (Component: React.ComponentType<P>) {
+  const ResultComponent: React.FunctionComponent<P> = (props: P) => {
+    let { myMembership } = props;
+
+    myMembership = (!myMembership || myMembership.handle.isEmpty) ? null : myMembership;
+
+    return <Component {...props} myMembership={ myMembership } />;
+  };
+
+  ResultComponent.displayName = `resolveMyProfile(${componentName(Component)})`;
+
+  return ResultComponent;
+}
+
+const withMyProfileCall = withCalls<MyAccountProps>(queryMembershipToProp('membershipById', {
+  paramName: 'myMemberId',
+  propName: 'myMembership'
+}));
+
+const withMyProfile = <P extends MyAccountProps>(Component: React.ComponentType<P>) =>
+  withMulti(Component, withMyProfileCall, resolveMyProfile);
+
+export const withMyAccount = <P extends MyAccountProps>(Component: React.ComponentType<P>) =>
+  withMulti(
+    Component,
+    withObservable(accountObservable.subject, { propName: 'allAccounts' }),
+    withMyAddress,
+    withMyMemberIds,
+    withMyMembership,
+    withMyProfile
+    // withContentWorkingGroup,
+    // withCurationActor
+  );

+ 104 - 0
pioneer/packages/joy-utils/src/react/hocs/guards.tsx

@@ -0,0 +1,104 @@
+import React from 'react';
+import { Message } from 'semantic-ui-react';
+import { Link } from 'react-router-dom';
+
+import { AccountId } from '@polkadot/types/interfaces';
+import { Vec, Option } from '@polkadot/types';
+import { withMulti } from '@polkadot/react-api/index';
+import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
+
+import { MemberId, Membership } from '@joystream/types/members';
+import { LeadId } from '@joystream/types/content-working-group';
+import { useMyMembership } from '../hooks';
+import { componentName } from '../helpers';
+import { withMyAccount } from './accounts';
+
+export type MyAddressProps = {
+  myAddress?: string;
+};
+
+export type MyAccountProps = MyAddressProps & {
+  myAccountId?: AccountId;
+  myMemberId?: MemberId;
+  memberIdsByRootAccountId?: Vec<MemberId>;
+  memberIdsByControllerAccountId?: Vec<MemberId>;
+  myMemberIdChecked?: boolean;
+  iAmMember?: boolean;
+  myMembership?: Membership | null;
+
+  // Content Working Group
+  curatorEntries?: any; // entire linked_map: CuratorId => Curator
+  isLeadSet?: Option<LeadId>;
+  contentLeadId?: LeadId;
+  contentLeadEntry?: any; // linked_map value
+
+  curationActor?: any;
+  allAccounts?: SubjectInfo;
+};
+
+export function MembershipRequired<P extends Record<string, unknown>> (Component: React.ComponentType<P>): React.ComponentType<P> {
+  const ResultComponent: React.FunctionComponent<P> = (props: P) => {
+    const { myMemberIdChecked, iAmMember } = useMyMembership();
+
+    if (!myMemberIdChecked) {
+      return <em>Loading...</em>;
+    } else if (iAmMember) {
+      return <Component {...props} />;
+    }
+
+    return (
+      <Message warning className='JoyMainStatus'>
+        <Message.Header>Only members can access this functionality.</Message.Header>
+        <div style={{ marginTop: '1rem' }}>
+          <Link to={'/members/edit'} className='ui button orange'>
+            Register here
+          </Link>
+          <span style={{ margin: '0 .5rem' }}> or </span>
+          <Link to={'/accounts'} className='ui button'>
+            Change key
+          </Link>
+        </div>
+      </Message>
+    );
+  };
+
+  ResultComponent.displayName = `MembershipRequired(${componentName(Component)})`;
+
+  return ResultComponent;
+}
+
+export function AccountRequired<P extends Record<string, unknown>> (Component: React.ComponentType<P>): React.ComponentType<P> {
+  const ResultComponent: React.FunctionComponent<P> = (props: P) => {
+    const { allAccounts } = useMyMembership();
+
+    if (allAccounts && !Object.keys(allAccounts).length) {
+      return (
+        <Message warning className='JoyMainStatus'>
+          <Message.Header>Please create a key to get started.</Message.Header>
+          <div style={{ marginTop: '1rem' }}>
+            <Link to={'/accounts'} className='ui button orange'>
+              Create key
+            </Link>
+          </div>
+        </Message>
+      );
+    }
+
+    return <Component {...props} />;
+  };
+
+  ResultComponent.displayName = `AccountRequired(${componentName(Component)})`;
+
+  return ResultComponent;
+}
+
+// TODO: We could probably use withAccountRequired, which wouldn't pass any addiotional props, just like withMembershipRequired.
+// Just need to make sure those passed props are not used in the extended components (they probably aren't).
+export const withOnlyAccounts = <P extends MyAccountProps>(Component: React.ComponentType<P>): React.ComponentType<P> =>
+  withMulti(Component, withMyAccount, AccountRequired);
+
+export const withMembershipRequired = <P extends Record<string, unknown>> (Component: React.ComponentType<P>): React.ComponentType<P> =>
+  withMulti(Component, AccountRequired, MembershipRequired);
+
+export const withOnlyMembers = <P extends MyAccountProps>(Component: React.ComponentType<P>): React.ComponentType<P> =>
+  withMulti(Component, withMyAccount, withMembershipRequired);

+ 2 - 0
pioneer/packages/joy-utils/src/react/hooks/index.ts

@@ -0,0 +1,2 @@
+export { default as useMyAccount } from './useMyAccount';
+export { default as useMyMembership } from './useMyMembership';

+ 6 - 0
pioneer/packages/joy-utils/src/react/hooks/useMyAccount.tsx

@@ -0,0 +1,6 @@
+import { useContext } from 'react';
+import { MyAccountContext } from '../context/account';
+
+export default function useMyAccount () {
+  return useContext(MyAccountContext);
+}

+ 6 - 0
pioneer/packages/joy-utils/src/react/hooks/useMyMembership.tsx

@@ -0,0 +1,6 @@
+import { useContext } from 'react';
+import { MyMembershipContext } from '../context';
+
+export default function useMyMembership () {
+  return useContext(MyMembershipContext);
+}

+ 0 - 28
pioneer/packages/old-apps/apps-routing/src/joy-pages.ts

@@ -1,28 +0,0 @@
-import { Routes } from './types';
-
-import { ToS, Privacy } from '@polkadot/joy-pages/index';
-
-export default ([
-  {
-    Component: ToS,
-    display: {
-      isHidden: true
-    },
-    i18n: {
-      defaultValue: 'Terms of Service'
-    },
-    icon: 'file outline',
-    name: 'pages/tos'
-  },
-  {
-    Component: Privacy,
-    display: {
-      isHidden: true
-    },
-    i18n: {
-      defaultValue: 'Privacy Policy'
-    },
-    icon: 'file outline',
-    name: 'pages/privacy'
-  }
-] as Routes);

+ 0 - 17
pioneer/packages/old-apps/apps/src/TopBar.css

@@ -1,17 +0,0 @@
-.JoyTopBar {
-  padding: .5rem .5rem;
-  background-color: #3f3f3f;
-  border-bottom: 1px solid #d4d4d5;
-  text-align: right;
-  margin: 0 -2rem;
-
-  &.NoMyAddress {
-    background-color: #ffeb83;
-    color: #000;
-    text-align: center;
-  }
-
-  .ui--InputAddress {
-    display: inline-block;
-  }
-}

+ 0 - 47
pioneer/packages/old-apps/apps/src/TopBar.tsx

@@ -1,47 +0,0 @@
-import React from 'react';
-// import { Link } from 'react-router-dom';
-import { I18nProps } from '@polkadot/react-components/types';
-import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext';
-import { InputAddress } from '@polkadot/react-components';
-import { Available } from '@polkadot/react-query';
-import translate from './translate';
-import './TopBar.css';
-
-type Props = I18nProps & {};
-
-function renderAddress (address: string) {
-  const balance = <span className="label">Balance: </span>;
-
-  return (
-    <div className="JoyTopBar">
-      <InputAddress
-        defaultValue={address}
-        help="My current key that signs transactions"
-        label="My key"
-        labelExtra={<Available label={balance} params={address} />}
-        type="account"
-      />
-    </div>
-  );
-}
-
-// function renderNoAddress() {
-//   return (
-//     <div className="JoyTopBar NoMyAddress">
-//       <i className="warning sign icon"></i>
-//       <span style={{ marginRight: '1rem' }}>You need to create a key if you want to use all features.</span>
-//       <Link className="ui small button orange" to="/accounts">
-//         Create key
-//       </Link>
-//     </div>
-//   );
-// }
-
-function Component (_props: Props) {
-  const {
-    state: { address }
-  } = useMyAccount();
-  return address ? renderAddress(address) : null;
-}
-
-export default translate(Component);

+ 0 - 11
pioneer/packages/old-apps/react-components/src/styles/old-theme.ts-unused

@@ -1,11 +0,0 @@
-/* Copyright 2017-2019 @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 theme from 'styled-theming';
-
-export const primaryColor = theme('theme', {
-  substrate: '#DB2828',
-  polkadot: '#E6007A',
-  default: 'darkorange'
-});

+ 1 - 1
pioneer/packages/page-staking/src/Targets/Summary.tsx

@@ -66,7 +66,7 @@ function Summary ({ lastReward, numNominators, numValidators, totalStaked }: Pro
           </CardSummary>
         )}
       </section>
-      {numValidators && numNominators && (
+      {numValidators !== undefined && numNominators !== undefined && (
         <CardSummary label={`${t<string>('validators')} / ${t<string>('nominators')}`}>
           {numValidators}&nbsp;/&nbsp;{numNominators}
         </CardSummary>

+ 8 - 0
pioneer/packages/react-components/src/InputAddress/index.tsx

@@ -20,6 +20,8 @@ import Dropdown from '../Dropdown';
 import createHeader from './createHeader';
 import createItem from './createItem';
 
+import { ACCOUNT_CHANGED_EVENT_NAME } from '@polkadot/joy-utils/react/context/account';
+
 interface Props {
   className?: string;
   defaultValue?: Uint8Array | string | null;
@@ -115,6 +117,12 @@ function setLastValue (type: KeyringOption$Type = DEFAULT_TYPE, value: string):
 
   options.defaults[type] = value;
   store.set(STORAGE_KEY, options);
+
+  if (type === 'account') {
+    // This lets us update joy-utils account context in order to always be in sync
+    // with options:InputAddress: { defaults: { account } }) from local storage
+    window.dispatchEvent(new CustomEvent<string>(ACCOUNT_CHANGED_EVENT_NAME, { detail: value }));
+  }
 }
 
 class InputAddress extends React.PureComponent<Props, State> {

+ 5 - 2
pioneer/packages/react-components/src/Tabs/Tab.tsx

@@ -16,16 +16,19 @@ interface Props extends TabItem {
   index: number;
   isSequence?: boolean;
   num: number;
+  forceMatchParams?: boolean;
 }
 
-function Tab ({ basePath, className = '', hasParams, index, isExact, isRoot, isSequence, name, num, text }: Props): React.ReactElement<Props> {
+function Tab ({ basePath, className = '', hasParams, index, isExact, isRoot, isSequence, name, num, text, forceMatchParams }: Props): React.ReactElement<Props> {
   const to = isRoot
     ? basePath
     : `${basePath}/${name}`;
 
   // only do exact matching when not the fallback (first position tab),
   // params are problematic for dynamic hidden such as app-accounts
-  const tabIsExact = isExact || !hasParams || (!isSequence && index === 0);
+  const tabIsExact = forceMatchParams
+    ? false
+    : (isExact || !hasParams || (!isSequence && index === 0));
 
   return (
     <NavLink

+ 1 - 0
pioneer/packages/react-components/src/Tabs/types.ts

@@ -11,4 +11,5 @@ export interface TabItem {
   isRoot?: boolean;
   name: string;
   text: React.ReactNode;
+  forceMatchParams?: boolean;
 }

+ 3 - 1
pioneer/packages/react-components/src/styles/index.ts

@@ -10,12 +10,13 @@ import cssMedia from './media';
 import cssRx from './rx';
 import cssSemantic from './semantic';
 import cssTheme from './theme';
+import cssJoystream from './joystream';
 
 interface Props {
   uiHighlight?: string;
 }
 
-const defaultHighlight = '#f19135'; // #999
+const defaultHighlight = '#4038FF'; // #999
 
 const getHighlight = (props: Props): string =>
   (props.uiHighlight || defaultHighlight);
@@ -281,4 +282,5 @@ export default createGlobalStyle<Props>`
   ${cssMedia}
   ${cssRx}
   ${cssComponents}
+  ${cssJoystream}
 `;

+ 16 - 8
pioneer/packages/old-apps/react-components/src/styles/joystream.ts → pioneer/packages/react-components/src/styles/joystream.ts

@@ -94,14 +94,6 @@ export default css`
     }
   }
 
-  .apps--SideBar-logo {
-    max-height: 26px !important;
-    margin: 1rem 1.5rem 2.5rem 0.75rem !important;
-  }
-  .collapsed .apps--SideBar-logo {
-    margin: 1rem 0.75rem 2.5rem 0.5rem !important;
-  }
-
   .JoyForm {
     margin-bottom: 1.5rem;
 
@@ -192,4 +184,20 @@ export default css`
   .text-blue {
     color: #3b83c0;
   }
+
+  /* Overrides */
+  .ui--IdentityIcon {
+    border: none !important;
+  }
+  /* Normalize SideBar icons width */
+  .apps--SideBar-Item-NavLink svg {
+    width: 20px !important;
+  }
+  /* Fix "collapsed" sidebar on mobile */
+  .apps--Wrapper:not(.menu-open) .apps--SideBar-Scroll {
+    padding: 0 !important;
+  }
+  h1 {
+    text-transform: none;
+  }
 `;

+ 1 - 1
pioneer/packages/react-components/src/styles/theme.ts

@@ -10,7 +10,7 @@ export const colorBtnDefault = '#767778';
 export const colorBtnShadow = '#98999a';
 
 /* highlighted buttons, orange */
-export const colorBtnHighlight = '#f19135';
+export const colorBtnHighlight = '#4038FF';
 
 /* primary buttons, blue */
 export const colorBtnPrimary = colorBtnDefault; // '#2e86ab';

+ 7 - 9
pioneer/tsconfig.json

@@ -4,16 +4,13 @@
     "build/**/*",
     "**/build/**/*",
     "packages/old-apps/**",
-    "packages/joy-members/**/*",
     "packages/joy-election/**/*",
     "packages/joy-forum/**/*",
     "packages/joy-help/**/*",
     "packages/joy-media/**/*",
-    "packages/joy-pages/**/*",
     "packages/joy-proposals/**/*",
     "packages/joy-roles/**/*",
     "packages/joy-settings/**/*",
-    "packages/joy-utils/**/*",
     "packages/joy-utils-old/**/*"
   ],
   "compilerOptions": {
@@ -23,6 +20,7 @@
     "resolveJsonModule": true,
     "baseUrl": ".",
     "paths": {
+      "@polkadot/types/augment": [ "../types/src/definitions/augment-types.ts" ],
       // "@joystream/types/": [ "../types/src/" ],
       // "@joystream/types/*": [ "../types/src/*" ],
       // "@polkadot/joy-election/": [ "packages/joy-election/src/" ],
@@ -33,18 +31,18 @@
       // "@polkadot/joy-help/*": [ "packages/joy-help/src/*" ],
       // "@polkadot/joy-media/": [ "packages/joy-media/src/" ],
       // "@polkadot/joy-media/*": [ "packages/joy-media/src/*" ],
-      // "@polkadot/joy-members/": [ "packages/joy-members/src/" ],
-      // "@polkadot/joy-members/*": [ "packages/joy-members/src/*" ],
-      // "@polkadot/joy-pages/": [ "packages/joy-pages/src/" ],
-      // "@polkadot/joy-pages/*": [ "packages/joy-pages/src/*" ],
+      "@polkadot/joy-members/": [ "packages/joy-members/src/" ],
+      "@polkadot/joy-members/*": [ "packages/joy-members/src/*" ],
+      "@polkadot/joy-pages/": [ "packages/joy-pages/src/" ],
+      "@polkadot/joy-pages/*": [ "packages/joy-pages/src/*" ],
       // "@polkadot/joy-proposals/": [ "packages/joy-proposals/src/" ],
       // "@polkadot/joy-proposals/*": [ "packages/joy-proposals/src/*" ],
       // "@polkadot/joy-roles/": [ "packages/joy-roles/src/" ],
       // "@polkadot/joy-roles/*": [ "packages/joy-roles/src/*" ],
       // "@polkadot/joy-settings/": [ "packages/joy-settings/src/" ],
       // "@polkadot/joy-settings/*": [ "packages/joy-settings/src/*" ],
-      // "@polkadot/joy-utils/": [ "packages/joy-utils/src/" ],
-      // "@polkadot/joy-utils/*": [ "packages/joy-utils/src/*" ],
+      "@polkadot/joy-utils/": [ "packages/joy-utils/src/" ],
+      "@polkadot/joy-utils/*": [ "packages/joy-utils/src/*" ],
       "@polkadot/apps/*": ["packages/apps/src/*"],
       "@polkadot/apps": ["packages/apps/src"],
       "@polkadot/apps-config/*": [ "packages/apps-config/src/*" ],

+ 272 - 18
yarn.lock

@@ -1374,6 +1374,13 @@
     pirates "^4.0.0"
     source-map-support "^0.5.16"
 
+"@babel/runtime@7.0.0":
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0.tgz#adeb78fedfc855aa05bc041640f3f6f98e85424c"
+  integrity sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA==
+  dependencies:
+    regenerator-runtime "^0.12.0"
+
 "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.3", "@babel/runtime@^7.11.1", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2", "@babel/runtime@^7.9.6":
   version "7.11.2"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
@@ -4450,6 +4457,13 @@
   resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
   integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
 
+"@types/query-string@^6.2.0":
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/@types/query-string/-/query-string-6.3.0.tgz#b6fa172a01405abcaedac681118e78429d62ea39"
+  integrity sha512-yuIv/WRffRzL7cBW+sla4HwBZrEXRNf1MKQ5SklPEadth+BKbDxiVG8A3iISN5B3yC4EeSCzMZP8llHTcUhOzQ==
+  dependencies:
+    query-string "*"
+
 "@types/reach__router@^1.2.3":
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/@types/reach__router/-/reach__router-1.3.5.tgz#14e1e981cccd3a5e50dc9e969a72de0b9d472f6d"
@@ -4648,16 +4662,37 @@
   dependencies:
     source-map "^0.6.1"
 
-"@types/unist@^2.0.0", "@types/unist@^2.0.2":
+"@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2":
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
   integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==
 
+"@types/uuid@^3.4.4":
+  version "3.4.9"
+  resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.9.tgz#fcf01997bbc9f7c09ae5f91383af076d466594e1"
+  integrity sha512-XDwyIlt/47l2kWLTzw/mtrpLdB+GPSskR2n/PIcPn+VYhVO77rGhRncIR5GPU0KRzXuqkDO+J5qqrG0Y8P6jzQ==
+
 "@types/uuid@^7.0.2":
   version "7.0.4"
   resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-7.0.4.tgz#00a5749810b4ad80bff73a61f9cc9d0d521feb3c"
   integrity sha512-WGZCqBZZ0mXN2RxvLHL6/7RCu+OWs28jgQMP04LWfpyJlQUMTR6YU9CNJAKDgbw+EV/u687INXuLUc7FuML/4g==
 
+"@types/vfile-message@*":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/@types/vfile-message/-/vfile-message-2.0.0.tgz#690e46af0fdfc1f9faae00cd049cc888957927d5"
+  integrity sha512-GpTIuDpb9u4zIO165fUy9+fXcULdD8HFRNli04GehoMVbeNq7D6OBnqSmg3lxZnC+UvgUhEWKxdKiwYUkGltIw==
+  dependencies:
+    vfile-message "*"
+
+"@types/vfile@^3.0.0":
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/@types/vfile/-/vfile-3.0.2.tgz#19c18cd232df11ce6fa6ad80259bc86c366b09b9"
+  integrity sha512-b3nLFGaGkJ9rzOcuXRfHkZMdjsawuDD0ENL9fzTophtBg8FJHSGbH7daXkEpcwy3v7Xol3pAvsmlYyFhR4pqJw==
+  dependencies:
+    "@types/node" "*"
+    "@types/unist" "*"
+    "@types/vfile-message" "*"
+
 "@types/vfile@^4.0.0":
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/@types/vfile/-/vfile-4.0.0.tgz#c32d13cbda319bc9f4ab3cacc0263b4ba1dd1ea3"
@@ -4708,6 +4743,11 @@
   resolved "https://registry.yarnpkg.com/@types/yoga-layout/-/yoga-layout-1.9.2.tgz#efaf9e991a7390dc081a0b679185979a83a9639a"
   integrity sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw==
 
+"@types/yup@^0.26.10":
+  version "0.26.37"
+  resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.37.tgz#7a52854ac602ba0dc969bebc960559f7464a1686"
+  integrity sha512-cDqR/ez4+iAVQYOwadXjKX4Dq1frtnDGV2GNVKj3aUVKVCKRvsr8esFk66j+LgeeJGmrMcBkkfCf3zk13MjV7A==
+
 "@typescript-eslint/eslint-plugin@3.8.0":
   version "3.8.0"
   resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.8.0.tgz#f82947bcdd9a4e42be7ad80dfd61f1dc411dd1df"
@@ -5895,7 +5935,7 @@ arrify@^2.0.1:
   resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
   integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
 
-asap@^2.0.0:
+asap@^2.0.0, asap@~2.0.3:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
   integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
@@ -8508,6 +8548,11 @@ core-js-pure@^3.0.1:
   resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.4.5.tgz#284474cb48d134e26e6e314cb89986c6c59480fb"
   integrity sha512-v3BoUOhmBvs4Z17jG/oM7qyv+tEEMvD1FYDDfxa6uD5W2rA/DpKvhvmyrBzxuMQTa/91UQKisaiqe0+0GuL2oA==
 
+core-js@^1.0.0:
+  version "1.2.7"
+  resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
+  integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=
+
 core-js@^2.4.0, core-js@^2.6.5:
   version "2.6.10"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.10.tgz#8a5b8391f8cc7013da703411ce5b585706300d7f"
@@ -8661,6 +8706,14 @@ create-react-context@0.3.0, create-react-context@^0.3.0:
     gud "^1.0.0"
     warning "^4.0.3"
 
+create-react-context@^0.2.2:
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.2.3.tgz#9ec140a6914a22ef04b8b09b7771de89567cb6f3"
+  integrity sha512-CQBmD0+QGgTaxDL3OX1IDXYqjkp2It4RIbcb99jS6AEg27Ga+a9G3JtK6SIu0HBwPLZlmwt9F7UwWA4Bn92Rag==
+  dependencies:
+    fbjs "^0.8.0"
+    gud "^1.0.0"
+
 cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5:
   version "6.0.5"
   resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
@@ -9223,6 +9276,11 @@ deepmerge@^1.5.2:
   resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753"
   integrity sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==
 
+deepmerge@^2.1.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
+  integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==
+
 deepmerge@^4.0.0, deepmerge@^4.2.2:
   version "4.2.2"
   resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
@@ -11289,6 +11347,19 @@ fb-watchman@^2.0.0:
   dependencies:
     bser "^2.0.0"
 
+fbjs@^0.8.0:
+  version "0.8.17"
+  resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
+  integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=
+  dependencies:
+    core-js "^1.0.0"
+    isomorphic-fetch "^2.1.1"
+    loose-envify "^1.0.0"
+    object-assign "^4.1.0"
+    promise "^7.1.1"
+    setimmediate "^1.0.5"
+    ua-parser-js "^0.7.18"
+
 fd-slicer@~1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
@@ -11494,7 +11565,7 @@ find-babel-config@^1.2.0:
     json5 "^0.5.1"
     path-exists "^3.0.0"
 
-find-cache-dir@^2.0.0, find-cache-dir@^2.1.0:
+find-cache-dir@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7"
   integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==
@@ -11636,6 +11707,11 @@ flush-write-stream@^1.0.0, flush-write-stream@^1.0.2:
     inherits "^2.0.3"
     readable-stream "^2.3.6"
 
+fn-name@~2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/fn-name/-/fn-name-2.0.1.tgz#5214d7537a4d06a4a301c0cc262feb84188002e7"
+  integrity sha1-UhTXU3pNBqSjAcDMJi/rhBiAAuc=
+
 focus-lock@^0.7.0:
   version "0.7.0"
   resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.7.0.tgz#b2bfb0ca7beacc8710a1ff74275fe0dc60a1d88a"
@@ -11737,6 +11813,21 @@ formidable@^1.2.0:
   resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9"
   integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==
 
+formik@^1.5.0:
+  version "1.5.8"
+  resolved "https://registry.yarnpkg.com/formik/-/formik-1.5.8.tgz#eee8cd345effe46839bc748c7f920486f12f14b0"
+  integrity sha512-fNvPe+ddbh+7xiByT25vuso2p2hseG/Yvuj211fV1DbCjljUEG9OpgRpcb7g7O3kxHX/q31cbZDzMxJXPWSNwA==
+  dependencies:
+    create-react-context "^0.2.2"
+    deepmerge "^2.1.1"
+    hoist-non-react-statics "^3.3.0"
+    lodash "^4.17.14"
+    lodash-es "^4.17.14"
+    prop-types "^15.6.1"
+    react-fast-compare "^2.0.1"
+    tiny-warning "^1.0.2"
+    tslib "^1.9.3"
+
 forwarded@~0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
@@ -14739,7 +14830,7 @@ isobject@^4.0.0:
   resolved "https://registry.yarnpkg.com/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0"
   integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==
 
-isomorphic-fetch@^2.2.1:
+isomorphic-fetch@^2.1.1, isomorphic-fetch@^2.2.1:
   version "2.2.1"
   resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
   integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=
@@ -16122,6 +16213,11 @@ locate-path@^5.0.0:
   dependencies:
     p-locate "^4.1.0"
 
+lodash-es@^4.17.14:
+  version "4.17.15"
+  resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
+  integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
+
 lodash._reinterpolate@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
@@ -16582,6 +16678,11 @@ markdown-loader@^5.1.0:
     loader-utils "^1.2.3"
     marked "^0.7.0"
 
+markdown-table@^1.1.0:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-1.1.3.tgz#9fcb69bcfdb8717bfd0398c6ec2d93036ef8de60"
+  integrity sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==
+
 markdown-table@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-2.0.0.tgz#194a90ced26d31fe753d8b9434430214c011865b"
@@ -16645,6 +16746,13 @@ mdast-add-list-metadata@1.0.1:
   dependencies:
     unist-util-visit-parents "1.1.2"
 
+mdast-util-compact@^1.0.0:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/mdast-util-compact/-/mdast-util-compact-1.0.4.tgz#d531bb7667b5123abf20859be086c4d06c894593"
+  integrity sha512-3YDMQHI5vRiS2uygEFYaqckibpJtKq5Sj2c8JioeOQBU6INpKbdWzfyLqFFnDwEcEnRFIdMsguzs5pC1Jp4Isg==
+  dependencies:
+    unist-util-visit "^1.1.0"
+
 mdast-util-compact@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/mdast-util-compact/-/mdast-util-compact-2.0.1.tgz#cabc69a2f43103628326f35b1acf735d55c99490"
@@ -18723,7 +18831,7 @@ parse-asn1@^5.0.0:
     pbkdf2 "^3.0.3"
     safe-buffer "^5.1.1"
 
-parse-entities@^1.1.0, parse-entities@^1.1.2:
+parse-entities@^1.0.2, parse-entities@^1.1.0, parse-entities@^1.1.2:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.2.2.tgz#c31bf0f653b6661354f8973559cb86dd1d5edf50"
   integrity sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==
@@ -19056,7 +19164,7 @@ pirates@^3.0.2:
   dependencies:
     node-modules-regexp "^1.0.0"
 
-pirates@^4.0.0, pirates@^4.0.1:
+pirates@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87"
   integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==
@@ -19805,6 +19913,13 @@ promise.prototype.finally@^3.1.0:
     es-abstract "^1.17.0-next.0"
     function-bind "^1.1.1"
 
+promise@^7.1.1:
+  version "7.3.1"
+  resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
+  integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==
+  dependencies:
+    asap "~2.0.3"
+
 promise@~1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/promise/-/promise-1.3.0.tgz#e5cc9a4c8278e4664ffedc01c7da84842b040175"
@@ -19850,6 +19965,11 @@ proper-lockfile@^4.0.0, proper-lockfile@^4.1.1:
     retry "^0.12.0"
     signal-exit "^3.0.2"
 
+property-expr@^1.5.0:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-1.5.1.tgz#22e8706894a0c8e28d58735804f6ba3a3673314f"
+  integrity sha512-CGuc0VUTGthpJXL36ydB6jnbyOf/rAHFvmVrJlH+Rg0DqqLFQGAP6hIaxD/G0OAmBJPhXDHuEJigrp0e0wFV6g==
+
 property-information@^5.0.0:
   version "5.5.0"
   resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.5.0.tgz#4dc075d493061a82e2b7d096f406e076ed859943"
@@ -20119,6 +20239,15 @@ qs@~6.5.2:
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
   integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
 
+query-string@*, query-string@^6.13.1, query-string@^6.2.0:
+  version "6.13.1"
+  resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.1.tgz#d913ccfce3b4b3a713989fe6d39466d92e71ccad"
+  integrity sha512-RfoButmcK+yCta1+FuU8REvisx1oEzhMKwhLUNcepQTPGcNMp1sIqjnfCtfnvGSQZQEhaBHvccujtWoUV3TTbA==
+  dependencies:
+    decode-uri-component "^0.2.0"
+    split-on-first "^1.0.0"
+    strict-uri-encode "^2.0.0"
+
 query-string@^4.1.0:
   version "4.3.4"
   resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
@@ -20136,15 +20265,6 @@ query-string@^5.0.1:
     object-assign "^4.1.0"
     strict-uri-encode "^1.0.0"
 
-query-string@^6.13.1:
-  version "6.13.1"
-  resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.1.tgz#d913ccfce3b4b3a713989fe6d39466d92e71ccad"
-  integrity sha512-RfoButmcK+yCta1+FuU8REvisx1oEzhMKwhLUNcepQTPGcNMp1sIqjnfCtfnvGSQZQEhaBHvccujtWoUV3TTbA==
-  dependencies:
-    decode-uri-component "^0.2.0"
-    split-on-first "^1.0.0"
-    strict-uri-encode "^2.0.0"
-
 querystring-es3@^0.2.0, querystring-es3@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@@ -20380,6 +20500,11 @@ react-error-overlay@^6.0.3:
   resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108"
   integrity sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA==
 
+react-fast-compare@^2.0.1:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
+  integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
+
 react-fast-compare@^3.0.1:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
@@ -20454,7 +20579,7 @@ react-lifecycles-compat@^3.0.4:
   resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
   integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
 
-react-markdown@^4.3.1:
+react-markdown@^4.0.6, react-markdown@^4.3.1:
   version "4.3.1"
   resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-4.3.1.tgz#39f0633b94a027445b86c9811142d05381300f2f"
   integrity sha512-HQlWFTbDxTtNY6bjgp3C3uv1h2xcjCSi1zAEzfBW9OwJJvENSYiLXWNXN5hHLsoqai7RnZiiHzcnWdXk2Splzw==
@@ -20980,6 +21105,11 @@ regenerator-runtime@^0.11.0:
   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
   integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
 
+regenerator-runtime@^0.12.0:
+  version "0.12.1"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
+  integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
+
 regenerator-runtime@^0.13.2:
   version "0.13.3"
   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5"
@@ -21121,6 +21251,27 @@ remark-parse@^5.0.0:
     vfile-location "^2.0.0"
     xtend "^4.0.1"
 
+remark-parse@^6.0.0:
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-6.0.3.tgz#c99131052809da482108413f87b0ee7f52180a3a"
+  integrity sha512-QbDXWN4HfKTUC0hHa4teU463KclLAnwpn/FBn87j9cKYJWWawbiLgMfP2Q4XwhxxuuuOxHlw+pSN0OKuJwyVvg==
+  dependencies:
+    collapse-white-space "^1.0.2"
+    is-alphabetical "^1.0.0"
+    is-decimal "^1.0.0"
+    is-whitespace-character "^1.0.0"
+    is-word-character "^1.0.0"
+    markdown-escapes "^1.0.0"
+    parse-entities "^1.1.0"
+    repeat-string "^1.5.4"
+    state-toggle "^1.0.0"
+    trim "0.0.1"
+    trim-trailing-lines "^1.0.0"
+    unherit "^1.0.4"
+    unist-util-remove-position "^1.0.0"
+    vfile-location "^2.0.0"
+    xtend "^4.0.1"
+
 remark-parse@^8.0.0:
   version "8.0.3"
   resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-8.0.3.tgz#9c62aa3b35b79a486454c690472906075f40c7e1"
@@ -21143,6 +21294,26 @@ remark-parse@^8.0.0:
     vfile-location "^3.0.0"
     xtend "^4.0.1"
 
+remark-stringify@^6.0.0:
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-6.0.4.tgz#16ac229d4d1593249018663c7bddf28aafc4e088"
+  integrity sha512-eRWGdEPMVudijE/psbIDNcnJLRVx3xhfuEsTDGgH4GsFF91dVhw5nhmnBppafJ7+NWINW6C7ZwWbi30ImJzqWg==
+  dependencies:
+    ccount "^1.0.0"
+    is-alphanumeric "^1.0.0"
+    is-decimal "^1.0.0"
+    is-whitespace-character "^1.0.0"
+    longest-streak "^2.0.1"
+    markdown-escapes "^1.0.0"
+    markdown-table "^1.1.0"
+    mdast-util-compact "^1.0.0"
+    parse-entities "^1.0.2"
+    repeat-string "^1.5.4"
+    state-toggle "^1.0.0"
+    stringify-entities "^1.0.1"
+    unherit "^1.0.4"
+    xtend "^4.0.1"
+
 remark-stringify@^8.0.0:
   version "8.1.1"
   resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-8.1.1.tgz#e2a9dc7a7bf44e46a155ec78996db896780d8ce5"
@@ -21163,6 +21334,15 @@ remark-stringify@^8.0.0:
     unherit "^1.0.4"
     xtend "^4.0.1"
 
+remark@^10.0.1:
+  version "10.0.1"
+  resolved "https://registry.yarnpkg.com/remark/-/remark-10.0.1.tgz#3058076dc41781bf505d8978c291485fe47667df"
+  integrity sha512-E6lMuoLIy2TyiokHprMjcWNJ5UxfGQjaMSMhV+f4idM625UjjK4j798+gPs5mfjzDE6vL0oFKVeZM6gZVSVrzQ==
+  dependencies:
+    remark-parse "^6.0.0"
+    remark-stringify "^6.0.0"
+    unified "^7.0.0"
+
 remark@^12.0.0:
   version "12.0.1"
   resolved "https://registry.yarnpkg.com/remark/-/remark-12.0.1.tgz#f1ddf68db7be71ca2bad0a33cd3678b86b9c709f"
@@ -21959,7 +22139,7 @@ set-value@^2.0.0, set-value@^2.0.1:
     is-plain-object "^2.0.3"
     split-string "^3.0.1"
 
-setimmediate@^1.0.4:
+setimmediate@^1.0.4, setimmediate@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
   integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
@@ -22858,6 +23038,16 @@ string_decoder@~1.1.1:
   dependencies:
     safe-buffer "~5.1.0"
 
+stringify-entities@^1.0.1:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-1.3.2.tgz#a98417e5471fd227b3e45d3db1861c11caf668f7"
+  integrity sha512-nrBAQClJAPN2p+uGCVJRPIPakKeKWZ9GtBCmormE7pWOSlHat7+x5A8gx85M7HM5Dt0BP3pP5RhVW77WdbJJ3A==
+  dependencies:
+    character-entities-html4 "^1.0.0"
+    character-entities-legacy "^1.0.0"
+    is-alphanumerical "^1.0.0"
+    is-hexadecimal "^1.0.0"
+
 stringify-entities@^3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-3.0.1.tgz#32154b91286ab0869ab2c07696223bd23b6dbfc0"
@@ -22972,6 +23162,11 @@ strip-json-comments@~2.0.1:
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
   integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
 
+strip-markdown@^3.0.3:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/strip-markdown/-/strip-markdown-3.1.2.tgz#172f6f89f9a98896e65a65422e0507f2bbac1667"
+  integrity sha512-NjwW6CEefesmHQPs7lof/lgnSriqUnRNOWpnrNPq9A7/yOCdnhaB7DcxlhYuN7WiiRUe349aitAsTQ/ajM9Dmw==
+
 strip-outer@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/strip-outer/-/strip-outer-1.0.1.tgz#b2fd2abf6604b9d1e6013057195df836b8a9d631"
@@ -23277,6 +23472,11 @@ symbol.prototype.description@^1.0.0:
     es-abstract "^1.17.0-next.1"
     has-symbols "^1.0.1"
 
+synchronous-promise@^2.0.5:
+  version "2.0.13"
+  resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.13.tgz#9d8c165ddee69c5a6542862b405bc50095926702"
+  integrity sha512-R9N6uDkVsghHePKh1TEqbnLddO2IY25OcsksyFp/qBe7XYd0PVbKEWxhcdMhpLzE1I6skj5l4aEZ3CRxcbArlA==
+
 table@^5.2.3, table@^5.4.6:
   version "5.4.6"
   resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"
@@ -23735,7 +23935,7 @@ tiny-invariant@^1.0.2, tiny-invariant@^1.0.6:
   resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
   integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
 
-tiny-warning@^1.0.0, tiny-warning@^1.0.3:
+tiny-warning@^1.0.0, tiny-warning@^1.0.2, tiny-warning@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
   integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
@@ -23863,6 +24063,11 @@ toposort@^1.0.0:
   resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029"
   integrity sha1-LmhELZ9k7HILjMieZEOsbKqVACk=
 
+toposort@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330"
+  integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=
+
 touch@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
@@ -24220,6 +24425,11 @@ typescript@^3.0.3, typescript@^3.7.2, typescript@^3.7.5, typescript@^3.8.3, type
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.7.tgz#98d600a5ebdc38f40cb277522f12dc800e9e25fa"
   integrity sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==
 
+ua-parser-js@^0.7.18:
+  version "0.7.21"
+  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777"
+  integrity sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==
+
 uc.micro@^1.0.1, uc.micro@^1.0.5:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
@@ -24324,6 +24534,20 @@ unified@^6.1.5:
     vfile "^2.0.0"
     x-is-string "^0.1.0"
 
+unified@^7.0.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/unified/-/unified-7.1.0.tgz#5032f1c1ee3364bd09da12e27fdd4a7553c7be13"
+  integrity sha512-lbk82UOIGuCEsZhPj8rNAkXSDXd6p0QLzIuSsCdxrqnqU56St4eyOB+AlXsVgVeRmetPTYydIuvFfpDIed8mqw==
+  dependencies:
+    "@types/unist" "^2.0.0"
+    "@types/vfile" "^3.0.0"
+    bail "^1.0.0"
+    extend "^3.0.0"
+    is-plain-obj "^1.1.0"
+    trough "^1.0.0"
+    vfile "^3.0.0"
+    x-is-string "^0.1.0"
+
 unified@^9.0.0:
   version "9.1.0"
   resolved "https://registry.yarnpkg.com/unified/-/unified-9.1.0.tgz#7ba82e5db4740c47a04e688a9ca8335980547410"
@@ -24842,6 +25066,14 @@ vfile-location@^3.0.0:
   resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-3.0.1.tgz#d78677c3546de0f7cd977544c367266764d31bb3"
   integrity sha512-yYBO06eeN/Ki6Kh1QAkgzYpWT1d3Qln+ZCtSbJqFExPl1S3y2qqotJQXoh6qEvl/jDlgpUJolBn3PItVnnZRqQ==
 
+vfile-message@*:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-2.0.4.tgz#5b43b88171d409eae58477d13f23dd41d52c371a"
+  integrity sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==
+  dependencies:
+    "@types/unist" "^2.0.0"
+    unist-util-stringify-position "^2.0.0"
+
 vfile-message@^1.0.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-1.1.1.tgz#5833ae078a1dfa2d96e9647886cd32993ab313e1"
@@ -24878,6 +25110,16 @@ vfile@^2.0.0:
     unist-util-stringify-position "^1.0.0"
     vfile-message "^1.0.0"
 
+vfile@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/vfile/-/vfile-3.0.1.tgz#47331d2abe3282424f4a4bb6acd20a44c4121803"
+  integrity sha512-y7Y3gH9BsUSdD4KzHsuMaCzRjglXN0W2EcMf0gpvu6+SbsGhMje7xDc8AEoeXy6mIwCKMI6BkjMsRjzQbhMEjQ==
+  dependencies:
+    is-buffer "^2.0.0"
+    replace-ext "1.0.0"
+    unist-util-stringify-position "^1.0.0"
+    vfile-message "^1.0.0"
+
 vinyl-fs@^3.0.1:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-3.0.3.tgz#c85849405f67428feabbbd5c5dbdd64f47d31bc7"
@@ -25881,6 +26123,18 @@ yoga-layout-prebuilt@^1.9.3:
   dependencies:
     "@types/yoga-layout" "1.9.2"
 
+yup@^0.26.10:
+  version "0.26.10"
+  resolved "https://registry.yarnpkg.com/yup/-/yup-0.26.10.tgz#3545839663289038faf25facfc07e11fd67c0cb1"
+  integrity sha512-keuNEbNSnsOTOuGCt3UJW69jDE3O4P+UHAakO7vSeFMnjaitcmlbij/a3oNb9g1Y1KvSKH/7O1R2PQ4m4TRylw==
+  dependencies:
+    "@babel/runtime" "7.0.0"
+    fn-name "~2.0.1"
+    lodash "^4.17.10"
+    property-expr "^1.5.0"
+    synchronous-promise "^2.0.5"
+    toposort "^2.0.2"
+
 zepto@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/zepto/-/zepto-1.2.0.tgz#e127bd9e66fd846be5eab48c1394882f7c0e4f98"