Browse Source

Upgrade joy-election

Leszek Wiesner 4 years ago
parent
commit
5d4a194959
48 changed files with 609 additions and 513 deletions
  1. 2 1
      package.json
  2. 0 1
      pioneer/.eslintignore
  3. 3 0
      pioneer/packages/apps-routing/src/index.ts
  4. 17 0
      pioneer/packages/apps-routing/src/joy-election.ts
  5. 2 0
      pioneer/packages/apps-routing/src/types.ts
  6. 24 5
      pioneer/packages/apps/src/Content/index.tsx
  7. 5 2
      pioneer/packages/apps/src/SideBar/Item.tsx
  8. 0 0
      pioneer/packages/joy-election/.skip-build
  9. 3 3
      pioneer/packages/joy-election/package.json
  10. 2 2
      pioneer/packages/joy-election/src/Applicant.tsx
  11. 21 8
      pioneer/packages/joy-election/src/Applicants.tsx
  12. 19 21
      pioneer/packages/joy-election/src/ApplyForm.tsx
  13. 2 2
      pioneer/packages/joy-election/src/CandidatePreview.tsx
  14. 3 3
      pioneer/packages/joy-election/src/Council.tsx
  15. 77 51
      pioneer/packages/joy-election/src/Dashboard.tsx
  16. 15 13
      pioneer/packages/joy-election/src/Reveals.tsx
  17. 14 6
      pioneer/packages/joy-election/src/SealedVote.tsx
  18. 18 11
      pioneer/packages/joy-election/src/SealedVotes.tsx
  19. 34 0
      pioneer/packages/joy-election/src/SidebarSubtitle.tsx
  20. 27 27
      pioneer/packages/joy-election/src/VoteForm.tsx
  21. 28 7
      pioneer/packages/joy-election/src/Votes.tsx
  22. 0 28
      pioneer/packages/joy-election/src/index.css
  23. 13 9
      pioneer/packages/joy-election/src/index.tsx
  24. 1 1
      pioneer/packages/joy-election/src/myVotesStore.ts
  25. 21 0
      pioneer/packages/joy-election/src/style.ts
  26. 2 1
      pioneer/packages/joy-election/src/utils.tsx
  27. 0 35
      pioneer/packages/joy-utils-old/src/InputStake.tsx
  28. 0 48
      pioneer/packages/joy-utils-old/src/MemberByAccountPreview.tsx
  29. 0 107
      pioneer/packages/joy-utils-old/src/transport/base.ts
  30. 0 34
      pioneer/packages/joy-utils-old/src/transport/index.ts
  31. 46 0
      pioneer/packages/joy-utils/src/react/components/InputStake.tsx
  32. 43 0
      pioneer/packages/joy-utils/src/react/components/MemberByAccountPreview.tsx
  33. 0 0
      pioneer/packages/joy-utils/src/react/components/MemberProfilePreview.tsx
  34. 7 7
      pioneer/packages/joy-utils/src/react/components/MembersDropdown.tsx
  35. 0 0
      pioneer/packages/joy-utils/src/react/components/PromiseComponent.tsx
  36. 1 0
      pioneer/packages/joy-utils/src/react/context/index.tsx
  37. 0 0
      pioneer/packages/joy-utils/src/react/context/transport.tsx
  38. 2 0
      pioneer/packages/joy-utils/src/react/hooks/index.ts
  39. 0 0
      pioneer/packages/joy-utils/src/react/hooks/usePromise.tsx
  40. 0 0
      pioneer/packages/joy-utils/src/react/hooks/useTransport.tsx
  41. 0 0
      pioneer/packages/joy-utils/src/transport/APIQueryCache.ts
  42. 66 0
      pioneer/packages/joy-utils/src/transport/base.ts
  43. 34 0
      pioneer/packages/joy-utils/src/transport/index.ts
  44. 16 0
      pioneer/packages/joy-utils/src/transport/members.ts
  45. 4 0
      pioneer/packages/joy-utils/src/types/members.ts
  46. 0 19
      pioneer/packages/old-apps/apps-routing/src/joy-election.ts
  47. 35 58
      pioneer/packages/react-components/src/styles/joystream.ts
  48. 2 3
      pioneer/tsconfig.json

+ 2 - 1
package.json

@@ -25,7 +25,8 @@
     "pioneer/packages/react*",
     "pioneer/packages/joy-utils",
     "pioneer/packages/joy-members",
-    "pioneer/packages/joy-pages"
+    "pioneer/packages/joy-pages",
+    "pioneer/packages/joy-election"
   ],
   "resolutions": {
     "@polkadot/api": "1.26.1",

+ 0 - 1
pioneer/.eslintignore

@@ -2,7 +2,6 @@
 **/coverage/*
 **/node_modules/*
 packages/old-apps/*
-packages/joy-election/*
 packages/joy-forum/*
 packages/joy-help/*
 packages/joy-media/*

+ 3 - 0
pioneer/packages/apps-routing/src/index.ts

@@ -20,11 +20,13 @@ import transfer from './transfer';
 // Joy packages
 import members from './joy-members';
 import { terms, privacyPolicy } from './joy-pages';
+import election from './joy-election';
 
 export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Routes {
   return appSettings.uiMode === 'light'
     ? [
       members(t),
+      election(t),
       null,
       transfer(t),
       accounts(t),
@@ -32,6 +34,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
     ]
     : [
       members(t),
+      election(t),
       null,
       transfer(t),
       accounts(t),

+ 17 - 0
pioneer/packages/apps-routing/src/joy-election.ts

@@ -0,0 +1,17 @@
+import { Route } from './types';
+
+import Election from '@polkadot/joy-election/index';
+import SidebarSubtitle from '@polkadot/joy-election/SidebarSubtitle';
+
+export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Route {
+  return {
+    Component: Election,
+    display: {
+      needsApi: ['query.council.activeCouncil', 'query.councilElection.stage']
+    },
+    text: t<string>('nav.election', 'Council', { ns: 'apps-routing' }),
+    icon: 'university',
+    name: 'council',
+    SubtitleComponent: SidebarSubtitle
+  }
+}

+ 2 - 0
pioneer/packages/apps-routing/src/types.ts

@@ -24,6 +24,8 @@ export interface Route {
   name: string;
   text: string;
   useCounter?: () => number | string | null;
+  // Joystream-specific
+  SubtitleComponent?: React.ComponentType<any>;
 }
 
 export type Routes = (Route | null)[];

+ 24 - 5
pioneer/packages/apps/src/Content/index.tsx

@@ -16,6 +16,11 @@ import { useTranslation } from '../translate';
 import NotFound from './NotFound';
 import Status from './Status';
 
+// Joystream-specific
+// We must use transport provider here instead of /apps/src/index
+// to avoid "Cannot create Transport: The Substrate API is not ready yet." error
+import { TransportProvider } from '@polkadot/joy-utils/react/context';
+
 interface Props {
   className?: string;
 }
@@ -60,11 +65,25 @@ function Content ({ className }: Props): React.ReactElement<Props> {
                 ? <NotFound />
                 : (
                   <ErrorBoundary trigger={name}>
-                    <Component
-                      basePath={`/${name}`}
-                      location={location}
-                      onStatusChange={queueAction}
-                    />
+                    { needsApi
+                      // Add transport provider for routes that need the api
+                      // (the above condition makes sure it's aleady initialized at this point)
+                      ? (
+                        <TransportProvider>
+                          <Component
+                            basePath={`/${name}`}
+                            location={location}
+                            onStatusChange={queueAction}
+                          />
+                        </TransportProvider>
+                      )
+                      : (
+                        <Component
+                          basePath={`/${name}`}
+                          location={location}
+                          onStatusChange={queueAction}
+                        />
+                      ) }
                   </ErrorBoundary>
                 )
               }

+ 5 - 2
pioneer/packages/apps/src/SideBar/Item.tsx

@@ -79,12 +79,15 @@ function Item ({ isCollapsed, onClick, route }: Props): React.ReactElement<Props
     return null;
   }
 
-  const { Modal, icon, name, text } = route;
+  const { Modal, SubtitleComponent, icon, name, text } = route;
 
   const body = (
     <>
       <Icon icon={icon} />
-      <span className='text'>{text}</span>
+      <span className='text'>
+        {text}
+        { SubtitleComponent && <SubtitleComponent/> }
+      </span>
       {!!count && (
         <Badge
           color='counter'

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


+ 3 - 3
pioneer/packages/joy-election/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"
   }
 }

+ 2 - 2
pioneer/packages/joy-election/src/Applicant.tsx

@@ -4,13 +4,13 @@ import { Table } from 'semantic-ui-react';
 
 import { I18nProps } from '@polkadot/react-components/types';
 import { ApiProps } from '@polkadot/react-api/types';
-import { withCalls } from '@polkadot/react-api/with';
+import { withCalls } from '@polkadot/react-api/hoc';
 import { AccountId } from '@polkadot/types/interfaces';
 import { formatBalance } from '@polkadot/util';
 import CandidatePreview from './CandidatePreview';
 
 import translate from './translate';
-import { calcTotalStake } from '@polkadot/joy-utils/index';
+import { calcTotalStake } from '@polkadot/joy-utils/functions/misc';
 import { ElectionStake } from '@joystream/types/council';
 
 type Props = ApiProps & I18nProps & {

+ 21 - 8
pioneer/packages/joy-election/src/Applicants.tsx

@@ -1,23 +1,26 @@
 import React from 'react';
-import { Table } from 'semantic-ui-react';
+import { Table, Message } from 'semantic-ui-react';
 import BN from 'bn.js';
 
 import { I18nProps } from '@polkadot/react-components/types';
 import { ApiProps } from '@polkadot/react-api/types';
-import { withCalls } from '@polkadot/react-api/with';
+import { withCalls } from '@polkadot/react-api/hoc';
 import { AccountId } from '@polkadot/types/interfaces';
+import { Option } from '@polkadot/types';
 import { formatNumber } from '@polkadot/util';
 
 import translate from './translate';
 import Applicant from './Applicant';
 import ApplyForm from './ApplyForm';
-import Section from '@polkadot/joy-utils/Section';
-import { queryToProp } from '@polkadot/joy-utils/index';
-import { withMyAccount, MyAccountProps } from '@polkadot/joy-utils/MyAccount';
+import Section from '@polkadot/joy-utils/react/components/Section';
+import { queryToProp } from '@polkadot/joy-utils/functions/misc';
+import { withMyAccount, MyAccountProps } from '@polkadot/joy-utils/react/hocs/accounts';
+import { ElectionStage } from '@joystream/types/src/council';
 
 type Props = ApiProps & I18nProps & MyAccountProps & {
   candidacyLimit?: BN;
   applicants?: Array<AccountId>;
+  stage?: Option<ElectionStage>;
 };
 
 class Applicants extends React.PureComponent<Props> {
@@ -38,12 +41,21 @@ class Applicants extends React.PureComponent<Props> {
   )
 
   render () {
-    const { myAddress, applicants = [], candidacyLimit = new BN(0) } = this.props;
+    const { myAddress, applicants = [], candidacyLimit = new BN(0), stage } = this.props;
     const title = <span>Applicants <sup>{applicants.length}/{formatNumber(candidacyLimit)}</sup></span>;
 
     return <>
       <Section title='My application'>
-        <ApplyForm myAddress={myAddress} />
+        { stage?.unwrapOr(undefined)?.isOfType('Announcing')
+          ? (
+            <ApplyForm myAddress={myAddress} />
+          )
+          : (
+            <Message warning>
+              Applying to council is only possible during <i><b>Announcing</b></i> stage.
+            </Message>
+          )
+        }
       </Section>
       <Section title={title}>
         {!applicants.length
@@ -59,6 +71,7 @@ class Applicants extends React.PureComponent<Props> {
 export default translate(
   withCalls<Props>(
     queryToProp('query.councilElection.candidacyLimit'),
-    queryToProp('query.councilElection.applicants')
+    queryToProp('query.councilElection.applicants'),
+    queryToProp('query.councilElection.stage')
   )(withMyAccount(Applicants))
 );

+ 19 - 21
pioneer/packages/joy-election/src/ApplyForm.tsx

@@ -3,21 +3,21 @@ import React from 'react';
 
 import { I18nProps } from '@polkadot/react-components/types';
 import { ApiProps } from '@polkadot/react-api/types';
-import { withCalls, withMulti } from '@polkadot/react-api/with';
+import { withCalls, withMulti } from '@polkadot/react-api/hoc';
 import { Labelled } from '@polkadot/react-components/index';
 import { Balance } from '@polkadot/types/interfaces';
 
 import translate from './translate';
-import TxButton from '@polkadot/joy-utils/TxButton';
-import InputStake from '@polkadot/joy-utils/InputStake';
+import TxButton from '@polkadot/joy-utils/react/components/TxButton';
+import InputStake from '@polkadot/joy-utils/react/components/InputStake';
 import { ElectionStake } from '@joystream/types/council';
-import { calcTotalStake, ZERO } from '@polkadot/joy-utils/index';
-import { MyAddressProps, withOnlyMembers } from '@polkadot/joy-utils/MyAccount';
+import { calcTotalStake, ZERO } from '@polkadot/joy-utils/functions/misc';
+import { MyAddressProps } from '@polkadot/joy-utils/react/hocs/accounts';
+import { withOnlyMembers } from '@polkadot/joy-utils/react/hocs/guards';
 
 type Props = ApiProps & I18nProps & MyAddressProps & {
   minStake?: Balance;
   alreadyStaked?: ElectionStake;
-  myBalance?: Balance;
 };
 
 type State = {
@@ -48,15 +48,17 @@ class ApplyForm extends React.PureComponent<Props, State> {
           isValid={isStakeValid}
           onChange={this.onChangeStake}
         />
-        <Labelled style={{ marginTop: '.5rem' }}>
-          <TxButton
-            size='large'
-            isDisabled={!isStakeValid}
-            label={buttonLabel}
-            params={[stake]}
-            tx='councilElection.apply'
-          />
-        </Labelled>
+        <div style={{ marginTop: '.5rem' }}>
+          <Labelled>
+            <TxButton
+              size='large'
+              isDisabled={!isStakeValid}
+              label={buttonLabel}
+              params={[stake]}
+              tx='councilElection.apply'
+            />
+          </Labelled>
+        </div>
       </div>
     );
   }
@@ -71,10 +73,8 @@ class ApplyForm extends React.PureComponent<Props, State> {
 
   private onChangeStake = (stake?: BN): void => {
     stake = stake || ZERO;
-    const { myBalance = ZERO } = this.props;
-    const isStakeLteBalance = stake.lte(myBalance);
     const isStakeGteMinStake = stake.add(this.alreadyStaked()).gte(this.minStake());
-    const isStakeValid = !stake.isZero() && isStakeGteMinStake && isStakeLteBalance;
+    const isStakeValid = !stake.isZero() && isStakeGteMinStake;
     this.setState({ stake, isStakeValid });
   }
 }
@@ -88,8 +88,6 @@ export default withMulti(
     ['query.councilElection.minCouncilStake',
       { propName: 'minStake' }],
     ['query.councilElection.applicantStakes',
-      { paramName: 'myAddress', propName: 'alreadyStaked' }],
-    ['query.balances.freeBalance',
-      { paramName: 'myAddress', propName: 'myBalance' }]
+      { paramName: 'myAddress', propName: 'alreadyStaked' }]
   )
 );

+ 2 - 2
pioneer/packages/joy-election/src/CandidatePreview.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
-import AddressMini from '@polkadot/react-components/AddressMiniJoy';
-import MemberByAccount from '@polkadot/joy-utils/MemberByAccountPreview';
+import AddressMini from '@polkadot/react-components/AddressMini';
+import MemberByAccount from '@polkadot/joy-utils/react/components/MemberByAccountPreview';
 import { AccountId } from '@polkadot/types/interfaces';
 
 import styled from 'styled-components';

+ 3 - 3
pioneer/packages/joy-election/src/Council.tsx

@@ -2,15 +2,15 @@ import React from 'react';
 
 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 { Table } from 'semantic-ui-react';
 import { formatBalance } from '@polkadot/util';
 import CouncilCandidate from './CandidatePreview';
 
-import { calcBackersStake } from '@polkadot/joy-utils/index';
+import { calcBackersStake } from '@polkadot/joy-utils/functions/misc';
 import { Seat } from '@joystream/types/council';
 import translate from './translate';
-import Section from '@polkadot/joy-utils/Section';
+import Section from '@polkadot/joy-utils/react/components/Section';
 
 type Props = ApiProps &
 I18nProps & {

+ 77 - 51
pioneer/packages/joy-election/src/Dashboard.tsx

@@ -3,14 +3,14 @@ import React from 'react';
 
 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 { BlockNumber, Balance } from '@polkadot/types/interfaces';
-import { Bubble } from '@polkadot/react-components/index';
+import { Label, Icon } from 'semantic-ui-react';
 import { formatNumber, formatBalance } from '@polkadot/util';
 
-import Section from '@polkadot/joy-utils/Section';
-import { queryToProp } from '@polkadot/joy-utils/index';
+import Section from '@polkadot/joy-utils/react/components/Section';
+import { queryToProp } from '@polkadot/joy-utils/functions/misc';
 import { ElectionStage, Seat } from '@joystream/types/council';
 import translate from './translate';
 
@@ -45,12 +45,17 @@ class Dashboard extends React.PureComponent<Props, State> {
     const title = `Council ${activeCouncil.length > 0 ? '' : '(not elected)'}`;
 
     return <Section title={title}>
-      <Bubble label='Council members'>
-        {activeCouncil.length}
-      </Bubble>
-      <Bubble icon='flag checkered' label='Term ends at block #'>
-        {formatNumber(p.termEndsAt)}
-      </Bubble>
+      <Label.Group color="grey" size="large">
+        <Label>
+          Council members
+          <Label.Detail>{activeCouncil.length}</Label.Detail>
+        </Label>
+        <Label>
+          <Icon name="flag checkered"/>
+          Term ends at block #
+          <Label.Detail>{formatNumber(p.termEndsAt)}</Label.Detail>
+        </Label>
+      </Label.Group>
     </Section>;
   }
 
@@ -76,20 +81,28 @@ class Dashboard extends React.PureComponent<Props, State> {
     const title = <>Election (<span className={stateClass}>{stateText}</span>)</>;
 
     return <Section title={title}>
-      <Bubble icon='target' label='Election round #'>
-        {formatNumber(round)}
-      </Bubble>
-      {isRunning && <>
-        <Bubble label='Stage'>
-          {stageName}
-        </Bubble>
-        <Bubble label='Blocks left'>
-          {formatNumber(leftBlocks)}
-        </Bubble>
-        <Bubble icon='flag checkered' label='Stage ends at block #'>
-          {formatNumber(stageEndsAt)}
-        </Bubble>
-      </>}
+      <Label.Group color="grey" size="large">
+        <Label>
+          <Icon name="target"/>
+          Election round #
+          <Label.Detail>{formatNumber(round)}</Label.Detail>
+        </Label>
+        {isRunning && <>
+          <Label>
+            Stage
+            <Label.Detail>{stageName}</Label.Detail>
+          </Label>
+          <Label>
+            Blocks left
+            <Label.Detail>{formatNumber(leftBlocks)}</Label.Detail>
+          </Label>
+          <Label>
+            <Icon name="flag checkered"/>
+            Stage ends at block #
+            <Label.Detail>{formatNumber(stageEndsAt)}</Label.Detail>
+          </Label>
+        </>}
+      </Label.Group>
     </Section>;
   }
 
@@ -98,33 +111,46 @@ class Dashboard extends React.PureComponent<Props, State> {
     const isAutoStart = (p.autoStart || false).valueOf();
 
     return <Section title='Configuration'>
-      <Bubble label='Auto-start elections'>
-        {isAutoStart ? 'Yes' : 'No'}
-      </Bubble>
-      <Bubble label='New term duration'>
-        {formatNumber(p.newTermDuration)}
-      </Bubble>
-      <Bubble label='Candidacy limit'>
-        {formatNumber(p.candidacyLimit)}
-      </Bubble>
-      <Bubble label='Council size'>
-        {formatNumber(p.councilSize)}
-      </Bubble>
-      <Bubble label='Min. council stake'>
-        {formatBalance(p.minCouncilStake)}
-      </Bubble>
-      <Bubble label='Min. voting stake'>
-        {formatBalance(p.minVotingStake)}
-      </Bubble>
-      <Bubble label='Announcing period'>
-        {formatNumber(p.announcingPeriod)} blocks
-      </Bubble>
-      <Bubble label='Voting period'>
-        {formatNumber(p.votingPeriod)} blocks
-      </Bubble>
-      <Bubble label='Revealing period'>
-        {formatNumber(p.revealingPeriod)} blocks
-      </Bubble>
+      <Label.Group color="grey" size="large">
+        <Label>
+          Auto-start elections
+          <Label.Detail>{isAutoStart ? 'Yes' : 'No'}</Label.Detail>
+        </Label>
+        <Label>
+          New term duration
+          <Label.Detail>{formatNumber(p.newTermDuration)}</Label.Detail>
+        </Label>
+        <Label>
+          Candidacy limit
+          <Label.Detail>{formatNumber(p.candidacyLimit)}</Label.Detail>
+        </Label>
+        <Label>
+          Council size
+          <Label.Detail>{formatNumber(p.councilSize)}</Label.Detail>
+        </Label>
+        <Label>
+          Min. council stake
+          <Label.Detail>{formatBalance(p.minCouncilStake)}</Label.Detail>
+        </Label>
+        <Label>
+          Min. voting stake
+          <Label.Detail>{formatBalance(p.minVotingStake)}</Label.Detail>
+        </Label>
+      </Label.Group>
+      <Label.Group color="grey" size="large">
+        <Label>
+          Announcing period
+          <Label.Detail>{formatNumber(p.announcingPeriod)} blocks</Label.Detail>
+        </Label>
+        <Label>
+          Voting period
+          <Label.Detail>{formatNumber(p.votingPeriod)} blocks</Label.Detail>
+        </Label>
+        <Label>
+          Revealing period
+          <Label.Detail>{formatNumber(p.revealingPeriod)} blocks</Label.Detail>
+        </Label>
+      </Label.Group>
     </Section>;
   }
 

+ 15 - 13
pioneer/packages/joy-election/src/Reveals.tsx

@@ -2,16 +2,16 @@ import React from 'react';
 
 import { AppProps, I18nProps } from '@polkadot/react-components/types';
 import { ApiProps } from '@polkadot/react-api/types';
-import { withCalls, withMulti } from '@polkadot/react-api/with';
+import { withCalls, withMulti } from '@polkadot/react-api/hoc';
 import { AccountId } from '@polkadot/types/interfaces';
 import { Input, Labelled, InputAddress } from '@polkadot/react-components/index';
 
 import translate from './translate';
-import { nonEmptyStr, queryToProp, getUrlParam } from '@polkadot/joy-utils/index';
+import { nonEmptyStr, queryToProp, getUrlParam } from '@polkadot/joy-utils/functions/misc';
 import { accountIdsToOptions, hashVote } from './utils';
-import TxButton from '@polkadot/joy-utils/TxButton';
+import TxButton from '@polkadot/joy-utils/react/components/TxButton';
 import { findVoteByHash } from './myVotesStore';
-import { withOnlyMembers } from '@polkadot/joy-utils/MyAccount';
+import { withOnlyMembers } from '@polkadot/joy-utils/react/hocs/guards';
 
 // AppsProps is needed to get a location from the route.
 type Props = AppProps & ApiProps & I18nProps & {
@@ -81,15 +81,17 @@ class RevealVoteForm extends React.PureComponent<Props, State> {
             onChange={this.onChangeSalt}
           />
         </div>}
-        <Labelled style={{ marginTop: '.5rem' }}>
-          <TxButton
-            size='large'
-            isDisabled={!isVoteRevealed}
-            label='Reveal this vote'
-            params={[hashedVote, applicantId, salt]}
-            tx='councilElection.reveal'
-          />
-        </Labelled>
+        <div style={{ marginTop: '.5rem' }}>
+          <Labelled>
+            <TxButton
+              size='large'
+              isDisabled={!isVoteRevealed}
+              label='Reveal this vote'
+              params={[hashedVote, applicantId, salt]}
+              tx='councilElection.reveal'
+            />
+          </Labelled>
+        </div>
       </div>
     );
   }

+ 14 - 6
pioneer/packages/joy-election/src/SealedVote.tsx

@@ -1,28 +1,30 @@
 import React from 'react';
 import { Link } from 'react-router-dom';
-import { Table } from 'semantic-ui-react';
+import { Table, Message } from 'semantic-ui-react';
 
 import { I18nProps } from '@polkadot/react-components/types';
 import { ApiProps } from '@polkadot/react-api/types';
-import { withCalls } from '@polkadot/react-api/with';
+import { withCalls } from '@polkadot/react-api/hoc';
 import { Hash } from '@polkadot/types/interfaces';
 import { formatBalance } from '@polkadot/util';
 
 import translate from './translate';
-import { calcTotalStake } from '@polkadot/joy-utils/index';
+import { calcTotalStake } from '@polkadot/joy-utils/functions/misc';
 import { SealedVote } from '@joystream/types/council';
-import AddressMini from '@polkadot/react-components/AddressMiniJoy';
+import AddressMini from '@polkadot/react-components/AddressMini';
 import CandidatePreview from './CandidatePreview';
 import { findVoteByHash } from './myVotesStore';
 
 type Props = ApiProps & I18nProps & {
   hash: Hash;
   sealedVote?: SealedVote;
+  isStageRevealing: boolean;
+  isMyVote: boolean;
 };
 
 class Comp extends React.PureComponent<Props> {
   renderCandidateOrAction () {
-    const { hash, sealedVote } = this.props;
+    const { hash, sealedVote, isStageRevealing, isMyVote } = this.props;
     if (!sealedVote) {
       return <em>Unknown hashed vote: {hash.toHex()}</em>;
     }
@@ -30,10 +32,16 @@ class Comp extends React.PureComponent<Props> {
     if (sealedVote.vote.isSome) {
       const candidateId = sealedVote.vote.unwrap();
       return <CandidatePreview accountId={candidateId} />;
-    } else {
+    } else if (isStageRevealing && isMyVote) {
       const revealUrl = `/council/reveals?hashedVote=${hash.toHex()}`;
       return <Link to={revealUrl} className='ui button primary inverted'>Reveal this vote</Link>;
     }
+    else if (isMyVote) {
+      return <Message warning>Wait until <i><b>Revealing</b></i> stage in order to reveal this vote.</Message>
+    }
+    else {
+      return <Message info>This vote has not been revealed yet.</Message>
+    }
   }
 
   render () {

+ 18 - 11
pioneer/packages/joy-election/src/SealedVotes.tsx

@@ -4,19 +4,20 @@ import { Button } from 'semantic-ui-react';
 
 import { I18nProps } from '@polkadot/react-components/types';
 import { ApiProps } from '@polkadot/react-api/types';
-import { withCalls } from '@polkadot/react-api/with';
+import { withCalls } from '@polkadot/react-api/hoc';
 import { Hash } from '@polkadot/types/interfaces';
 
 import translate from './translate';
 import SealedVote from './SealedVote';
-import { queryToProp } from '@polkadot/joy-utils/index';
-import { MyAddressProps } from '@polkadot/joy-utils/MyAccount';
+import { queryToProp } from '@polkadot/joy-utils/functions/misc';
+import { MyAddressProps } from '@polkadot/joy-utils/react/hocs/accounts';
 import { SavedVote } from './myVotesStore';
-import Section from '@polkadot/joy-utils/Section';
+import Section from '@polkadot/joy-utils/react/components/Section';
 
 type Props = ApiProps & I18nProps & MyAddressProps & {
   myVotes?: SavedVote[];
   commitments?: Hash[];
+  isStageRevealing: boolean;
 };
 
 class Comp extends React.PureComponent<Props> {
@@ -28,9 +29,13 @@ class Comp extends React.PureComponent<Props> {
     return commitments.filter(x => myVotesOnly === isMyVote(x.toHex()));
   }
 
-  private renderVotes = (votes: Hash[]) => {
+  private renderVotes = (votes: Hash[], areVotesMine: boolean) => {
     return votes.map((hash, index) =>
-      <SealedVote key={index} hash={hash} />
+      <SealedVote
+        key={index}
+        hash={hash}
+        isStageRevealing={this.props.isStageRevealing}
+        isMyVote={areVotesMine}/>
     );
   }
 
@@ -39,17 +44,19 @@ class Comp extends React.PureComponent<Props> {
     const otherVotes = this.filterVotes(false);
 
     return <>
-      <Section title={`My previous votes (${myVotes.length})`}>{
+      <Section title={`My previous votes (${myVotes.length})`}>
+      {
         !myVotes.length
           ? <em>No votes by the current account found on the current browser.</em>
-          : this.renderVotes(myVotes)
-      }</Section>
+          : this.renderVotes(myVotes, true)
+      }
+      { this.props.isStageRevealing && <Button primary as={Link} to="reveals">Reveal other vote</Button> }
+      </Section>
       <Section title={`Other votes (${otherVotes.length})`}>
-        <Button primary as={Link} to="reveals">Reveal a vote</Button>
         {
           !otherVotes.length
             ? <em>No votes submitted by other accounts yet.</em>
-            : this.renderVotes(otherVotes)
+            : this.renderVotes(otherVotes, false)
         }
       </Section>
     </>;

+ 34 - 0
pioneer/packages/joy-election/src/SidebarSubtitle.tsx

@@ -0,0 +1,34 @@
+/** Component providing election stage subtitle for SideBar menu **/
+import React from 'react';
+import { ElectionStage } from '@joystream/types/council';
+import { Option } from '@polkadot/types/codec';
+import { useApi, useCall } from '@polkadot/react-hooks';
+import styled from 'styled-components';
+
+const colorByStage = {
+  Announcing: '#4caf50',
+  Voting: '#2196f3',
+  Revealing: '#ff5722'
+} as const;
+
+type StyledSubtitleProps = {
+  stage?: keyof typeof colorByStage;
+}
+const StyledSubtitle = styled.div`
+  display: block;
+  font-size: 0.85rem;
+  color: ${ (props: StyledSubtitleProps) => props.stage ? colorByStage[props.stage] : 'grey' };
+`;
+
+export default function SidebarSubtitle () {
+  const apiProps = useApi();
+  const electionStage = useCall<Option<ElectionStage>>(apiProps.isApiReady && apiProps.api.query.councilElection.stage, []);
+
+  if (electionStage) {
+    const stageName = electionStage.unwrapOr(undefined)?.type;
+    const text = stageName ? `${stageName} stage` : 'No active election';
+    return <StyledSubtitle stage={stageName}>{text}</StyledSubtitle>;
+  }
+
+  return null;
+}

+ 27 - 27
pioneer/packages/joy-election/src/VoteForm.tsx

@@ -6,7 +6,7 @@ import { Message, Table } from 'semantic-ui-react';
 
 import { AppProps, I18nProps } from '@polkadot/react-components/types';
 import { ApiProps } from '@polkadot/react-api/types';
-import { withCalls, withMulti } from '@polkadot/react-api/with';
+import { withCalls, withMulti } from '@polkadot/react-api/hoc';
 import { AccountId, Balance } from '@polkadot/types/interfaces';
 import { Button, Input, Labelled } from '@polkadot/react-components/index';
 import { SubmittableResult } from '@polkadot/api';
@@ -14,12 +14,13 @@ import { formatBalance } from '@polkadot/util';
 
 import translate from './translate';
 import { hashVote } from './utils';
-import { queryToProp, ZERO, getUrlParam, nonEmptyStr } from '@polkadot/joy-utils/index';
-import TxButton from '@polkadot/joy-utils/TxButton';
-import InputStake from '@polkadot/joy-utils/InputStake';
+import { queryToProp, ZERO, getUrlParam, nonEmptyStr } from '@polkadot/joy-utils/functions/misc';
+import TxButton from '@polkadot/joy-utils/react/components/TxButton';
+import InputStake from '@polkadot/joy-utils/react/components/InputStake';
 import CandidatePreview from './CandidatePreview';
-import { MyAccountProps, withOnlyMembers } from '@polkadot/joy-utils/MyAccount';
-import MembersDropdown from '@polkadot/joy-utils/MembersDropdown';
+import { MyAccountProps } from '@polkadot/joy-utils/react/hocs/accounts';
+import { withOnlyMembers } from '@polkadot/joy-utils/react/hocs/guards'
+import MembersDropdown from '@polkadot/joy-utils/react/components/MembersDropdown';
 import { saveVote, NewVote } from './myVotesStore';
 import { TxFailedCallback } from '@polkadot/react-components/Status/types';
 
@@ -103,14 +104,11 @@ class Component extends React.PureComponent<Props, State> {
               </Table.Row>
             </Table.Body>
           </Table>
-          <Labelled style={{ marginTop: '.5rem' }}>
-            <Button
-              size='large'
-              label='Submit another vote'
-              onClick={this.resetForm}
-              icon=''
-            />
-          </Labelled>
+          <Button
+            label='Submit another vote'
+            onClick={this.resetForm}
+            icon='arrow-left'
+          />
         </div>
 
       // New vote form:
@@ -137,7 +135,7 @@ class Component extends React.PureComponent<Props, State> {
               onChange={this.onChangeSalt}
             />
             <div className='medium' style={{ margin: '.5rem' }}>
-              <Button onClick={this.newRandomSalt} icon=''>Generate</Button>
+              <Button onClick={this.newRandomSalt} icon='cubes' label='Generate' />
               <Message compact warning size='tiny' content='You need to remember this salt!' />
             </div>
           </div>
@@ -148,18 +146,20 @@ class Component extends React.PureComponent<Props, State> {
               value={hashedVote}
             />
           </div>
-          <Labelled style={{ marginTop: '.5rem' }}>
-            <TxButton
-              size='large'
-              isDisabled={!isFormValid}
-              label='Submit my vote'
-              params={[hashedVote, stake]}
-              tx='councilElection.vote'
-              txStartCb={this.onFormSubmitted}
-              txFailedCb={this.onTxFailed}
-              txSuccessCb={(txResult: SubmittableResult) => this.onTxSuccess(buildNewVote() as NewVote, txResult)}
-            />
-          </Labelled>
+          <div style={{ marginTop: '.5rem' }}>
+            <Labelled>
+              <TxButton
+                size='large'
+                isDisabled={!isFormValid}
+                label='Submit my vote'
+                params={[hashedVote, stake]}
+                tx='councilElection.vote'
+                txStartCb={this.onFormSubmitted}
+                txFailedCb={this.onTxFailed}
+                txSuccessCb={(txResult: SubmittableResult) => this.onTxSuccess(buildNewVote() as NewVote, txResult)}
+              />
+            </Labelled>
+          </div>
         </div>}
       </>
     );

+ 28 - 7
pioneer/packages/joy-election/src/Votes.tsx

@@ -2,30 +2,51 @@ import React from 'react';
 
 import { AppProps, I18nProps } from '@polkadot/react-components/types';
 import { ApiProps } from '@polkadot/react-api/types';
+import { withCalls } from '@polkadot/react-api/hoc';
+import { Message } from 'semantic-ui-react';
+import { Option } from '@polkadot/types';
 
 import translate from './translate';
 import SealedVotes from './SealedVotes';
-import Section from '@polkadot/joy-utils/Section';
-import { withMyAccount, MyAccountProps } from '@polkadot/joy-utils/MyAccount';
+import Section from '@polkadot/joy-utils/react/components/Section';
+import { withMyAccount, MyAccountProps } from '@polkadot/joy-utils/react/hocs/accounts';
 import { getVotesByVoter } from './myVotesStore';
 import VoteForm from './VoteForm';
+import { queryToProp } from '@polkadot/joy-utils/functions/misc';
+import { ElectionStage } from '@joystream/types/src/council';
 
-type Props = AppProps & ApiProps & I18nProps & MyAccountProps & {};
+type Props = AppProps & ApiProps & I18nProps & MyAccountProps & {
+  stage?: Option<ElectionStage>;
+};
 
 class Component extends React.PureComponent<Props> {
   render () {
-    const { myAddress } = this.props;
+    const { myAddress, stage } = this.props;
     const myVotes = myAddress ? getVotesByVoter(myAddress) : [];
 
     return <>
       <Section title='My vote'>
-        <VoteForm {...this.props} myAddress={myAddress} />
+      { stage?.unwrapOr(undefined)?.isOfType('Voting')
+          ? (
+            <VoteForm {...this.props} myAddress={myAddress} />
+          )
+          : (
+            <Message warning>
+              Voting is only possible during <i><b>Voting</b></i> stage.
+            </Message>
+          )
+      }
       </Section>
-      <SealedVotes myAddress={myAddress} myVotes={myVotes} />
+      <SealedVotes
+        isStageRevealing={stage?.unwrapOr(undefined)?.isOfType('Revealing') || false}
+        myAddress={myAddress}
+        myVotes={myVotes} />
     </>;
   }
 }
 
 export default translate(
-  withMyAccount(Component)
+  withCalls<Props>(
+    queryToProp('query.councilElection.stage')
+  )(withMyAccount(Component))
 );

+ 0 - 28
pioneer/packages/joy-election/src/index.css

@@ -1,28 +0,0 @@
-
-.JoyElection--NotRunning {
-  /* nothing yet */
-}
-.JoyElection--Running {
-  font-style: italic;
-  color: green;
-}
-.SealedVoteTable {
-  -webkit-box-shadow: 0 1px 2px 0 rgba(34,36,38,.15) !important;
-  box-shadow: 0 1px 2px 0 rgba(34,36,38,.15) !important;
-  tr td:first-child {
-    color: #999 !important;
-    font-weight: normal !important;
-  }
-}
-
-.SidebarSubtitle {
-  &.Announcing {
-    color: #4caf50; /* green */
-  }
-  &.Voting {
-    color: #2196f3; /* blue */
-  }
-  &.Revealing {
-    color: #ff5722; /* red */
-  }
-}

+ 13 - 9
pioneer/packages/joy-election/src/index.tsx

@@ -1,14 +1,16 @@
 import React from 'react';
 import { Route, Switch } from 'react-router';
 
-import { AppProps, I18nProps } from '@polkadot/react-components/types';
-import { ApiProps } from '@polkadot/react-api/types';
-import { withCalls } from '@polkadot/react-api/with';
+import { I18nProps } from '@polkadot/react-components/types';
+import { RouteProps } from '@polkadot/apps-routing/types';
+import { withCalls } from '@polkadot/react-api/hoc';
 import { AccountId, Hash } from '@polkadot/types/interfaces';
-import Tabs, { TabItem } from '@polkadot/react-components/Tabs';
+import Tabs from '@polkadot/react-components/Tabs';
+import { TabItem } from '@polkadot/react-components/Tabs/types';
 
 // our app-specific styles
-import './index.css';
+import style from './style';
+import styled from 'styled-components';
 
 // local imports and components
 import translate from './translate';
@@ -17,11 +19,13 @@ import Council from './Council';
 import Applicants from './Applicants';
 import Votes from './Votes';
 import Reveals from './Reveals';
-import { queryToProp } from '@polkadot/joy-utils/index';
+import { queryToProp } from '@polkadot/joy-utils/functions/misc';
 import { Seat } from '@joystream/types/council';
 
+const ElectionMain = styled.main`${style}`;
+
 // define out internal types
-type Props = AppProps & ApiProps & I18nProps & {
+type Props = RouteProps & I18nProps & {
   activeCouncil?: Seat[];
   applicants?: AccountId[];
   commitments?: Hash[];
@@ -59,7 +63,7 @@ class App extends React.PureComponent<Props, State> {
     const { basePath } = this.props;
     const tabs = this.buildTabs();
     return (
-      <main className='election--App'>
+      <ElectionMain className='election--App'>
         <header>
           <Tabs basePath={basePath} items={tabs} />
         </header>
@@ -70,7 +74,7 @@ class App extends React.PureComponent<Props, State> {
           <Route path={`${basePath}/reveals`} component={Reveals} />
           <Route component={Dashboard} />
         </Switch>
-      </main>
+      </ElectionMain>
     );
   }
 }

+ 1 - 1
pioneer/packages/joy-election/src/myVotesStore.ts

@@ -1,5 +1,5 @@
 import store from 'store';
-import { nonEmptyArr } from '@polkadot/joy-utils/index';
+import { nonEmptyArr } from '@polkadot/joy-utils/functions/misc';
 
 const MY_VOTES = 'joy.myVotes';
 

+ 21 - 0
pioneer/packages/joy-election/src/style.ts

@@ -0,0 +1,21 @@
+import { css } from 'styled-components';
+
+const style = css`
+  .JoyElection--NotRunning {
+    /* nothing yet */
+  }
+  .JoyElection--Running {
+    font-style: italic;
+    color: green;
+  }
+  .SealedVoteTable {
+    -webkit-box-shadow: 0 1px 2px 0 rgba(34,36,38,.15) !important;
+    box-shadow: 0 1px 2px 0 rgba(34,36,38,.15) !important;
+    tr td:first-child {
+      color: #999 !important;
+      font-weight: normal !important;
+    }
+  }
+`;
+
+export default style;

+ 2 - 1
pioneer/packages/joy-election/src/utils.tsx

@@ -1,10 +1,11 @@
+// TODO: Move to joy-utils?
 import { AccountId } from '@polkadot/types/interfaces';
 
 // Keyring / identicon / address
 // -----------------------------------
 
 import createItem from '@polkadot/ui-keyring/options/item';
-import { findNameByAddress } from '@polkadot/joy-utils/index';
+import { findNameByAddress } from '@polkadot/joy-utils/functions/misc';
 
 // Hash
 // -----------------------------------

+ 0 - 35
pioneer/packages/joy-utils-old/src/InputStake.tsx

@@ -1,35 +0,0 @@
-import BN from 'bn.js';
-import React from 'react';
-import { InputBalance, Bubble } from '@polkadot/react-components/index';
-import { formatBalance } from '@polkadot/util';
-
-type Props = {
-  label?: string;
-  min?: BN;
-  isValid?: boolean;
-  onChange: (stake?: BN) => void;
-};
-
-export default class Component extends React.PureComponent<Props> {
-  render () {
-    const { min, label, isValid, onChange } = this.props;
-    return (
-      <div className='ui--row'>
-        <InputBalance
-          className='medium'
-          label={label || 'Amount to be staked:'}
-          onChange={onChange}
-        />
-        {min && !min.isZero() && <div className='medium' style={{ marginLeft: '.5rem' }}>
-          <Bubble
-            className={`left pointing ${isValid ? 'ok' : 'warn'}`}
-            icon={isValid ? 'check' : 'warning sign'}
-            label='Minimum stake'
-          >
-            {formatBalance(min)}
-          </Bubble>
-        </div>}
-      </div>
-    );
-  }
-}

+ 0 - 48
pioneer/packages/joy-utils-old/src/MemberByAccountPreview.tsx

@@ -1,48 +0,0 @@
-import React, { useEffect, useState, useContext } from 'react';
-
-import { Loader } from 'semantic-ui-react';
-import { ApiContext } from '@polkadot/react-api';
-import ProfilePreview from './MemberProfilePreview';
-import { AccountId } from '@polkadot/types/interfaces';
-import { memberFromAccount, MemberFromAccount } from './accounts';
-
-import styled from 'styled-components';
-
-const MemberByAccount = styled.div``;
-
-type Props = {
-  accountId: AccountId | string;
-};
-
-const MemberByAccountPreview: React.FunctionComponent<Props> = ({ accountId }) => {
-  const { api } = useContext(ApiContext);
-  const [member, setMember] = useState(null as MemberFromAccount | null);
-  useEffect(
-    () => {
-      let isSubscribed = true;
-      memberFromAccount(api, accountId).then(member => isSubscribed && setMember(member));
-      return () => { isSubscribed = false; };
-    },
-    [accountId]
-  );
-
-  return (
-    <MemberByAccount>
-      { member
-        ? (
-          member.profile
-            ? <ProfilePreview
-              avatar_uri={member.profile.avatar_uri.toString()}
-              root_account={member.profile.root_account.toString()}
-              handle={member.profile.handle.toString()}
-              id={member.id}
-              link={true}/>
-            : 'Member profile not found!'
-        )
-        : <Loader inline active>Fetching member profile...</Loader>
-      }
-    </MemberByAccount>
-  );
-};
-
-export default MemberByAccountPreview;

+ 0 - 107
pioneer/packages/joy-utils-old/src/transport/base.ts

@@ -1,107 +0,0 @@
-import { ApiPromise } from '@polkadot/api';
-import { Codec } from '@polkadot/types/types';
-import { APIQueryCache } from '../APIQueryCache';
-
-export default abstract class BaseTransport {
-  protected api: ApiPromise;
-  protected cacheApi: APIQueryCache;
-
-  constructor (api: ApiPromise, cacheApi: APIQueryCache) {
-    this.api = api;
-    this.cacheApi = cacheApi;
-  }
-
-  protected get proposalsEngine () {
-    return this.cacheApi.query.proposalsEngine;
-  }
-
-  protected get proposalsCodex () {
-    return this.cacheApi.query.proposalsCodex;
-  }
-
-  protected get proposalsDiscussion () {
-    return this.cacheApi.query.proposalsDiscussion;
-  }
-
-  protected get members () {
-    return this.cacheApi.query.members;
-  }
-
-  protected get council () {
-    return this.cacheApi.query.council;
-  }
-
-  protected get councilElection () {
-    return this.cacheApi.query.councilElection;
-  }
-
-  protected get actors () {
-    return this.cacheApi.query.actors;
-  }
-
-  protected get contentWorkingGroup () {
-    return this.cacheApi.query.contentWorkingGroup;
-  }
-
-  protected get minting () {
-    return this.cacheApi.query.minting;
-  }
-
-  protected get hiring () {
-    return this.cacheApi.query.hiring;
-  }
-
-  protected get stake () {
-    return this.cacheApi.query.stake;
-  }
-
-  protected get recurringRewards () {
-    return this.cacheApi.query.recurringRewards;
-  }
-
-  protected queryMethodByName (name: string) {
-    const [module, method] = name.split('.');
-    return this.api.query[module][method];
-  }
-
-  // Fetch all double map entries using only the first key
-  //
-  // TODO: FIXME: This may be a risky implementation, because it relies on a few assumptions about how the data is stored etc.
-  // With the current runtime version we can rely on the fact that all storage keys for double-map values start with the same
-  // 32-bytes prefix assuming a given (fixed) value of the first key (ie. for all values like map[x][y], the storage key starts
-  // with the same prefix as long as x remains the same. Changing y will not affect this prefix)
-  protected async doubleMapEntries<T extends Codec> (
-    methodName: string,
-    firstKey: Codec,
-    valueConverter: (hex: string) => T,
-    getEntriesCount: () => Promise<number>,
-    secondKeyStart = 1
-  ): Promise<{ secondKey: number; value: T}[]> {
-    // Get prefix and storage keys of all entries
-    const firstEntryStorageKey = this.queryMethodByName(methodName).key(firstKey, secondKeyStart);
-    const entryStorageKeyPrefix = firstEntryStorageKey.substr(0, 66); // "0x" + 64 hex characters (32 bytes)
-    const allEntriesStorageKeys = await this.api.rpc.state.getKeys(entryStorageKeyPrefix);
-
-    // Create storageKey-to-secondKey map
-    const maxSecondKey = (await getEntriesCount()) - 1 + secondKeyStart;
-    const storageKeyToSecondKey: { [key: string]: number } = {};
-    for (let secondKey = secondKeyStart; secondKey <= maxSecondKey; ++secondKey) {
-      const storageKey = this.queryMethodByName(methodName).key(firstKey, secondKey);
-      storageKeyToSecondKey[storageKey] = secondKey;
-    }
-
-    // Create the resulting entries array
-    const entries: { secondKey: number; value: T }[] = [];
-    for (const key of allEntriesStorageKeys) {
-      const value: any = await this.api.rpc.state.getStorage(key);
-      if (typeof value === 'object' && value !== null && value.raw) {
-        entries.push({
-          secondKey: storageKeyToSecondKey[key.toString()],
-          value: valueConverter(value.raw.toString())
-        });
-      }
-    }
-
-    return entries;
-  }
-}

+ 0 - 34
pioneer/packages/joy-utils-old/src/transport/index.ts

@@ -1,34 +0,0 @@
-import { ApiPromise } from '@polkadot/api';
-import ChainTransport from './chain';
-import ContentWorkingGroupTransport from './contentWorkingGroup';
-import ProposalsTransport from './proposals';
-import MembersTransport from './members';
-import CouncilTransport from './council';
-import ValidatorsTransport from './validators';
-import WorkingGroupsTransport from './workingGroups';
-import { APIQueryCache } from '../APIQueryCache';
-
-export default class Transport {
-  protected api: ApiPromise;
-  protected cacheApi: APIQueryCache;
-  // Specific transports
-  public chain: ChainTransport;
-  public members: MembersTransport;
-  public council: CouncilTransport;
-  public proposals: ProposalsTransport;
-  public contentWorkingGroup: ContentWorkingGroupTransport;
-  public validators: ValidatorsTransport;
-  public workingGroups: WorkingGroupsTransport;
-
-  constructor (api: ApiPromise) {
-    this.api = api;
-    this.cacheApi = new APIQueryCache(api);
-    this.chain = new ChainTransport(api, this.cacheApi);
-    this.members = new MembersTransport(api, this.cacheApi);
-    this.validators = new ValidatorsTransport(api, this.cacheApi);
-    this.council = new CouncilTransport(api, this.cacheApi, this.members, this.chain);
-    this.contentWorkingGroup = new ContentWorkingGroupTransport(api, this.cacheApi, this.members);
-    this.proposals = new ProposalsTransport(api, this.cacheApi, this.members, this.chain, this.council);
-    this.workingGroups = new WorkingGroupsTransport(api, this.cacheApi, this.members);
-  }
-}

+ 46 - 0
pioneer/packages/joy-utils/src/react/components/InputStake.tsx

@@ -0,0 +1,46 @@
+import BN from 'bn.js';
+import React from 'react';
+import { InputBalance } from '@polkadot/react-components/index';
+import { Label } from 'semantic-ui-react';
+import { formatBalance } from '@polkadot/util';
+
+type Props = {
+  label?: string;
+  min?: BN;
+  isValid?: boolean;
+  onChange: (stake?: BN) => void;
+};
+
+export default class Component extends React.PureComponent<Props> {
+  render () {
+    const { min, label, isValid, onChange } = this.props;
+    return (
+      <div className='ui--row' style={{ display: 'flex' }}>
+        <InputBalance
+          className='medium'
+          label={label || 'Amount to be staked:'}
+          onChange={onChange}
+        />
+        {min && !min.isZero() && (
+          <div
+            className='medium'
+            style={{
+              marginLeft: '.5rem',
+              display: 'flex',
+              alignItems: 'center'
+            }}>
+            <Label
+              color={ isValid ? 'green' : 'red' }
+              icon={isValid ? 'check' : 'warning sign'}
+              label='Minimum stake'
+              pointing="left"
+            >
+              Minimum stake
+              <Label.Detail>{formatBalance(min)}</Label.Detail>
+            </Label>
+          </div>
+        ) }
+      </div>
+    );
+  }
+}

+ 43 - 0
pioneer/packages/joy-utils/src/react/components/MemberByAccountPreview.tsx

@@ -0,0 +1,43 @@
+import React from 'react';
+
+import ProfilePreview from './MemberProfilePreview';
+import { AccountId } from '@polkadot/types/interfaces';
+import { MemberFromAccount } from '../../types/members';
+import { useTransport, usePromise } from '../hooks';
+
+import styled from 'styled-components';
+import PromiseComponent from './PromiseComponent';
+
+const MemberByAccount = styled.div``;
+
+type Props = {
+  accountId: AccountId | string;
+};
+
+const MemberByAccountPreview: React.FunctionComponent<Props> = ({ accountId }) => {
+  const transport = useTransport();
+  const [member, error, loading] = usePromise<MemberFromAccount | null>(
+    () => transport.members.membershipFromAccount(accountId),
+    null,
+    [accountId]
+  );
+
+  return (
+    <PromiseComponent error={error} loading={loading} message="Fetching member profile...">
+      <MemberByAccount>
+        { member && (
+          member.profile
+            ? <ProfilePreview
+              avatar_uri={member.profile.avatar_uri.toString()}
+              root_account={member.profile.root_account.toString()}
+              handle={member.profile.handle.toString()}
+              id={member.memberId}
+              link={true}/>
+            : 'Member profile not found!'
+        ) }
+      </MemberByAccount>
+    </PromiseComponent>
+  );
+};
+
+export default MemberByAccountPreview;

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


+ 7 - 7
pioneer/packages/joy-utils-old/src/MembersDropdown.tsx → pioneer/packages/joy-utils/src/react/components/MembersDropdown.tsx

@@ -1,9 +1,9 @@
-import React, { useEffect, useState, useContext } from 'react';
+import React, { useEffect, useState } from 'react';
 import { Dropdown, DropdownItemProps, DropdownProps } from 'semantic-ui-react';
 import { Membership } from '@joystream/types/members';
-import { memberFromAccount, MemberFromAccount } from './accounts';
+import { MemberFromAccount } from '../../types/members';
+import { useTransport } from '../hooks';
 import { AccountId } from '@polkadot/types/interfaces';
-import { ApiContext } from '@polkadot/react-api';
 import styled from 'styled-components';
 
 const StyledMembersDropdown = styled(Dropdown)`
@@ -16,9 +16,9 @@ const StyledMembersDropdown = styled(Dropdown)`
 function membersToOptions (members: MemberFromAccount[]) {
   const validMembers = members.filter(m => m.profile !== undefined) as (MemberFromAccount & { profile: Membership })[];
   return validMembers
-    .map(({ id, profile, account }) => ({
+    .map(({ memberId, profile, account }) => ({
       key: profile.handle,
-      text: `${profile.handle} (id:${id})`,
+      text: `${profile.handle} (id:${memberId})`,
       value: account,
       image: profile.avatar_uri.toString() ? { avatar: true, src: profile.avatar_uri } : null
     }));
@@ -33,7 +33,7 @@ type Props = {
 };
 
 const MembersDropdown: React.FunctionComponent<Props> = ({ accounts, ...passedProps }) => {
-  const { api } = useContext(ApiContext);
+  const transport = useTransport();
   // State
   const [loading, setLoading] = useState(true);
   const [membersOptions, setMembersOptions] = useState([] as DropdownItemProps[]);
@@ -41,7 +41,7 @@ const MembersDropdown: React.FunctionComponent<Props> = ({ accounts, ...passedPr
   useEffect(() => {
     let isSubscribed = true;
     Promise
-      .all(accounts.map(acc => memberFromAccount(api, acc)))
+      .all(accounts.map(acc => transport.members.membershipFromAccount(acc)))
       .then(members => {
         if (isSubscribed) {
           setMembersOptions(membersToOptions(members));

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


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

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

+ 0 - 0
pioneer/packages/joy-utils-old/src/react/context/transport.tsx → pioneer/packages/joy-utils/src/react/context/transport.tsx


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

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

+ 0 - 0
pioneer/packages/joy-utils-old/src/react/hooks/usePromise.tsx → pioneer/packages/joy-utils/src/react/hooks/usePromise.tsx


+ 0 - 0
pioneer/packages/joy-utils-old/src/react/hooks/useTransport.tsx → pioneer/packages/joy-utils/src/react/hooks/useTransport.tsx


+ 0 - 0
pioneer/packages/joy-utils-old/src/APIQueryCache.ts → pioneer/packages/joy-utils/src/transport/APIQueryCache.ts


+ 66 - 0
pioneer/packages/joy-utils/src/transport/base.ts

@@ -0,0 +1,66 @@
+import { ApiPromise } from '@polkadot/api';
+import { Codec } from '@polkadot/types/types';
+import { APIQueryCache } from './APIQueryCache';
+
+export default abstract class BaseTransport {
+  protected api: ApiPromise;
+  protected cacheApi: APIQueryCache;
+
+  constructor (api: ApiPromise, cacheApi: APIQueryCache) {
+    this.api = api;
+    this.cacheApi = cacheApi;
+  }
+
+  protected get proposalsEngine () {
+    return this.cacheApi.query.proposalsEngine;
+  }
+
+  protected get proposalsCodex () {
+    return this.cacheApi.query.proposalsCodex;
+  }
+
+  protected get proposalsDiscussion () {
+    return this.cacheApi.query.proposalsDiscussion;
+  }
+
+  protected get members () {
+    return this.cacheApi.query.members;
+  }
+
+  protected get council () {
+    return this.cacheApi.query.council;
+  }
+
+  protected get councilElection () {
+    return this.cacheApi.query.councilElection;
+  }
+
+  protected get actors () {
+    return this.cacheApi.query.actors;
+  }
+
+  protected get contentWorkingGroup () {
+    return this.cacheApi.query.contentWorkingGroup;
+  }
+
+  protected get minting () {
+    return this.cacheApi.query.minting;
+  }
+
+  protected get hiring () {
+    return this.cacheApi.query.hiring;
+  }
+
+  protected get stake () {
+    return this.cacheApi.query.stake;
+  }
+
+  protected get recurringRewards () {
+    return this.cacheApi.query.recurringRewards;
+  }
+
+  protected queryMethodByName (name: string) {
+    const [module, method] = name.split('.');
+    return this.api.query[module][method];
+  }
+}

+ 34 - 0
pioneer/packages/joy-utils/src/transport/index.ts

@@ -0,0 +1,34 @@
+import { ApiPromise } from '@polkadot/api';
+// import ChainTransport from './chain';
+// import ContentWorkingGroupTransport from './contentWorkingGroup';
+// import ProposalsTransport from './proposals';
+import MembersTransport from './members';
+// import CouncilTransport from './council';
+// import ValidatorsTransport from './validators';
+// import WorkingGroupsTransport from './workingGroups';
+import { APIQueryCache } from './APIQueryCache';
+
+export default class Transport {
+  protected api: ApiPromise;
+  protected cacheApi: APIQueryCache;
+  // Specific transports
+  // public chain: ChainTransport;
+  public members: MembersTransport;
+  // public council: CouncilTransport;
+  // public proposals: ProposalsTransport;
+  // public contentWorkingGroup: ContentWorkingGroupTransport;
+  // public validators: ValidatorsTransport;
+  // public workingGroups: WorkingGroupsTransport;
+
+  constructor (api: ApiPromise) {
+    this.api = api;
+    this.cacheApi = new APIQueryCache(api);
+    // this.chain = new ChainTransport(api, this.cacheApi);
+    this.members = new MembersTransport(api, this.cacheApi);
+    // this.validators = new ValidatorsTransport(api, this.cacheApi);
+    // this.council = new CouncilTransport(api, this.cacheApi, this.members, this.chain);
+    // this.contentWorkingGroup = new ContentWorkingGroupTransport(api, this.cacheApi, this.members);
+    // this.proposals = new ProposalsTransport(api, this.cacheApi, this.members, this.chain, this.council);
+    // this.workingGroups = new WorkingGroupsTransport(api, this.cacheApi, this.members);
+  }
+}

+ 16 - 0
pioneer/packages/joy-utils-old/src/transport/members.ts → pioneer/packages/joy-utils/src/transport/members.ts

@@ -1,5 +1,8 @@
 import BaseTransport from './base';
 import { MemberId, Membership } from '@joystream/types/members';
+import { Vec } from '@polkadot/types/codec';
+import { AccountId } from '@polkadot/types/interfaces';
+import { MemberFromAccount } from '../types/members';
 
 export default class MembersTransport extends BaseTransport {
   async membershipById (id: MemberId | number): Promise<Membership | null> {
@@ -21,4 +24,17 @@ export default class MembersTransport extends BaseTransport {
   async nextMemberId (): Promise<number> {
     return (await this.members.nextMemberId() as MemberId).toNumber();
   }
+
+  async membershipFromAccount (accountId: AccountId | string): Promise<MemberFromAccount> {
+    const memberIdsRoot = (await this.members.memberIdsByRootAccountId(accountId)) as Vec<MemberId>;
+    const memberIdsController = (await this.members.memberIdsByControllerAccountId(accountId)) as Vec<MemberId>;
+    const memberId: MemberId | undefined = memberIdsRoot.toArray().concat(memberIdsController.toArray())[0];
+    const profile = memberId ? await this.expectedMembership(memberId) : undefined;
+
+    return {
+      account: accountId.toString(),
+      memberId: memberId && memberId.toNumber(),
+      profile,
+    };
+  }
 }

+ 4 - 0
pioneer/packages/joy-utils-old/src/types/members.ts → pioneer/packages/joy-utils/src/types/members.ts

@@ -1,3 +1,5 @@
+import { Membership } from '@joystream/types/members';
+
 export type ParsedMember = {
   about: string;
   avatar_uri: string;
@@ -10,3 +12,5 @@ export type ParsedMember = {
   subscription: any;
   suspended: boolean;
 };
+
+export type MemberFromAccount = { account: string; memberId?: number; profile?: Membership };

+ 0 - 19
pioneer/packages/old-apps/apps-routing/src/joy-election.ts

@@ -1,19 +0,0 @@
-import { Routes } from './types';
-
-import Election from '@polkadot/joy-election/index';
-
-export const councilSidebarName = 'council';
-
-export default [
-  {
-    Component: Election,
-    display: {
-      needsApi: ['query.council.activeCouncil', 'query.councilElection.stage']
-    },
-    i18n: {
-      defaultValue: 'Council'
-    },
-    icon: 'university',
-    name: councilSidebarName
-  }
-] as Routes;

+ 35 - 58
pioneer/packages/react-components/src/styles/joystream.ts

@@ -31,60 +31,6 @@ export default css`
     }
   }
 
-  .ui--AddressMini {
-    .ui--IdentityIcon {
-      margin-left: 0;
-    }
-    .ui--AddressMini-info {
-      display: block;
-    }
-    .ui--AddressMini-details {
-      display: block;
-      text-align: left;
-    }
-    .ui--AddressSummary-name,
-    .ui--AddressSummary-balance,
-    .ui--AddressSummary-memo {
-      display: inline-block;
-      margin-top: 0;
-      padding-top: 0;
-      font-size: 0.8rem !important;
-      margin-right: 1rem;
-      font-weight: 100;
-      opacity: 0.8;
-      overflow: hidden;
-    }
-  }
-
-  .ui--Bubble.ui.label {
-    background-color: #f2f2f2;
-    margin: 0.25rem 0;
-    margin-right: 0.5rem;
-
-    &.pointing:before {
-      background-color: #e6e6e6;
-    }
-    &.warn {
-      color: #f2711c !important;
-      border: 1px solid;
-    }
-    &.ok {
-      color: #21ba45 !important;
-      border: 1px solid;
-    }
-  }
-
-  .SidebarItem {
-    display: inline-flex;
-    flex-direction: column;
-
-    .SidebarSubtitle {
-      display: block;
-      font-size: 0.85rem;
-      color: grey;
-    }
-  }
-
   .JoySection {
     margin: 2rem 0;
 
@@ -185,19 +131,50 @@ export default css`
     color: #3b83c0;
   }
 
-  /* Overrides */
+  /* Remove IdentityIcon border (not working well for members list) */
   .ui--IdentityIcon {
     border: none !important;
   }
-  /* Normalize SideBar icons width */
-  .apps--SideBar-Item-NavLink svg {
-    width: 20px !important;
+
+  .apps--SideBar-Item-NavLink {
+    /* Normalize SideBar icons width */
+    svg {
+      width: 20px !important;
+    }
+    /* Display SideBar subtitle below title */
+    .text {
+      display: inline-flex;
+      flex-direction: column;
+    }
   }
+
   /* Fix "collapsed" sidebar on mobile */
   .apps--Wrapper:not(.menu-open) .apps--SideBar-Scroll {
     padding: 0 !important;
   }
+
+  /* Turn off global text-transform on h1 */
   h1 {
     text-transform: none;
   }
+
+  /* AddressMini customization */
+  .ui--AddressMini {
+    display: grid;
+    grid-template-rows: auto auto;
+    grid-template-columns: min-content min-content;
+    .ui--AddressMini-icon {
+      grid-row: 1/3;
+      grid-column: 2/3;
+      align-self: center;
+    }
+    .ui--AddressMini-balances .ui--FormatBalance {
+      font-size: 1rem !important;
+      margin: 0 !important;
+    }
+    .ui--AddressMini-info {
+      min-width: 10em;
+      max-width: 10em;
+    }
+  }
 `;

+ 2 - 3
pioneer/tsconfig.json

@@ -4,7 +4,6 @@
     "build/**/*",
     "**/build/**/*",
     "packages/old-apps/**",
-    "packages/joy-election/**/*",
     "packages/joy-forum/**/*",
     "packages/joy-help/**/*",
     "packages/joy-media/**/*",
@@ -23,8 +22,8 @@
       "@polkadot/types/augment": [ "../types/src/definitions/augment-types.ts" ],
       // "@joystream/types/": [ "../types/src/" ],
       // "@joystream/types/*": [ "../types/src/*" ],
-      // "@polkadot/joy-election/": [ "packages/joy-election/src/" ],
-      // "@polkadot/joy-election/*": [ "packages/joy-election/src/*" ],
+      "@polkadot/joy-election/": [ "packages/joy-election/src/" ],
+      "@polkadot/joy-election/*": [ "packages/joy-election/src/*" ],
       // "@polkadot/joy-forum/": [ "packages/joy-forum/src/" ],
       // "@polkadot/joy-forum/*": [ "packages/joy-forum/src/*" ],
       // "@polkadot/joy-help/": [ "packages/joy-help/src/" ],