Parcourir la source

Merge remote-tracking branch 'joystream/nicaea' into bureaucracy

# Conflicts:
#	Cargo.lock
#	runtime/Cargo.toml
#	runtime/src/lib.rs
Shamil Gadelshin il y a 4 ans
Parent
commit
67cf5ee232
100 fichiers modifiés avec 1234 ajouts et 1245 suppressions
  1. 20 20
      .github/workflows/pioneer-pr.yml
  2. 6 0
      .gitignore
  3. 2 2
      Cargo.lock
  4. 11 2
      README.md
  5. 1 1
      cli/package.json
  6. 1 1
      node/Cargo.toml
  7. 1 1
      package.json
  8. 5 1
      pioneer/.eslintrc.js
  9. 0 32
      pioneer/.github/workflows/push-master.yml
  10. 1 1
      pioneer/babel.config.js
  11. 3 1
      pioneer/package.json
  12. 3 4
      pioneer/packages/app-accounts/src/MemoForm.tsx
  13. 2 2
      pioneer/packages/app-accounts/src/Overview.tsx
  14. 2 2
      pioneer/packages/app-accounts/src/modals/Create.tsx
  15. 4 4
      pioneer/packages/app-accounts/src/modals/Derive.tsx
  16. 1 1
      pioneer/packages/app-accounts/src/modals/Import.tsx
  17. 2 2
      pioneer/packages/app-accounts/src/modals/Qr.tsx
  18. 6 7
      pioneer/packages/app-address-book/src/MemoByAccount.tsx
  19. 14 4
      pioneer/packages/app-explorer/src/SummarySession.tsx
  20. 2 2
      pioneer/packages/app-staking/src/Actions/Accounts.tsx
  21. 3 3
      pioneer/packages/app-staking/src/Overview/CurrentList.tsx
  22. 5 5
      pioneer/packages/app-staking/src/Overview/index.tsx
  23. 1 1
      pioneer/packages/app-staking/src/index.tsx
  24. 4 3
      pioneer/packages/apps-routing/src/index.ts
  25. 6 2
      pioneer/packages/apps-routing/src/joy-pages.ts
  26. 4 4
      pioneer/packages/apps/src/SideBar/Item.tsx
  27. 9 9
      pioneer/packages/apps/src/SideBar/index.tsx
  28. 2 2
      pioneer/packages/apps/src/TopBar.tsx
  29. 12 12
      pioneer/packages/apps/webpack.config.js
  30. 4 5
      pioneer/packages/joy-election/src/Applicant.tsx
  31. 17 18
      pioneer/packages/joy-election/src/Applicants.tsx
  32. 5 6
      pioneer/packages/joy-election/src/ApplyForm.tsx
  33. 4 4
      pioneer/packages/joy-election/src/CandidatePreview.tsx
  34. 17 17
      pioneer/packages/joy-election/src/Council.tsx
  35. 20 21
      pioneer/packages/joy-election/src/Dashboard.tsx
  36. 4 5
      pioneer/packages/joy-election/src/Reveals.tsx
  37. 29 30
      pioneer/packages/joy-election/src/SealedVote.tsx
  38. 6 7
      pioneer/packages/joy-election/src/SealedVotes.tsx
  39. 91 92
      pioneer/packages/joy-election/src/VoteForm.tsx
  40. 6 7
      pioneer/packages/joy-election/src/index.tsx
  41. 8 8
      pioneer/packages/joy-election/src/myVotesStore.ts
  42. 14 14
      pioneer/packages/joy-election/src/utils.tsx
  43. 58 58
      pioneer/packages/joy-forum/src/CategoryList.tsx
  44. 42 45
      pioneer/packages/joy-forum/src/Context.tsx
  45. 10 10
      pioneer/packages/joy-forum/src/EditCategory.tsx
  46. 16 16
      pioneer/packages/joy-forum/src/EditReply.tsx
  47. 10 10
      pioneer/packages/joy-forum/src/EditThread.tsx
  48. 23 23
      pioneer/packages/joy-forum/src/ForumSudo.tsx
  49. 6 6
      pioneer/packages/joy-forum/src/Moderate.tsx
  50. 4 4
      pioneer/packages/joy-forum/src/ViewReply.tsx
  51. 31 31
      pioneer/packages/joy-forum/src/ViewThread.tsx
  52. 1 3
      pioneer/packages/joy-forum/src/calls.tsx
  53. 24 25
      pioneer/packages/joy-forum/src/index.tsx
  54. 21 21
      pioneer/packages/joy-forum/src/utils.tsx
  55. 11 8
      pioneer/packages/joy-forum/src/validation.tsx
  56. 1 1
      pioneer/packages/joy-help/src/Help.tsx
  57. 0 1
      pioneer/packages/joy-help/src/index.tsx
  58. 52 46
      pioneer/packages/joy-media/src/DiscoveryProvider.tsx
  59. 48 48
      pioneer/packages/joy-media/src/IterableFile.ts
  60. 29 33
      pioneer/packages/joy-media/src/MediaView.tsx
  61. 12 15
      pioneer/packages/joy-media/src/TransportContext.tsx
  62. 42 41
      pioneer/packages/joy-media/src/Upload.tsx
  63. 5 5
      pioneer/packages/joy-media/src/channels/ChannelAvatar.tsx
  64. 4 4
      pioneer/packages/joy-media/src/channels/ChannelAvatarAndName.tsx
  65. 1 1
      pioneer/packages/joy-media/src/channels/ChannelHeader.tsx
  66. 9 9
      pioneer/packages/joy-media/src/channels/ChannelHelpers.ts
  67. 5 5
      pioneer/packages/joy-media/src/channels/ChannelNameAsLink.tsx
  68. 12 12
      pioneer/packages/joy-media/src/channels/ChannelPreview.tsx
  69. 6 6
      pioneer/packages/joy-media/src/channels/ChannelPreviewStats.tsx
  70. 11 11
      pioneer/packages/joy-media/src/channels/ChannelsByOwner.tsx
  71. 3 3
      pioneer/packages/joy-media/src/channels/ChannelsByOwner.view.tsx
  72. 15 15
      pioneer/packages/joy-media/src/channels/CurationPanel.tsx
  73. 20 24
      pioneer/packages/joy-media/src/channels/EditChannel.tsx
  74. 6 6
      pioneer/packages/joy-media/src/channels/EditChannel.view.tsx
  75. 7 7
      pioneer/packages/joy-media/src/channels/ViewChannel.tsx
  76. 4 4
      pioneer/packages/joy-media/src/channels/ViewChannel.view.tsx
  77. 14 14
      pioneer/packages/joy-media/src/channels/ViewMusicChannel.tsx
  78. 10 10
      pioneer/packages/joy-media/src/channels/ViewVideoChannel.tsx
  79. 7 7
      pioneer/packages/joy-media/src/channels/YouHaveNoChannels.tsx
  80. 11 11
      pioneer/packages/joy-media/src/common/BgImg.tsx
  81. 10 11
      pioneer/packages/joy-media/src/common/FormTabs.tsx
  82. 3 4
      pioneer/packages/joy-media/src/common/MediaDropdownOptions.tsx
  83. 103 96
      pioneer/packages/joy-media/src/common/MediaForms.tsx
  84. 31 31
      pioneer/packages/joy-media/src/common/MediaPlayerView.tsx
  85. 32 33
      pioneer/packages/joy-media/src/common/MediaPlayerWithResolver.tsx
  86. 2 2
      pioneer/packages/joy-media/src/common/NoContentYet.tsx
  87. 17 17
      pioneer/packages/joy-media/src/common/TypeHelpers.ts
  88. 3 3
      pioneer/packages/joy-media/src/entities/ChannelEntity.ts
  89. 7 7
      pioneer/packages/joy-media/src/entities/EntityHelpers.ts
  90. 17 17
      pioneer/packages/joy-media/src/entities/MusicAlbumEntity.ts
  91. 12 12
      pioneer/packages/joy-media/src/entities/MusicTrackEntity.ts
  92. 6 6
      pioneer/packages/joy-media/src/explore/AllChannels.tsx
  93. 6 6
      pioneer/packages/joy-media/src/explore/AllVideos.tsx
  94. 10 10
      pioneer/packages/joy-media/src/explore/ExploreContent.tsx
  95. 3 3
      pioneer/packages/joy-media/src/explore/ExploreContent.view.tsx
  96. 17 17
      pioneer/packages/joy-media/src/explore/PlayContent.tsx
  97. 2 2
      pioneer/packages/joy-media/src/index.tsx
  98. 2 2
      pioneer/packages/joy-media/src/mocks/ContentLicense.mock.ts
  99. 1 1
      pioneer/packages/joy-media/src/mocks/CurationStatus.mock.ts
  100. 1 1
      pioneer/packages/joy-media/src/mocks/FeaturedContent.mock.ts

+ 20 - 20
pioneer/.github/workflows/pr-any.yml → .github/workflows/pioneer-pr.yml

@@ -1,9 +1,9 @@
-name: PR
-on: [pull_request]
+name: Pioneer
+on: [pull_request, push]
 
 jobs:
-  lint:
-    name: Linting
+  pioneer_build_ubuntu:
+    name: Ubuntu Build
     runs-on: ubuntu-latest
     strategy:
       matrix:
@@ -14,14 +14,14 @@ jobs:
       uses: actions/setup-node@v1
       with:
         node-version: ${{ matrix.node-version }}
-    - name: lint
+    - name: build
       run: |
         yarn install --frozen-lockfile
-        yarn lint
+        yarn workspace pioneer build
 
-  test:
-    name: Testing
-    runs-on: ubuntu-latest
+  pioneer_build_osx:
+    name: MacOS Build
+    runs-on: macos-latest
     strategy:
       matrix:
         node-version: [12.x]
@@ -31,13 +31,13 @@ jobs:
       uses: actions/setup-node@v1
       with:
         node-version: ${{ matrix.node-version }}
-    - name: test
+    - name: build
       run: |
         yarn install --frozen-lockfile
-        yarn test
+        yarn workspace pioneer build
 
-  build_code:
-    name: Build Code
+  pioneer_lint_ubuntu:
+    name: Ubuntu Linting
     runs-on: ubuntu-latest
     strategy:
       matrix:
@@ -48,14 +48,14 @@ jobs:
       uses: actions/setup-node@v1
       with:
         node-version: ${{ matrix.node-version }}
-    - name: build
+    - name: lint
       run: |
         yarn install --frozen-lockfile
-        yarn build:code
+        yarn workspace pioneer lint
 
-  build_i18n:
-    name: Build i18n
-    runs-on: ubuntu-latest
+  pioneer_lint_osx:
+    name: MacOS Linting
+    runs-on: macos-latest
     strategy:
       matrix:
         node-version: [12.x]
@@ -65,7 +65,7 @@ jobs:
       uses: actions/setup-node@v1
       with:
         node-version: ${{ matrix.node-version }}
-    - name: build
+    - name: lint
       run: |
         yarn install --frozen-lockfile
-        yarn build:i18n
+        yarn workspace pioneer lint

+ 6 - 0
.gitignore

@@ -26,3 +26,9 @@ yarn*
 
 # Compiled WASM code
 *.wasm
+
+# Temporary files
+.tmp/
+
+# Istanbul report output
+**.nyc_output/

+ 2 - 2
Cargo.lock

@@ -1569,7 +1569,7 @@ dependencies = [
 
 [[package]]
 name = "joystream-node"
-version = "2.2.0"
+version = "2.3.0"
 dependencies = [
  "ctrlc",
  "derive_more 0.14.1",
@@ -1614,7 +1614,7 @@ dependencies = [
 
 [[package]]
 name = "joystream-node-runtime"
-version = "6.14.0"
+version = "6.15.0"
 dependencies = [
  "parity-scale-codec",
  "safe-mix",

+ 11 - 2
README.md

@@ -85,14 +85,23 @@ This will build and run a fresh new local development chain purging existing one
 cargo test
 ```
 
-### API integration tests
+### Network tests
 
 ```bash
-./scripts/run-dev-chain.sh
+./scripts/run-test-chain.sh
 yarn test
 ```
 
 To run the integration tests with a different chain, you can omit step running the local development chain and set the node URL using `NODE_URL` environment variable.
+Proposal grace periods should be set to 0, otherwise proposal network tests will fail.
+
+### Rome-Constantinople migration network test
+
+Ensure Rome node is up and running, and node URL is set using `NODE_URL` environment variable (default value is `localhost:9944`).
+
+```bash
+yarn test-migration
+```
 
 ## Joystream Runtime
 

+ 1 - 1
cli/package.json

@@ -8,7 +8,7 @@
   },
   "bugs": "https://github.com/Joystream/substrate-runtime-joystream/issues",
   "dependencies": {
-    "@joystream/types": "^0.9.1",
+    "@joystream/types": "^0.10.0",
     "@oclif/command": "^1.5.19",
     "@oclif/config": "^1.14.0",
     "@oclif/plugin-help": "^2.2.3",

+ 1 - 1
node/Cargo.toml

@@ -3,7 +3,7 @@ authors = ['Joystream']
 build = 'build.rs'
 edition = '2018'
 name = 'joystream-node'
-version = '2.2.0'
+version = '2.3.0'
 default-run = "joystream-node"
 
 [[bin]]

+ 1 - 1
package.json

@@ -6,7 +6,7 @@
 		"test": "yarn && yarn workspaces run test",
 		"test-migration": "yarn && yarn workspaces run test-migration",
 		"postinstall": "yarn workspace @joystream/types build",
-    "cargo-checks": "devops/git-hooks/pre-commit && devops/git-hooks/pre-push",
+		"cargo-checks": "devops/git-hooks/pre-commit && devops/git-hooks/pre-push",
 		"cargo-build": "scripts/cargo-build.sh"
 	},
 	"workspaces": [

+ 5 - 1
pioneer/.eslintrc.js

@@ -11,6 +11,10 @@ module.exports = {
   },
   rules: {
     ...base.rules,
-    '@typescript-eslint/no-explicit-any': 'off'
+    '@typescript-eslint/no-explicit-any': 'off',
+    '@typescript-eslint/camelcase': 'off',
+    'react/prop-types': 'off',
+    'new-cap': 'off',
+    '@typescript-eslint/interface-name-prefix': 'off'
   }
 };

+ 0 - 32
pioneer/.github/workflows/push-master.yml

@@ -1,32 +0,0 @@
-name: Master
-on:
-  push:
-    branches:
-      - master
-
-jobs:
-  build_code:
-    name: Build Code
-    if: "! contains(github.event.head_commit.message, '[CI Skip]')"
-    runs-on: ubuntu-latest
-    strategy:
-      matrix:
-        node-version: [12.x]
-    steps:
-    - uses: actions/checkout@v1
-      with:
-        token: ${{ secrets.GH_PAT }}
-    - name: Use Node.js ${{ matrix.node-version }}
-      uses: actions/setup-node@v1
-      with:
-        node-version: ${{ matrix.node-version }}
-    - name: build
-      env:
-        CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
-        GH_PAGES_SRC: packages/apps/build
-        GH_PAT: ${{ secrets.GH_PAT }}
-        NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
-      run: |
-        yarn install --frozen-lockfile
-        yarn polkadot-dev-ghact-build
-        yarn polkadot-dev-ghact-docs

+ 1 - 1
pioneer/babel.config.js

@@ -1,4 +1,4 @@
 module.exports = {
   extends: '@polkadot/dev-react/config/babel',
-  sourceType: 'unambiguous',
+  sourceType: 'unambiguous'
 };

+ 3 - 1
pioneer/package.json

@@ -16,6 +16,8 @@
     "clean": "polkadot-dev-clean-build",
     "clean:i18n": "rm -rf packages/apps/public/locales/en && mkdir -p packages/apps/public/locales/en",
     "lint": "eslint --ext .js,.jsx,.ts,.tsx . && tsc --noEmit --pretty",
+    "lint-only-errors": "eslint --quiet --ext .js,.jsx,.ts,.tsx . && tsc --noEmit --pretty",
+    "lint-autofix": "eslint --fix --ext .js,.jsx,.ts,.tsx . && tsc --noEmit --pretty",
     "postinstall": "polkadot-dev-yarn-only",
     "test": "echo \"skipping tests\"",
     "vanitygen": "node packages/app-accounts/scripts/vanitygen.js",
@@ -80,6 +82,6 @@
     "node-sass": "^4.13.0",
     "sass-loader": "^8.0.0",
     "style-loader": "^1.0.0",
-    "@joystream/types": "^0.9.1"
+    "@joystream/types": "^0.10.0"
   }
 }

+ 3 - 4
pioneer/packages/app-accounts/src/MemoForm.tsx

@@ -9,15 +9,14 @@ import { Text } from '@polkadot/types';
 type Props = MyAccountProps & {};
 
 type State = {
-  memo: string,
-  modified: boolean,
+  memo: string;
+  modified: boolean;
 };
 
 class Component extends React.PureComponent<Props, State> {
-
   state: State = {
     memo: '',
-    modified: false,
+    modified: false
   };
 
   render () {

+ 2 - 2
pioneer/packages/app-accounts/src/Overview.tsx

@@ -59,7 +59,7 @@ function Overview ({ accounts, onStatusChange, t }: Props): React.ReactElement<P
             label={t('Restore JSON')}
             onClick={_toggleImport}
           />
-         {isLedger() && (
+          {isLedger() && (
             <>
               <Button.Or />
               <Button
@@ -87,7 +87,7 @@ function Overview ({ accounts, onStatusChange, t }: Props): React.ReactElement<P
           onStatusChange={onStatusChange}
         />
       )}
-     {accounts && Object.keys(accounts).map((address): React.ReactNode => (
+      {accounts && Object.keys(accounts).map((address): React.ReactNode => (
         <Account
           address={address}
           key={address}

+ 2 - 2
pioneer/packages/app-accounts/src/modals/Create.tsx

@@ -45,7 +45,7 @@ function Create ({ className, onClose, onStatusChange, seed: propsSeed, t, type:
   };
   const _onChangeName = (name: string): void => setName({ isNameValid: !!name.trim(), name });
   const _toggleConfirmation = (): void => setIsConfirmationOpen(!isConfirmationOpen);
-  const context = useMyAccount()
+  const context = useMyAccount();
 
   const _onCommit = (): void => {
     if (!isValid) {
@@ -53,7 +53,7 @@ function Create ({ className, onClose, onStatusChange, seed: propsSeed, t, type:
     }
 
     const status = createAccount(`${seed}${derivePath}`, pairType, name, password, t('created account'));
-    context.set(status.account as string)
+    context.set(status.account as string);
 
     _toggleConfirmation();
     onStatusChange(status);

+ 4 - 4
pioneer/packages/app-accounts/src/modals/Derive.tsx

@@ -11,13 +11,13 @@ import React, { useContext, useEffect, useState } from 'react';
 import { AddressRow, Button, Input, InputAddress, Modal, Password, StatusContext } from '@polkadot/react-components';
 import { useDebounce } from '@polkadot/react-components/hooks';
 import keyring from '@polkadot/ui-keyring';
-import { isPasswordValid } from '@polkadot/joy-utils/accounts';
+import { isPasswordValid, downloadAccount } from '@polkadot/joy-utils/accounts';
 import { keyExtractPath } from '@polkadot/util-crypto';
 
 import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext';
 
 import translate from '../translate';
-import { downloadAccount } from '@polkadot/joy-utils/accounts';
+
 import CreateConfirmation from './CreateConfirmation';
 
 interface Props extends I18nProps {
@@ -82,7 +82,7 @@ function Derive ({ className, from, onClose, t }: Props): React.ReactElement {
   const [suri, setSuri] = useState('');
   const debouncedSuri = useDebounce(suri);
   const isValid = !!address && !deriveError && isNameValid && isPassValid;
-  const context = useMyAccount()
+  const context = useMyAccount();
 
   useEffect((): void => {
     setIsLocked(source.isLocked);
@@ -122,7 +122,7 @@ function Derive ({ className, from, onClose, t }: Props): React.ReactElement {
     }
 
     const status = createAccount(source, suri, name, password, t('created account'));
-    context.set(status.account as string)
+    context.set(status.account as string);
 
     _toggleConfirmation();
     queueAction(status);

+ 1 - 1
pioneer/packages/app-accounts/src/modals/Import.tsx

@@ -160,7 +160,7 @@ class Import extends TxComponent<Props, State> {
       status.message = t('account restored');
 
       InputAddress.setLastValue('account', address);
-      this.context.set(address)
+      this.context.set(address);
     } catch (error) {
       this.setState({ isPassValid: true });
 

+ 2 - 2
pioneer/packages/app-accounts/src/modals/Qr.tsx

@@ -27,7 +27,7 @@ interface Props extends I18nProps, ModalProps {
 function QrModal ({ className, onClose, onStatusChange, t }: Props): React.ReactElement<Props> {
   const [{ isNameValid, name }, setName] = useState({ isNameValid: false, name: '' });
   const [scanned, setScanned] = useState<Scanned | null>(null);
-  const context = useMyAccount()
+  const context = useMyAccount();
 
   const _onNameChange = (name: string): void => setName({ isNameValid: !!name.trim(), name });
 
@@ -40,7 +40,7 @@ function QrModal ({ className, onClose, onStatusChange, t }: Props): React.React
 
     keyring.addExternal(address, { genesisHash, name: name.trim() });
     InputAddress.setLastValue('account', address);
-    context.set(address)
+    context.set(address);
 
     onStatusChange({
       account: address,

+ 6 - 7
pioneer/packages/app-address-book/src/MemoByAccount.tsx

@@ -6,21 +6,20 @@ import MemoView from '@polkadot/joy-utils/memo/MemoView';
 import { RouteComponentProps } from 'react-router';
 
 type Props = RouteComponentProps & {
-  history: History,
+  history: History;
   match: {
     params: {
-      accountId?: string
-    }
-  }
+      accountId?: string;
+    };
+  };
 };
 
 type State = {
-  loaded: boolean,
-  accountInput?: string
+  loaded: boolean;
+  accountInput?: string;
 };
 
 export default class Component extends React.PureComponent<Props, State> {
-
   static getDerivedStateFromProps (props: Props, currentState: State): State | null {
     const { match: { params: { accountId } } } = props;
     const { loaded } = currentState;

+ 14 - 4
pioneer/packages/app-explorer/src/SummarySession.tsx

@@ -12,10 +12,15 @@ import { withCalls } from '@polkadot/react-api';
 import translate from './translate';
 import { formatNumber } from '@polkadot/util';
 
+import { SessionIndex } from '@polkadot/types/interfaces';
+import { u64 } from '@polkadot/types/primitive';
+
 interface Props extends I18nProps {
   sessionInfo?: DerivedSessionInfo;
   withEra?: boolean;
   withSession?: boolean;
+  epochIndex?: u64;
+  currentEraStartSessionIndex?: SessionIndex;
 }
 
 function renderSession ({ sessionInfo, t, withSession = true }: Props): React.ReactNode {
@@ -44,12 +49,15 @@ function renderSession ({ sessionInfo, t, withSession = true }: Props): React.Re
     );
 }
 
-function renderEra ({ sessionInfo, t, withEra = true }: Props): React.ReactNode {
-  if (!withEra || !sessionInfo) {
+function renderEra ({ sessionInfo, t, withEra = true, epochIndex, currentEraStartSessionIndex }: Props): React.ReactNode {
+  if (!withEra || !sessionInfo || !epochIndex || !currentEraStartSessionIndex) {
     return null;
   }
 
   const label = t('era');
+  const { sessionLength, sessionProgress } = sessionInfo;
+  // eraProgress is calculated the wrong way in polkadot/api v0.96.1 (fixed in v0.97.1)
+  const eraProgress = epochIndex.sub(currentEraStartSessionIndex).mul(sessionLength).add(sessionProgress);
 
   return sessionInfo.sessionLength.gtn(0)
     ? (
@@ -57,7 +65,7 @@ function renderEra ({ sessionInfo, t, withEra = true }: Props): React.ReactNode
         label={label}
         progress={{
           total: sessionInfo.eraLength,
-          value: sessionInfo.eraProgress
+          value: eraProgress
         }}
       />
     )
@@ -79,6 +87,8 @@ function SummarySession (props: Props): React.ReactElement<Props> {
 
 export default translate(
   withCalls<Props>(
-    ['derive.session.info', { propName: 'sessionInfo' }]
+    ['derive.session.info', { propName: 'sessionInfo' }],
+    ['query.babe.epochIndex', { propName: 'epochIndex' }],
+    ['query.staking.currentEraStartSessionIndex', { propName: 'currentEraStartSessionIndex' }]
   )(SummarySession)
 );

+ 2 - 2
pioneer/packages/app-staking/src/Actions/Accounts.tsx

@@ -24,7 +24,7 @@ interface Props extends I18nProps, ComponentProps, ApiProps {
   myControllers?: string[];
 }
 
-function getMyStashes(myControllers?: string[], allAccounts?: SubjectInfo): string[] | null {
+function getMyStashes (myControllers?: string[], allAccounts?: SubjectInfo): string[] | null {
   const result: string[] = [];
 
   if (!myControllers) {
@@ -40,7 +40,7 @@ function getMyStashes(myControllers?: string[], allAccounts?: SubjectInfo): stri
   return result;
 }
 
-function Accounts({
+function Accounts ({
   allAccounts,
   allStashes,
   className,

+ 3 - 3
pioneer/packages/app-staking/src/Overview/CurrentList.tsx

@@ -23,7 +23,7 @@ interface Props extends I18nProps {
   stakingOverview?: DerivedStakingOverview;
 }
 
-function renderColumn(
+function renderColumn (
   myAccounts: string[],
   addresses: AccountId[] | string[],
   defaultName: string,
@@ -56,11 +56,11 @@ function renderColumn(
   );
 }
 
-function filterAccounts(list: string[] = [], without: AccountId[] | string[]): string[] {
+function filterAccounts (list: string[] = [], without: AccountId[] | string[]): string[] {
   return list.filter((accountId): boolean => !without.includes(accountId as any));
 }
 
-function CurrentList(props: Props): React.ReactElement<Props> {
+function CurrentList (props: Props): React.ReactElement<Props> {
   const { isSubstrateV2 } = useContext(ApiContext);
   const [filter, setFilter] = useState<ValidatorFilter>('all');
   const [myAccounts] = useState(keyring.getAccounts().map(({ address }): string => address));

+ 5 - 5
pioneer/packages/app-staking/src/Overview/index.tsx

@@ -14,7 +14,7 @@ import Summary from './Summary';
 
 interface Props extends BareProps, ComponentProps {}
 
-export default function Overview({
+export default function Overview ({
   allControllers,
   allStashes,
   recentlyOnline,
@@ -29,10 +29,10 @@ export default function Overview({
     validators &&
       setNext(
         isSubstrateV2
-          ? // this is a V2 node currentValidators is a list of stashes
-            allStashes.filter((address): boolean => !validators.includes(address as any))
-          : // this is a V1 node currentValidators is a list of controllers
-            allControllers.filter((address): boolean => !validators.includes(address as any))
+          // this is a V2 node currentValidators is a list of stashes
+          ? allStashes.filter((address): boolean => !validators.includes(address as any))
+          // this is a V1 node currentValidators is a list of controllers
+          : allControllers.filter((address): boolean => !validators.includes(address as any))
       );
   }, [allControllers, allStashes, validators]);
 

+ 1 - 1
pioneer/packages/app-staking/src/index.tsx

@@ -34,7 +34,7 @@ interface Props extends AppProps, ApiProps, I18nProps {
 const EMPY_ACCOUNTS: string[] = [];
 const EMPTY_ALL: [string[], string[]] = [EMPY_ACCOUNTS, EMPY_ACCOUNTS];
 
-function App({
+function App ({
   allAccounts,
   allStashesAndControllers: [allStashes, allControllers] = EMPTY_ALL,
   basePath,

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

@@ -14,7 +14,7 @@ import members from './joy-members';
 import proposals from './joy-proposals';
 import roles from './joy-roles';
 import storageRoles from './joy-storage';
-// import pages from './joy-pages';
+import pages from './joy-pages';
 
 // import template from './123code';
 import accounts from './accounts';
@@ -69,11 +69,12 @@ if (appSettings.isFullMode) {
     js,
     toolbox,
     null
-  )
+  );
 }
 
 routes = routes.concat(
-  settings
+  settings,
+  pages
 );
 
 const setup: Routing = {

+ 6 - 2
pioneer/packages/apps-routing/src/joy-pages.ts

@@ -5,7 +5,9 @@ import { ToS, Privacy } from '@polkadot/joy-pages/index';
 export default ([
   {
     Component: ToS,
-    display: {},
+    display: {
+      isHidden: true
+    },
     i18n: {
       defaultValue: 'Terms of Service'
     },
@@ -14,7 +16,9 @@ export default ([
   },
   {
     Component: Privacy,
-    display: {},
+    display: {
+      isHidden: true
+    },
     i18n: {
       defaultValue: 'Privacy Policy'
     },

+ 4 - 4
pioneer/packages/apps/src/SideBar/Item.tsx

@@ -40,7 +40,7 @@ type Subtitle = {
 const disabledLog: Map<string, string> = new Map();
 const TOOLTIP_OFFSET = { right: -4 };
 
-function logDisabled(route: string, message: string): void {
+function logDisabled (route: string, message: string): void {
   if (!disabledLog.get(route)) {
     disabledLog.set(route, message);
 
@@ -48,7 +48,7 @@ function logDisabled(route: string, message: string): void {
   }
 }
 
-function hasEndpoint(api: ApiPromise, endpoint: string): boolean {
+function hasEndpoint (api: ApiPromise, endpoint: string): boolean {
   const [area, section, method] = endpoint.split('.');
 
   try {
@@ -58,7 +58,7 @@ function hasEndpoint(api: ApiPromise, endpoint: string): boolean {
   }
 }
 
-function checkVisible(
+function checkVisible (
   name: string,
   { api, isApiReady, isApiConnected }: ApiProps,
   hasAccounts: boolean,
@@ -93,7 +93,7 @@ function checkVisible(
   return notFound.length === 0;
 }
 
-function Item({
+function Item ({
   allAccounts,
   route: { Modal, display, i18n, icon, name },
   t,

+ 9 - 9
pioneer/packages/apps/src/SideBar/index.tsx

@@ -18,7 +18,7 @@ import translate from '../translate';
 import Item from './Item';
 import NetworkModal from '../modals/Network';
 
-interface Props extends I18nProps /*ApiProps,*/ {
+interface Props extends I18nProps /* ApiProps, */ {
   className?: string;
   collapse: () => void;
   handleResize: () => void;
@@ -33,10 +33,10 @@ type OuterLinkProps = {
   icon?: SemanticICONS;
 };
 
-export function OuterLink({ url, title, icon = 'external alternate' }: OuterLinkProps) {
+export function OuterLink ({ url, title, icon = 'external alternate' }: OuterLinkProps) {
   return (
     <Menu.Item className="apps--SideBar-Item">
-      <a className="apps--SideBar-Item-NavLink" href={url} target="_blank">
+      <a className="apps--SideBar-Item-NavLink" href={url} target="_blank" rel="noopener noreferrer">
         <Icon name={icon} />
         <span className="text">{title}</span>
       </a>
@@ -44,7 +44,12 @@ export function OuterLink({ url, title, icon = 'external alternate' }: OuterLink
   );
 }
 
-function SideBar({
+function JoystreamLogo (isCollapsed: boolean) {
+  const logo = isCollapsed ? 'images/logo-j.svg' : 'images/logo-joytream.svg';
+  return <img alt="Joystream" className="apps--SideBar-logo" src={logo} />;
+}
+
+function SideBar ({
   className,
   collapse,
   handleResize,
@@ -149,8 +154,3 @@ export default translate(
     }
   `
 );
-
-function JoystreamLogo(isCollapsed: boolean) {
-  const logo = isCollapsed ? 'images/logo-j.svg' : 'images/logo-joytream.svg';
-  return <img alt="Joystream" className="apps--SideBar-logo" src={logo} />;
-}

+ 2 - 2
pioneer/packages/apps/src/TopBar.tsx

@@ -9,7 +9,7 @@ import './TopBar.css';
 
 type Props = I18nProps & {};
 
-function renderAddress(address: string) {
+function renderAddress (address: string) {
   const balance = <span className="label">Balance: </span>;
 
   return (
@@ -37,7 +37,7 @@ function renderAddress(address: string) {
 //   );
 // }
 
-function Component(_props: Props) {
+function Component (_props: Props) {
   const {
     state: { address }
   } = useMyAccount();

+ 12 - 12
pioneer/packages/apps/webpack.config.js

@@ -83,19 +83,19 @@ function createWebpack ({ alias = {}, context, name = 'index' }) {
           ]
         },
         {
-            test: /\.s[ac]ss$/i,
-            use: [
-              // Creates `style` nodes from JS strings
-              'style-loader',
-              // Translates CSS into CommonJS
-              'css-loader',
-              // Compiles Sass to CSS
-              'sass-loader',
-            ],
+          test: /\.s[ac]ss$/i,
+          use: [
+            // Creates `style` nodes from JS strings
+            'style-loader',
+            // Translates CSS into CommonJS
+            'css-loader',
+            // Compiles Sass to CSS
+            'sass-loader'
+          ]
         },
         {
-            test: /\.less$/,
-            loaders: [ 'style-loader', 'css-loader', 'less-loader' ]
+          test: /\.less$/,
+          loaders: ['style-loader', 'css-loader', 'less-loader']
         },
         {
           test: /\.css$/,
@@ -233,6 +233,6 @@ module.exports = createWebpack({
       }, {})
     ),
     // TODO: Me might try to add it to "findPackages", but this will be a little more tricky
-    "@joystream/types": path.resolve(__dirname, '../../../types/src')
+    '@joystream/types': path.resolve(__dirname, '../../../types/src')
   }
 });

+ 4 - 5
pioneer/packages/joy-election/src/Applicant.tsx

@@ -7,20 +7,19 @@ import { ApiProps } from '@polkadot/react-api/types';
 import { withCalls } from '@polkadot/react-api/with';
 import { AccountId } from '@polkadot/types/interfaces';
 import { formatBalance } from '@polkadot/util';
-import CandidatePreview from "./CandidatePreview";
+import CandidatePreview from './CandidatePreview';
 
 import translate from './translate';
 import { calcTotalStake } from '@polkadot/joy-utils/index';
 import { Stake } from '@joystream/types/';
 
 type Props = ApiProps & I18nProps & {
-  index: number,
-  accountId: AccountId,
-  stake?: Stake
+  index: number;
+  accountId: AccountId;
+  stake?: Stake;
 };
 
 class Applicant extends React.PureComponent<Props> {
-
   render () {
     const { index, accountId, stake } = this.props;
     const voteUrl = `/council/votes?applicantId=${accountId.toString()}`;

+ 17 - 18
pioneer/packages/joy-election/src/Applicants.tsx

@@ -16,25 +16,24 @@ import { queryToProp } from '@polkadot/joy-utils/index';
 import { withMyAccount, MyAccountProps } from '@polkadot/joy-utils/MyAccount';
 
 type Props = ApiProps & I18nProps & MyAccountProps & {
-  candidacyLimit?: BN,
-  applicants?: Array<AccountId>
+  candidacyLimit?: BN;
+  applicants?: Array<AccountId>;
 };
 
 class Applicants extends React.PureComponent<Props> {
-
   private renderTable = (applicants: Array<AccountId>) => (
     <Table celled selectable compact>
-    <Table.Header>
-      <Table.Row>
-        <Table.HeaderCell>#</Table.HeaderCell>
-        <Table.HeaderCell>Applicant</Table.HeaderCell>
-        <Table.HeaderCell>Total stake</Table.HeaderCell>
-        <Table.HeaderCell style={{ width: '1%' }}>Actions</Table.HeaderCell>
-      </Table.Row>
-    </Table.Header>
-    <Table.Body>{applicants.map((accountId, index) => (
-      <Applicant key={index} index={index} accountId={accountId} />
-    ))}</Table.Body>
+      <Table.Header>
+        <Table.Row>
+          <Table.HeaderCell>#</Table.HeaderCell>
+          <Table.HeaderCell>Applicant</Table.HeaderCell>
+          <Table.HeaderCell>Total stake</Table.HeaderCell>
+          <Table.HeaderCell style={{ width: '1%' }}>Actions</Table.HeaderCell>
+        </Table.Row>
+      </Table.Header>
+      <Table.Body>{applicants.map((accountId, index) => (
+        <Applicant key={index} index={index} accountId={accountId} />
+      ))}</Table.Body>
     </Table>
   )
 
@@ -47,10 +46,10 @@ class Applicants extends React.PureComponent<Props> {
         <ApplyForm myAddress={myAddress} />
       </Section>
       <Section title={title}>
-      {!applicants.length
-        ? <em>No applicants yet</em>
-        : this.renderTable(applicants)
-      }
+        {!applicants.length
+          ? <em>No applicants yet</em>
+          : this.renderTable(applicants)
+        }
       </Section>
     </>;
   }

+ 5 - 6
pioneer/packages/joy-election/src/ApplyForm.tsx

@@ -15,14 +15,14 @@ import { calcTotalStake, ZERO } from '@polkadot/joy-utils/index';
 import { MyAddressProps, withOnlyMembers } from '@polkadot/joy-utils/MyAccount';
 
 type Props = ApiProps & I18nProps & MyAddressProps & {
-  minStake?: Balance,
-  alreadyStaked?: Stake,
-  myBalance?: Balance
+  minStake?: Balance;
+  alreadyStaked?: Stake;
+  myBalance?: Balance;
 };
 
 type State = {
-  stake: BN,
-  isStakeValid: boolean
+  stake: BN;
+  isStakeValid: boolean;
 };
 
 const DEFAULT_STATE: State = {
@@ -31,7 +31,6 @@ const DEFAULT_STATE: State = {
 };
 
 class ApplyForm extends React.PureComponent<Props, State> {
-
   state = DEFAULT_STATE;
 
   render () {

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

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

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

@@ -1,28 +1,28 @@
-import React from "react";
+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 { Table } from "semantic-ui-react";
-import { formatBalance } from "@polkadot/util";
+import { ApiProps } from '@polkadot/react-api/types';
+import { I18nProps } from '@polkadot/react-components/types';
+import { withCalls } from '@polkadot/react-api/with';
+import { Table } from 'semantic-ui-react';
+import { formatBalance } from '@polkadot/util';
 import CouncilCandidate from './CandidatePreview';
 
-import { calcBackersStake } from "@polkadot/joy-utils/index";
-import { Seat } from "@joystream/types/";
-import translate from "./translate";
-import Section from "@polkadot/joy-utils/Section";
+import { calcBackersStake } from '@polkadot/joy-utils/index';
+import { Seat } from '@joystream/types/';
+import translate from './translate';
+import Section from '@polkadot/joy-utils/Section';
 
 type Props = ApiProps &
-  I18nProps & {
-    council?: Seat[];
-  };
+I18nProps & {
+  council?: Seat[];
+};
 
 type State = {};
 
 class Council extends React.PureComponent<Props, State> {
   state: State = {};
 
-  private renderTable(council: Seat[]) {
+  private renderTable (council: Seat[]) {
     return (
       <Table celled selectable compact>
         <Table.Header>
@@ -30,7 +30,7 @@ class Council extends React.PureComponent<Props, State> {
             <Table.HeaderCell>#</Table.HeaderCell>
             <Table.HeaderCell>Council member</Table.HeaderCell>
             <Table.HeaderCell>Own stake</Table.HeaderCell>
-            <Table.HeaderCell>Backers' stake</Table.HeaderCell>
+            <Table.HeaderCell>{'Backers\' stake'}</Table.HeaderCell>
             <Table.HeaderCell>Backers count</Table.HeaderCell>
           </Table.Row>
         </Table.Header>
@@ -51,7 +51,7 @@ class Council extends React.PureComponent<Props, State> {
     );
   }
 
-  render() {
+  render () {
     const { council = [] } = this.props;
     // console.log({ council });
     return (
@@ -64,5 +64,5 @@ class Council extends React.PureComponent<Props, State> {
 
 // inject the actual API calls automatically into props
 export default translate(
-  withCalls<Props>(["query.council.activeCouncil", { propName: "council" }])(Council)
+  withCalls<Props>(['query.council.activeCouncil', { propName: 'council' }])(Council)
 );

+ 20 - 21
pioneer/packages/joy-election/src/Dashboard.tsx

@@ -15,29 +15,28 @@ import { ElectionStage, Seat } from '@joystream/types/';
 import translate from './translate';
 
 type Props = ApiProps & I18nProps & {
-  bestNumber?: BN,
-
-  activeCouncil?: Seat[],
-  termEndsAt?: BlockNumber,
-
-  autoStart?: Boolean,
-  newTermDuration?: BN,
-  candidacyLimit?: BN,
-  councilSize?: BN,
-  minCouncilStake?: Balance,
-  minVotingStake?: Balance,
-  announcingPeriod?: BlockNumber,
-  votingPeriod?: BlockNumber,
-  revealingPeriod?: BlockNumber,
-
-  round?: BN,
-  stage?: Option<ElectionStage>
+  bestNumber?: BN;
+
+  activeCouncil?: Seat[];
+  termEndsAt?: BlockNumber;
+
+  autoStart?: boolean;
+  newTermDuration?: BN;
+  candidacyLimit?: BN;
+  councilSize?: BN;
+  minCouncilStake?: Balance;
+  minVotingStake?: Balance;
+  announcingPeriod?: BlockNumber;
+  votingPeriod?: BlockNumber;
+  revealingPeriod?: BlockNumber;
+
+  round?: BN;
+  stage?: Option<ElectionStage>;
 };
 
 type State = {};
 
 class Dashboard extends React.PureComponent<Props, State> {
-
   state: State = {};
 
   renderCouncil () {
@@ -58,12 +57,12 @@ class Dashboard extends React.PureComponent<Props, State> {
   renderElection () {
     const { bestNumber, round, stage } = this.props;
 
-    let stageName: string | undefined = undefined;
-    let stageEndsAt: BlockNumber | undefined = undefined;
+    let stageName: string | undefined;
+    let stageEndsAt: BlockNumber | undefined;
     if (stage && stage.isSome) {
       const stageValue = stage.value as ElectionStage;
       stageEndsAt = stageValue.value as BlockNumber; // contained value
-      stageName = stageValue.type; //name of Enum variant
+      stageName = stageValue.type; // name of Enum variant
     }
 
     let leftBlocks: BN | undefined;

+ 4 - 5
pioneer/packages/joy-election/src/Reveals.tsx

@@ -21,17 +21,16 @@ type Props = AppProps & ApiProps & I18nProps & {
 };
 
 type State = {
-  applicantId?: string | null,
-  salt?: string,
-  hashedVote?: string | null
+  applicantId?: string | null;
+  salt?: string;
+  hashedVote?: string | null;
 };
 
 class RevealVoteForm extends React.PureComponent<Props, State> {
-
   constructor (props: Props) {
     super(props);
     let { applicantId, location } = this.props;
-    applicantId = applicantId ? applicantId : getUrlParam(location, 'applicantId');
+    applicantId = applicantId || getUrlParam(location, 'applicantId');
     const hashedVote = getUrlParam(location, 'hashedVote');
 
     this.state = {

+ 29 - 30
pioneer/packages/joy-election/src/SealedVote.tsx

@@ -12,16 +12,15 @@ import translate from './translate';
 import { calcTotalStake } from '@polkadot/joy-utils/index';
 import { SealedVote } from '@joystream/types/';
 import AddressMini from '@polkadot/react-components/AddressMiniJoy';
-import CandidatePreview from "./CandidatePreview";
+import CandidatePreview from './CandidatePreview';
 import { findVoteByHash } from './myVotesStore';
 
 type Props = ApiProps & I18nProps & {
-  hash: Hash,
-  sealedVote?: SealedVote
+  hash: Hash;
+  sealedVote?: SealedVote;
 };
 
 class Comp extends React.PureComponent<Props> {
-
   renderCandidateOrAction () {
     const { hash, sealedVote } = this.props;
     if (!sealedVote) {
@@ -43,32 +42,32 @@ class Comp extends React.PureComponent<Props> {
 
     return !sealedVote ? null : (
       <Table celled selectable compact definition className='SealedVoteTable'>
-      <Table.Body>
-      <Table.Row>
-        <Table.Cell>Hash</Table.Cell>
-        <Table.Cell><code>{hash.toHex()}</code></Table.Cell>
-      </Table.Row>
-      {myVote && <Table.Row>
-        <Table.Cell>Salt</Table.Cell>
-        <Table.Cell><code>{myVote.salt}</code></Table.Cell>
-      </Table.Row>}
-      <Table.Row>
-        <Table.Cell>Stake</Table.Cell>
-        <Table.Cell>{formatBalance(calcTotalStake(sealedVote.stake))}</Table.Cell>
-      </Table.Row>
-      {myVote && <Table.Row>
-        <Table.Cell>Voted on</Table.Cell>
-        <Table.Cell>{new Date(myVote.votedOnTime).toLocaleString()}</Table.Cell>
-      </Table.Row>}
-      <Table.Row>
-        <Table.Cell>Voter</Table.Cell>
-        <Table.Cell><AddressMini value={sealedVote.voter} isShort={false} isPadded={false} withBalance={true} /></Table.Cell>
-      </Table.Row>
-      <Table.Row>
-        <Table.Cell>Candidate</Table.Cell>
-        <Table.Cell>{this.renderCandidateOrAction()}</Table.Cell>
-      </Table.Row>
-      </Table.Body>
+        <Table.Body>
+          <Table.Row>
+            <Table.Cell>Hash</Table.Cell>
+            <Table.Cell><code>{hash.toHex()}</code></Table.Cell>
+          </Table.Row>
+          {myVote && <Table.Row>
+            <Table.Cell>Salt</Table.Cell>
+            <Table.Cell><code>{myVote.salt}</code></Table.Cell>
+          </Table.Row>}
+          <Table.Row>
+            <Table.Cell>Stake</Table.Cell>
+            <Table.Cell>{formatBalance(calcTotalStake(sealedVote.stake))}</Table.Cell>
+          </Table.Row>
+          {myVote && <Table.Row>
+            <Table.Cell>Voted on</Table.Cell>
+            <Table.Cell>{new Date(myVote.votedOnTime).toLocaleString()}</Table.Cell>
+          </Table.Row>}
+          <Table.Row>
+            <Table.Cell>Voter</Table.Cell>
+            <Table.Cell><AddressMini value={sealedVote.voter} isShort={false} isPadded={false} withBalance={true} /></Table.Cell>
+          </Table.Row>
+          <Table.Row>
+            <Table.Cell>Candidate</Table.Cell>
+            <Table.Cell>{this.renderCandidateOrAction()}</Table.Cell>
+          </Table.Row>
+        </Table.Body>
       </Table>
     );
   }

+ 6 - 7
pioneer/packages/joy-election/src/SealedVotes.tsx

@@ -13,12 +13,11 @@ import { SavedVote } from './myVotesStore';
 import Section from '@polkadot/joy-utils/Section';
 
 type Props = ApiProps & I18nProps & MyAddressProps & {
-  myVotes?: SavedVote[],
-  commitments?: Hash[]
+  myVotes?: SavedVote[];
+  commitments?: Hash[];
 };
 
 class Comp extends React.PureComponent<Props> {
-
   private filterVotes = (myVotesOnly: boolean): Hash[] => {
     const { myVotes = [], commitments = [] } = this.props;
     const isMyVote = (hash: string): boolean => {
@@ -40,13 +39,13 @@ class Comp extends React.PureComponent<Props> {
     return <>
       <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)
+          ? <em>No votes by the current account found on the current browser.</em>
+          : this.renderVotes(myVotes)
       }</Section>
       <Section title={`Other votes (${otherVotes.length})`}>{
         !otherVotes.length
-        ? <em>No votes submitted by other accounts yet.</em>
-        : this.renderVotes(otherVotes)
+          ? <em>No votes submitted by other accounts yet.</em>
+          : this.renderVotes(otherVotes)
       }</Section>
     </>;
   }

+ 91 - 92
pioneer/packages/joy-election/src/VoteForm.tsx

@@ -17,9 +17,9 @@ 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 CandidatePreview from "./CandidatePreview";
+import CandidatePreview from './CandidatePreview';
 import { MyAccountProps, withOnlyMembers } from '@polkadot/joy-utils/MyAccount';
-import MembersDropdown from "@polkadot/joy-utils/MembersDropdown";
+import MembersDropdown from '@polkadot/joy-utils/MembersDropdown';
 import { saveVote, NewVote } from './myVotesStore';
 import { TxFailedCallback } from '@polkadot/react-components/Status/types';
 
@@ -30,27 +30,26 @@ function randomSalt () {
 
 // AppsProps is needed to get a location from the route.
 type Props = AppProps & ApiProps & I18nProps & MyAccountProps & {
-  applicantId?: string | null,
-  minVotingStake?: Balance,
-  applicants?: AccountId[],
-  location?: any,
+  applicantId?: string | null;
+  minVotingStake?: Balance;
+  applicants?: AccountId[];
+  location?: any;
 };
 
 type State = {
-  applicantId?: string | null,
-  stake?: BN,
-  salt?: string,
-  isStakeValid?: boolean,
-  isFormSubmitted: boolean
+  applicantId?: string | null;
+  stake?: BN;
+  salt?: string;
+  isStakeValid?: boolean;
+  isFormSubmitted: boolean;
 };
 
 class Component extends React.PureComponent<Props, State> {
-
   constructor (props: Props) {
     super(props);
 
     let { applicantId, location } = this.props;
-    applicantId = applicantId ? applicantId : getUrlParam(location, 'applicantId');
+    applicantId = applicantId || getUrlParam(location, 'applicantId');
 
     this.state = {
       applicantId,
@@ -78,90 +77,90 @@ class Component extends React.PureComponent<Props, State> {
       <>{isFormSubmitted
 
       // Summary of submitted vote:
-      ? <div>
-        <Message info>
+        ? <div>
+          <Message info>
           Your vote has been sent
-        </Message>
-        <Table celled selectable compact definition className='SealedVoteTable'>
-        <Table.Body>
-          <Table.Row>
-            <Table.Cell>Applicant</Table.Cell>
-            <Table.Cell>
-              { applicantId && <CandidatePreview accountId={applicantId}/> }
-            </Table.Cell>
-          </Table.Row>
-          <Table.Row>
-            <Table.Cell>Stake</Table.Cell>
-            <Table.Cell>{formatBalance(stake)}</Table.Cell>
-          </Table.Row>
-          <Table.Row>
-            <Table.Cell>Salt</Table.Cell>
-            <Table.Cell><code>{salt}</code></Table.Cell>
-          </Table.Row>
-          <Table.Row>
-            <Table.Cell>Hashed vote</Table.Cell>
-            <Table.Cell><code>{hashedVote}</code></Table.Cell>
-          </Table.Row>
-        </Table.Body>
-        </Table>
-        <Labelled style={{ marginTop: '.5rem' }}>
-          <Button
-            size='large'
-            label='Submit another vote'
-            onClick={this.resetForm}
-            icon=''
-          />
-        </Labelled>
-      </div>
+          </Message>
+          <Table celled selectable compact definition className='SealedVoteTable'>
+            <Table.Body>
+              <Table.Row>
+                <Table.Cell>Applicant</Table.Cell>
+                <Table.Cell>
+                  { applicantId && <CandidatePreview accountId={applicantId}/> }
+                </Table.Cell>
+              </Table.Row>
+              <Table.Row>
+                <Table.Cell>Stake</Table.Cell>
+                <Table.Cell>{formatBalance(stake)}</Table.Cell>
+              </Table.Row>
+              <Table.Row>
+                <Table.Cell>Salt</Table.Cell>
+                <Table.Cell><code>{salt}</code></Table.Cell>
+              </Table.Row>
+              <Table.Row>
+                <Table.Cell>Hashed vote</Table.Cell>
+                <Table.Cell><code>{hashedVote}</code></Table.Cell>
+              </Table.Row>
+            </Table.Body>
+          </Table>
+          <Labelled style={{ marginTop: '.5rem' }}>
+            <Button
+              size='large'
+              label='Submit another vote'
+              onClick={this.resetForm}
+              icon=''
+            />
+          </Labelled>
+        </div>
 
       // New vote form:
-      : <div>
-        <div className='ui--row'>
-          <MembersDropdown
-            onChange={ (event, data) => this.onChangeApplicant(data.value as string) }
-            accounts={this.props.applicants || []}
-            value={applicantId || ''}
-            placeholder="Select an applicant you support"
-          />
-        </div>
-        <InputStake
-          min={this.minStake()}
-          isValid={isStakeValid}
-          onChange={this.onChangeStake}
-        />
-        <div className='ui--row'>
-          <Input
-            className='large'
-            isDisabled={true}
-            label='Random salt:'
-            value={salt}
-            onChange={this.onChangeSalt}
-          />
-          <div className='medium' style={{ margin: '.5rem' }}>
-            <Button onClick={this.newRandomSalt} icon=''>Generate</Button>
-            <Message compact warning size='tiny' content='You need to remember this salt!' />
+        : <div>
+          <div className='ui--row'>
+            <MembersDropdown
+              onChange={ (event, data) => this.onChangeApplicant(data.value as string) }
+              accounts={this.props.applicants || []}
+              value={applicantId || ''}
+              placeholder="Select an applicant you support"
+            />
           </div>
-        </div>
-        <div className='ui--row'>
-          <Input
-            isDisabled={true}
-            label='Hashed vote:'
-            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)}
+          <InputStake
+            min={this.minStake()}
+            isValid={isStakeValid}
+            onChange={this.onChangeStake}
           />
-        </Labelled>
-      </div>}
+          <div className='ui--row'>
+            <Input
+              className='large'
+              isDisabled={true}
+              label='Random salt:'
+              value={salt}
+              onChange={this.onChangeSalt}
+            />
+            <div className='medium' style={{ margin: '.5rem' }}>
+              <Button onClick={this.newRandomSalt} icon=''>Generate</Button>
+              <Message compact warning size='tiny' content='You need to remember this salt!' />
+            </div>
+          </div>
+          <div className='ui--row'>
+            <Input
+              isDisabled={true}
+              label='Hashed vote:'
+              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>}
       </>
     );
   }

+ 6 - 7
pioneer/packages/joy-election/src/index.tsx

@@ -22,15 +22,14 @@ import { Seat } from '@joystream/types/';
 
 // define out internal types
 type Props = AppProps & ApiProps & I18nProps & {
-  activeCouncil?: Seat[],
-  applicants?: AccountId[],
-  commitments?: Hash[]
+  activeCouncil?: Seat[];
+  applicants?: AccountId[];
+  commitments?: Hash[];
 };
 
 type State = {};
 
 class App extends React.PureComponent<Props, State> {
-
   state: State = {};
 
   private buildTabs (): TabItem[] {
@@ -43,15 +42,15 @@ class App extends React.PureComponent<Props, State> {
       },
       {
         name: 'members',
-        text: t(`Council members`) + ` (${activeCouncil.length})`
+        text: t('Council members') + ` (${activeCouncil.length})`
       },
       {
         name: 'applicants',
-        text: t(`Applicants`) + ` (${applicants.length})`
+        text: t('Applicants') + ` (${applicants.length})`
       },
       {
         name: 'votes',
-        text: t(`Votes`) + ` (${commitments.length})`
+        text: t('Votes') + ` (${commitments.length})`
       },
       {
         name: 'reveals',

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

@@ -4,17 +4,17 @@ import { nonEmptyArr } from '@polkadot/joy-utils/index';
 const MY_VOTES = 'joy.myVotes';
 
 export type NewVote = {
-  voterId: string,
-  applicantId: string,
-  stake: string, // Actually this is a BN serialized to string.
-  salt: string,
-  hash: string
+  voterId: string;
+  applicantId: string;
+  stake: string; // Actually this is a BN serialized to string.
+  salt: string;
+  hash: string;
 };
 
 export type SavedVote = NewVote & {
-  isRevealed: boolean,
-  votedOnTime: number,
-  revealedOnTime?: number
+  isRevealed: boolean;
+  votedOnTime: number;
+  revealedOnTime?: number;
 };
 
 /** Get all votes that are stored in a local sotrage.  */

+ 14 - 14
pioneer/packages/joy-election/src/utils.tsx

@@ -1,19 +1,26 @@
 import { AccountId } from '@polkadot/types/interfaces';
 
-export type HashedVote = {
-  applicantId: string,
-  salt: string,
-  hash: string
-};
-
 // Keyring / identicon / address
 // -----------------------------------
 
 import createItem from '@polkadot/ui-keyring/options/item';
 import { findNameByAddress } from '@polkadot/joy-utils/index';
 
+// Hash
+// -----------------------------------
+
+import { decodeAddress } from '@polkadot/keyring';
+import { stringToU8a } from '@polkadot/util';
+import { blake2AsHex } from '@polkadot/util-crypto';
+
+export type HashedVote = {
+  applicantId: string;
+  salt: string;
+  hash: string;
+};
+
 const createAddressOption = (address: string) => {
-  let name = findNameByAddress(address);
+  const name = findNameByAddress(address);
   return createItem(address, name);
 };
 
@@ -27,13 +34,6 @@ export const accountIdsToOptions = (applicants: Array<AccountId>): any => {
   return [];
 };
 
-// Hash
-// -----------------------------------
-
-import { decodeAddress } from '@polkadot/keyring';
-import { stringToU8a } from '@polkadot/util';
-import { blake2AsHex } from '@polkadot/util-crypto';
-
 /** hash(accountId + salt) */
 export const hashVote = (accountId?: string | null, salt?: string): string | null => {
   if (!accountId || !salt) {

+ 58 - 58
pioneer/packages/joy-forum/src/CategoryList.tsx

@@ -22,8 +22,8 @@ import { IfIAmForumSudo } from './ForumSudo';
 import { MemberPreview } from '@polkadot/joy-members/MemberPreview';
 
 type CategoryActionsProps = {
-  id: CategoryId
-  category: Category
+  id: CategoryId;
+  category: Category;
 };
 
 function CategoryActions (props: CategoryActionsProps) {
@@ -31,10 +31,10 @@ function CategoryActions (props: CategoryActionsProps) {
   const className = 'ui button ActionButton';
 
   type BtnProps = {
-    label: string,
-    icon?: string,
-    archive?: boolean,
-    delete?: boolean
+    label: string;
+    icon?: string;
+    archive?: boolean;
+    delete?: boolean;
   };
 
   const UpdateCategoryButton = (btnProps: BtnProps) => {
@@ -98,20 +98,16 @@ function CategoryActions (props: CategoryActionsProps) {
 }
 
 type InnerViewCategoryProps = {
-  category?: Category,
-  page?: number,
-  preview?: boolean,
-  history?: History
+  category?: Category;
+  page?: number;
+  preview?: boolean;
+  history?: History;
 };
 
 type ViewCategoryProps = InnerViewCategoryProps & {
-  id: CategoryId
+  id: CategoryId;
 };
 
-const ViewCategory = withForumCalls<ViewCategoryProps>(
-  ['categoryById', { propName: 'category', paramName: 'id' }]
-)(InnerViewCategory);
-
 function InnerViewCategory (props: InnerViewCategoryProps) {
   const { history, category, page = 1, preview = false } = props;
 
@@ -162,7 +158,7 @@ function InnerViewCategory (props: InnerViewCategoryProps) {
 
   const renderSubCategoriesAndThreads = () => <>
     {category.archived &&
-      <JoyWarn title={`This category is archived.`}>
+      <JoyWarn title={'This category is archived.'}>
         No new subcategories, threads and posts can be added to it.
       </JoyWarn>
     }
@@ -195,30 +191,26 @@ function InnerViewCategory (props: InnerViewCategoryProps) {
     </h1>
 
     {category.deleted
-      ? <JoyWarn title={`This category is deleted`} />
+      ? <JoyWarn title={'This category is deleted'} />
       : renderSubCategoriesAndThreads()
     }
   </>);
 }
 
+const ViewCategory = withForumCalls<ViewCategoryProps>(
+  ['categoryById', { propName: 'category', paramName: 'id' }]
+)(InnerViewCategory);
+
 type InnerCategoryThreadsProps = {
-  category: Category,
-  page: number,
-  history: History
+  category: Category;
+  page: number;
+  history: History;
 };
 
 type CategoryThreadsProps = ApiProps & InnerCategoryThreadsProps & {
-  nextThreadId?: ThreadId
+  nextThreadId?: ThreadId;
 };
 
-export const CategoryThreads = withMulti(
-  InnerCategoryThreads,
-  withApi,
-  withForumCalls<CategoryThreadsProps>(
-    ['nextThreadId', { propName: 'nextThreadId' }]
-  )
-);
-
 function InnerCategoryThreads (props: CategoryThreadsProps) {
   const { api, category, nextThreadId, page, history } = props;
 
@@ -267,7 +259,7 @@ function InnerCategoryThreads (props: CategoryThreadsProps) {
     };
 
     loadThreads();
-  }, [ bnToStr(category.id), bnToStr(nextThreadId) ]);
+  }, [bnToStr(category.id), bnToStr(nextThreadId)]);
 
   // console.log({ nextThreadId: bnToStr(nextThreadId), loaded, threads });
 
@@ -317,14 +309,22 @@ function InnerCategoryThreads (props: CategoryThreadsProps) {
   </>;
 }
 
+export const CategoryThreads = withMulti(
+  InnerCategoryThreads,
+  withApi,
+  withForumCalls<CategoryThreadsProps>(
+    ['nextThreadId', { propName: 'nextThreadId' }]
+  )
+);
+
 type ViewCategoryByIdProps = UrlHasIdProps & {
-  history: History,
+  history: History;
   match: {
     params: {
-      id: string
-      page?: string
-    }
-  }
+      id: string;
+      page?: string;
+    };
+  };
 };
 
 export function ViewCategoryById (props: ViewCategoryByIdProps) {
@@ -339,18 +339,10 @@ export function ViewCategoryById (props: ViewCategoryByIdProps) {
 }
 
 type CategoryListProps = ApiProps & {
-  nextCategoryId?: CategoryId,
-  parentId?: CategoryId
+  nextCategoryId?: CategoryId;
+  parentId?: CategoryId;
 };
 
-export const CategoryList = withMulti(
-  InnerCategoryList,
-  withApi,
-  withForumCalls<CategoryListProps>(
-    ['nextCategoryId', { propName: 'nextCategoryId' }]
-  )
-);
-
 function InnerCategoryList (props: CategoryListProps) {
   const { api, parentId, nextCategoryId } = props;
   const [loaded, setLoaded] = useState(false);
@@ -380,7 +372,7 @@ function InnerCategoryList (props: CategoryListProps) {
     };
 
     loadCategories();
-  }, [ bnToStr(parentId), bnToStr(nextCategoryId) ]);
+  }, [bnToStr(parentId), bnToStr(nextCategoryId)]);
 
   // console.log({ nextCategoryId: bnToStr(nextCategoryId), loaded, categories });
 
@@ -394,18 +386,26 @@ function InnerCategoryList (props: CategoryListProps) {
 
   return (
     <Table celled selectable compact>
-    <Table.Header>
-      <Table.Row>
-        <Table.HeaderCell>Category</Table.HeaderCell>
-        <Table.HeaderCell>Threads</Table.HeaderCell>
-        <Table.HeaderCell>Subcategories</Table.HeaderCell>
-        <Table.HeaderCell>Actions</Table.HeaderCell>
-        <Table.HeaderCell>Creator</Table.HeaderCell>
-      </Table.Row>
-    </Table.Header>
-    <Table.Body>{categories.map((category, i) => (
-      <InnerViewCategory key={i} preview category={category} />
-    ))}</Table.Body>
+      <Table.Header>
+        <Table.Row>
+          <Table.HeaderCell>Category</Table.HeaderCell>
+          <Table.HeaderCell>Threads</Table.HeaderCell>
+          <Table.HeaderCell>Subcategories</Table.HeaderCell>
+          <Table.HeaderCell>Actions</Table.HeaderCell>
+          <Table.HeaderCell>Creator</Table.HeaderCell>
+        </Table.Row>
+      </Table.Header>
+      <Table.Body>{categories.map((category, i) => (
+        <InnerViewCategory key={i} preview category={category} />
+      ))}</Table.Body>
     </Table>
   );
 }
+
+export const CategoryList = withMulti(
+  InnerCategoryList,
+  withApi,
+  withForumCalls<CategoryListProps>(
+    ['nextCategoryId', { propName: 'nextCategoryId' }]
+  )
+);

+ 42 - 45
pioneer/packages/joy-forum/src/Context.tsx

@@ -3,28 +3,27 @@
 
 import React, { useReducer, createContext, useContext } from 'react';
 import { Category, Thread, Reply, ModerationAction, BlockchainTimestamp } from '@joystream/types/forum';
-import { Option, Text } from '@polkadot/types';
-import { GenericAccountId } from '@polkadot/types';
+import { Option, Text, GenericAccountId } from '@polkadot/types';
 
 type CategoryId = number;
 type ThreadId = number;
 type ReplyId = number;
 
 export type ForumState = {
-  sudo?: string,
+  sudo?: string;
 
-  nextCategoryId: CategoryId,
-  categoryById: Map<CategoryId, Category>,
-  rootCategoryIds: CategoryId[],
-  categoryIdsByParentId: Map<CategoryId, CategoryId[]>,
+  nextCategoryId: CategoryId;
+  categoryById: Map<CategoryId, Category>;
+  rootCategoryIds: CategoryId[];
+  categoryIdsByParentId: Map<CategoryId, CategoryId[]>;
 
-  nextThreadId: ThreadId,
-  threadById: Map<ThreadId, Thread>,
-  threadIdsByCategoryId: Map<CategoryId, ThreadId[]>,
+  nextThreadId: ThreadId;
+  threadById: Map<ThreadId, Thread>;
+  threadIdsByCategoryId: Map<CategoryId, ThreadId[]>;
 
-  nextReplyId: ReplyId,
-  replyById: Map<ReplyId, Reply>,
-  replyIdsByThreadId: Map<ThreadId, ReplyId[]>
+  nextReplyId: ReplyId;
+  replyById: Map<ReplyId, Reply>;
+  replyIdsByThreadId: Map<ThreadId, ReplyId[]>;
 };
 
 const initialState: ForumState = {
@@ -45,58 +44,58 @@ const initialState: ForumState = {
 };
 
 type SetForumSudo = {
-  type: 'SetForumSudo',
-  sudo?: string
+  type: 'SetForumSudo';
+  sudo?: string;
 };
 
 type NewCategoryAction = {
-  type: 'NewCategory',
-  category: Category,
-  onCreated?: (newId: number) => void
+  type: 'NewCategory';
+  category: Category;
+  onCreated?: (newId: number) => void;
 };
 
 type UpdateCategoryAction = {
-  type: 'UpdateCategory',
-  category: Category,
-  id: CategoryId
+  type: 'UpdateCategory';
+  category: Category;
+  id: CategoryId;
 };
 
 type NewThreadAction = {
-  type: 'NewThread',
-  thread: Thread,
-  onCreated?: (newId: number) => void
+  type: 'NewThread';
+  thread: Thread;
+  onCreated?: (newId: number) => void;
 };
 
 type UpdateThreadAction = {
-  type: 'UpdateThread',
-  thread: Thread,
-  id: ThreadId
+  type: 'UpdateThread';
+  thread: Thread;
+  id: ThreadId;
 };
 
 type ModerateThreadAction = {
-  type: 'ModerateThread',
-  id: ThreadId,
-  moderator: string,
-  rationale: string
+  type: 'ModerateThread';
+  id: ThreadId;
+  moderator: string;
+  rationale: string;
 };
 
 type NewReplyAction = {
-  type: 'NewReply',
-  reply: Reply,
-  onCreated?: (newId: number) => void
+  type: 'NewReply';
+  reply: Reply;
+  onCreated?: (newId: number) => void;
 };
 
 type UpdateReplyAction = {
-  type: 'UpdateReply',
-  reply: Reply,
-  id: ReplyId
+  type: 'UpdateReply';
+  reply: Reply;
+  id: ReplyId;
 };
 
 type ModerateReplyAction = {
-  type: 'ModerateReply',
-  id: ReplyId,
-  moderator: string,
-  rationale: string
+  type: 'ModerateReply';
+  id: ReplyId;
+  moderator: string;
+  rationale: string;
 };
 
 type ForumAction =
@@ -111,9 +110,7 @@ type ForumAction =
   ModerateReplyAction;
 
 function reducer (state: ForumState, action: ForumAction): ForumState {
-
   switch (action.type) {
-
     case 'SetForumSudo': {
       const { sudo } = action;
       return {
@@ -314,8 +311,8 @@ function functionStub () {
 }
 
 export type ForumContextProps = {
-  state: ForumState,
-  dispatch: React.Dispatch<ForumAction>
+  state: ForumState;
+  dispatch: React.Dispatch<ForumAction>;
 };
 
 const contextStub: ForumContextProps = {

+ 10 - 10
pioneer/packages/joy-forum/src/EditCategory.tsx

@@ -50,15 +50,15 @@ const buildSchema = (props: ValidationProps) => {
 };
 
 type OuterProps = ValidationProps & {
-  history?: History,
-  id?: CategoryId,
-  parentId?: CategoryId,
-  struct?: Category
+  history?: History;
+  id?: CategoryId;
+  parentId?: CategoryId;
+  struct?: Category;
 };
 
 type FormValues = {
-  title: string,
-  description: string
+  title: string;
+  description: string;
 };
 
 type FormProps = OuterProps & FormikProps<FormValues>;
@@ -94,7 +94,7 @@ const InnerForm = (props: FormProps) => {
     setSubmitting(false);
     if (txResult == null) {
       // Tx cancelled.
-      return;
+
     }
   };
 
@@ -134,11 +134,11 @@ const InnerForm = (props: FormProps) => {
       ];
     } else {
       // NOTE: currently update_category doesn't support title and description updates.
-      return [ /* TODO add all required params */ ];
+      return [];
     }
   };
 
-  const categoryWord = isSubcategory ? `subcategory` : `category`;
+  const categoryWord = isSubcategory ? 'subcategory' : 'category';
 
   const form =
     <Form className='ui form JoyForm EditEntityForm'>
@@ -155,7 +155,7 @@ const InnerForm = (props: FormProps) => {
           size='large'
           label={isNew
             ? `Create a ${categoryWord}`
-            : `Update a category`
+            : 'Update a category'
           }
           isDisabled={!dirty || isSubmitting}
           params={buildTxParams()}

+ 16 - 16
pioneer/packages/joy-forum/src/EditReply.tsx

@@ -40,14 +40,14 @@ const buildSchema = (props: ValidationProps) => {
 };
 
 type OuterProps = ValidationProps & {
-  history?: History,
-  id?: PostId
-  struct?: Post
-  threadId: ThreadId
+  history?: History;
+  id?: PostId;
+  struct?: Post;
+  threadId: ThreadId;
 };
 
 type FormValues = {
-  text: string
+  text: string;
 };
 
 type FormProps = OuterProps & FormikProps<FormValues>;
@@ -72,6 +72,12 @@ const InnerForm = (props: FormProps) => {
     text
   } = values;
 
+  const goToThreadView = () => {
+    if (history) {
+      history.push('/forum/threads/' + threadId.toString());
+    }
+  };
+
   const onSubmit = (sendTx: () => void) => {
     if (isValid) sendTx();
   };
@@ -80,7 +86,7 @@ const InnerForm = (props: FormProps) => {
     setSubmitting(false);
     if (txResult == null) {
       // Tx cancelled.
-      return;
+
     }
   };
 
@@ -96,15 +102,9 @@ const InnerForm = (props: FormProps) => {
 
     const textParam = new Text(text);
     if (!id) {
-      return [ threadId, textParam ];
+      return [threadId, textParam];
     } else {
-      return [ id, textParam ];
-    }
-  };
-
-  const goToThreadView = () => {
-    if (history) {
-      history.push('/forum/threads/' + threadId.toString());
+      return [id, textParam];
     }
   };
 
@@ -161,7 +161,7 @@ const EditForm = withFormik<OuterProps, FormValues>({
   mapPropsToValues: props => {
     const { struct } = props;
     return {
-      text: struct && struct.current_text || ''
+      text: (struct && struct.current_text) || ''
     };
   },
 
@@ -204,7 +204,7 @@ function withThreadIdFromUrl (Component: React.ComponentType<OuterProps>) {
 }
 
 type HasPostIdProps = {
-  id: PostId
+  id: PostId;
 };
 
 function withIdFromUrl (Component: React.ComponentType<HasPostIdProps>) {

+ 10 - 10
pioneer/packages/joy-forum/src/EditThread.tsx

@@ -51,16 +51,16 @@ const buildSchema = (props: ValidationProps) => {
 };
 
 type OuterProps = ValidationProps & {
-  history?: History,
-  id?: ThreadId
-  struct?: Thread
-  categoryId?: CategoryId
+  history?: History;
+  id?: ThreadId;
+  struct?: Thread;
+  categoryId?: CategoryId;
 };
 
 type FormValues = {
   // pinned: boolean,
-  title: string,
-  text: string
+  title: string;
+  text: string;
 };
 
 type FormProps = OuterProps & FormikProps<FormValues>;
@@ -97,7 +97,7 @@ const InnerForm = (props: FormProps) => {
     setSubmitting(false);
     if (txResult == null) {
       // Tx cancelled.
-      return;
+
     }
   };
 
@@ -132,7 +132,7 @@ const InnerForm = (props: FormProps) => {
   }
 
   const isNew = struct === undefined;
-  const resolvedCategoryId = categoryId ? categoryId : (struct as Thread).category_id;
+  const resolvedCategoryId = categoryId || (struct as Thread).category_id;
 
   const buildTxParams = () => {
     if (!isValid) return [];
@@ -145,7 +145,7 @@ const InnerForm = (props: FormProps) => {
       ];
     } else {
       // NOTE: currently forum SRML doesn't support thread update.
-      return [ /* TODO add all required params */ ];
+      return [];
     }
   };
 
@@ -172,7 +172,7 @@ const InnerForm = (props: FormProps) => {
         <Field component={Checkbox} id='pinned' name='pinned' disabled={isSubmitting} label={`This thread is ${!pinned ? 'not' : '' } pinned`} />
       </LabelledField> */}
 
-      <LabelledText name='title' placeholder={`Title`} {...props} />
+      <LabelledText name='title' placeholder={'Title'} {...props} />
 
       <LabelledField name='text' {...props}>
         <Field component='textarea' id='text' name='text' disabled={isSubmitting} rows={5} placeholder='Type here. You can use Markdown.' />

+ 23 - 23
pioneer/packages/joy-forum/src/ForumSudo.tsx

@@ -22,11 +22,11 @@ import { TxFailedCallback, TxCallback } from '@polkadot/react-components/Status/
 const buildSchema = () => Yup.object().shape({});
 
 type OuterProps = {
-  currentSudo?: string
+  currentSudo?: string;
 };
 
 type FormValues = {
-  sudo?: string
+  sudo?: string;
 };
 
 type FormProps = OuterProps & FormikProps<FormValues>;
@@ -47,7 +47,7 @@ const InnerForm = (props: FormProps) => {
     sudo
   } = values;
 
-  const [ showSelector, setShowSelector ] = useState(false);
+  const [showSelector, setShowSelector] = useState(false);
 
   const resetForm = () => {
     setShowSelector(false);
@@ -62,7 +62,7 @@ const InnerForm = (props: FormProps) => {
     setSubmitting(false);
     if (txResult == null) {
       // Tx cancelled.
-      return;
+
     }
   };
 
@@ -75,10 +75,10 @@ const InnerForm = (props: FormProps) => {
 
   const buildTxParams = () => {
     if (!isValid) return [];
-    return [ new Option('AccountId', sudo) ];
+    return [new Option('AccountId', sudo)];
   };
 
-  type SudoInputAddressProps = FieldProps<FormValues>; /* & InputAddressProps*/;
+  type SudoInputAddressProps = FieldProps<FormValues>; /* & InputAddressProps */
 
   const SudoInputAddress = ({ field, form, ...props }: SudoInputAddressProps) => {
     const { name, value } = field;
@@ -115,7 +115,7 @@ const InnerForm = (props: FormProps) => {
           }
           isDisabled={!dirty || isSubmitting}
           params={buildTxParams()}
-          tx={`forum.setForumSudo`}
+          tx={'forum.setForumSudo'}
           onClick={onSubmit}
           txFailedCb={onTxFailed}
           txSuccessCb={onTxSuccess}
@@ -166,7 +166,7 @@ const EditForm = withFormik<OuterProps, FormValues>({
 })(InnerForm);
 
 type LoadStructProps = {
-  structOpt: Option<AccountId>
+  structOpt: Option<AccountId>;
 };
 
 const withLoadForumSudo = withForumCalls<LoadStructProps>(
@@ -189,14 +189,6 @@ export const EditForumSudo = withMulti(
   withLoadForumSudo
 );
 
-export function withOnlyForumSudo<P extends {}> (Component: React.ComponentType<P>) {
-  return withMulti(
-    Component,
-    withLoadForumSudo,
-    innerWithOnlyForumSudo
-  );
-}
-
 function innerWithOnlyForumSudo<P extends LoadStructProps> (Component: React.ComponentType<P>) {
   return function (props: P) {
     const { structOpt } = props;
@@ -212,7 +204,7 @@ function innerWithOnlyForumSudo<P extends LoadStructProps> (Component: React.Com
       return <Component {...props} />;
     } else {
       return (
-        <JoyError title={`Only forum sudo can access this functionality.`}>
+        <JoyError title={'Only forum sudo can access this functionality.'}>
           <div>Current forum sudo:</div>
           <div>{sudo ? <AddressMini value={sudo} /> : 'UNDEFINED'}</div>
         </JoyError>
@@ -221,17 +213,20 @@ function innerWithOnlyForumSudo<P extends LoadStructProps> (Component: React.Com
   };
 }
 
+export function withOnlyForumSudo<P extends {}> (Component: React.ComponentType<P>) {
+  return withMulti(
+    Component,
+    withLoadForumSudo,
+    innerWithOnlyForumSudo
+  );
+}
+
 type ForumSudoContextProps = {
-  forumSudo?: AccountId
+  forumSudo?: AccountId;
 };
 
 export const ForumSudoContext = createContext<ForumSudoContextProps>({});
 
-export const ForumSudoProvider = withMulti(
-  InnerForumSudoProvider,
-  withLoadForumSudo
-);
-
 export function InnerForumSudoProvider (props: React.PropsWithChildren<LoadStructProps>) {
   const { structOpt } = props;
   const forumSudo = structOpt ? structOpt.unwrapOr(undefined) : undefined;
@@ -242,6 +237,11 @@ export function InnerForumSudoProvider (props: React.PropsWithChildren<LoadStruc
   );
 }
 
+export const ForumSudoProvider = withMulti(
+  InnerForumSudoProvider,
+  withLoadForumSudo
+);
+
 export function useForumSudo () {
   return useContext(ForumSudoContext);
 }

+ 6 - 6
pioneer/packages/joy-forum/src/Moderate.tsx

@@ -36,12 +36,12 @@ const buildSchema = (props: ValidationProps) => {
 };
 
 type OuterProps = ValidationProps & {
-  id: ThreadId | ReplyId,
-  onCloseForm?: () => void
+  id: ThreadId | ReplyId;
+  onCloseForm?: () => void;
 };
 
 type FormValues = {
-  rationale: string
+  rationale: string;
 };
 
 type FormProps = OuterProps & FormikProps<FormValues>;
@@ -75,7 +75,7 @@ const InnerForm = (props: FormProps) => {
     setSubmitting(false);
     if (txResult == null) {
       // Tx cancelled.
-      return;
+
     }
   };
 
@@ -91,9 +91,9 @@ const InnerForm = (props: FormProps) => {
 
     const rationaleParam = new Text(rationale);
     if (isThread) {
-      return [ id, rationaleParam ];
+      return [id, rationaleParam];
     } else {
-      return [ id, rationaleParam ];
+      return [id, rationaleParam];
     }
   };
 

+ 4 - 4
pioneer/packages/joy-forum/src/ViewReply.tsx

@@ -12,9 +12,9 @@ import { MemberPreview } from '@polkadot/joy-members/MemberPreview';
 import { FlexCenter } from '@polkadot/joy-utils/FlexCenter';
 
 type ViewReplyProps = {
-  reply: Post,
-  thread: Thread,
-  category: Category
+  reply: Post;
+  thread: Thread;
+  category: Category;
 };
 
 export function ViewReply (props: ViewReplyProps) {
@@ -35,7 +35,7 @@ export function ViewReply (props: ViewReplyProps) {
     if (!reply.moderation) return null;
 
     return <>
-      <JoyWarn title={`This reply is moderated. Rationale:`}>
+      <JoyWarn title={'This reply is moderated. Rationale:'}>
         <ReactMarkdown className='JoyMemo--full' source={reply.moderation.rationale} linkTarget='_blank' />
       </JoyWarn>
     </>;

+ 31 - 31
pioneer/packages/joy-forum/src/ViewThread.tsx

@@ -20,8 +20,8 @@ import { IfIAmForumSudo } from './ForumSudo';
 import { MemberPreview } from '@polkadot/joy-members/MemberPreview';
 
 type ThreadTitleProps = {
-  thread: Thread,
-  className?: string
+  thread: Thread;
+  className?: string;
 };
 
 function ThreadTitle (props: ThreadTitleProps) {
@@ -37,25 +37,17 @@ function ThreadTitle (props: ThreadTitleProps) {
 }
 
 type InnerViewThreadProps = {
-  category: Category,
-  thread: Thread,
-  page?: number,
-  preview?: boolean,
-  history?: History
+  category: Category;
+  thread: Thread;
+  page?: number;
+  preview?: boolean;
+  history?: History;
 };
 
 type ViewThreadProps = ApiProps & InnerViewThreadProps & {
-  nextPostId?: ThreadId
+  nextPostId?: ThreadId;
 };
 
-export const ViewThread = withMulti(
-  InnerViewThread,
-  withApi,
-  withForumCalls<ViewThreadProps>(
-    ['nextPostId', { propName: 'nextPostId' }]
-  )
-);
-
 function InnerViewThread (props: ViewThreadProps) {
   const [showModerateForm, setShowModerateForm] = useState(false);
   const { history, category, thread, page = 1, preview = false } = props;
@@ -76,7 +68,7 @@ function InnerViewThread (props: ViewThreadProps) {
   const totalPostsInThread = thread.num_posts_ever_created.toNumber();
 
   if (!category) {
-    return <em>Thread's category was not found.</em>;
+    return <em>{'Thread\'s category was not found.'}</em>;
   } else if (category.deleted) {
     return renderThreadNotFound();
   }
@@ -129,8 +121,8 @@ function InnerViewThread (props: ViewThreadProps) {
       );
       const sortedPosts = orderBy(
         postsInThisThread,
-        [ x => x.nr_in_thread.toNumber() ],
-        [ 'asc' ]
+        [x => x.nr_in_thread.toNumber()],
+        ['asc']
       );
 
       setPosts(sortedPosts);
@@ -138,7 +130,7 @@ function InnerViewThread (props: ViewThreadProps) {
     };
 
     loadPosts();
-  }, [ bnToStr(thread.id), bnToStr(nextPostId) ]);
+  }, [bnToStr(thread.id), bnToStr(nextPostId)]);
 
   // console.log({ nextPostId: bnToStr(nextPostId), loaded, posts });
 
@@ -211,7 +203,7 @@ function InnerViewThread (props: ViewThreadProps) {
     if (!thread.moderation) return null;
 
     return <>
-      <JoyWarn title={`This thread is moderated. Rationale:`}>
+      <JoyWarn title={'This thread is moderated. Rationale:'}>
         <ReactMarkdown className='JoyMemo--full' source={thread.moderation.rationale} linkTarget='_blank' />
       </JoyWarn>
     </>;
@@ -224,7 +216,7 @@ function InnerViewThread (props: ViewThreadProps) {
       {renderActions()}
     </h1>
     {category.archived &&
-      <JoyWarn title={`This thread is in archived category.`}>
+      <JoyWarn title={'This thread is in archived category.'}>
         No new replies can be posted.
       </JoyWarn>
     }
@@ -238,18 +230,24 @@ function InnerViewThread (props: ViewThreadProps) {
   </div>;
 }
 
+export const ViewThread = withMulti(
+  InnerViewThread,
+  withApi,
+  withForumCalls<ViewThreadProps>(
+    ['nextPostId', { propName: 'nextPostId' }]
+  )
+);
+
 type ViewThreadByIdProps = ApiProps & {
-  history: History,
+  history: History;
   match: {
     params: {
-      id: string
-      page?: string
-    }
-  }
+      id: string;
+      page?: string;
+    };
+  };
 };
 
-export const ViewThreadById = withApi(InnerViewThreadById);
-
 function InnerViewThreadById (props: ViewThreadByIdProps) {
   const { api, history, match: { params: { id, page: pageStr } } } = props;
 
@@ -288,7 +286,7 @@ function InnerViewThreadById (props: ViewThreadByIdProps) {
     };
 
     loadThreadAndCategory();
-  }, [ id, page ]);
+  }, [id, page]);
 
   // console.log({ threadId: id, page });
 
@@ -301,8 +299,10 @@ function InnerViewThreadById (props: ViewThreadByIdProps) {
   }
 
   if (category.isEmpty) {
-    return <em>Thread's category was not found</em>;
+    return <em>{ 'Thread\'s category was not found' }</em>;
   }
 
   return <ViewThread id={threadId} category={category} thread={thread} page={page} history={history} />;
 }
+
+export const ViewThreadById = withApi(InnerViewThreadById);

+ 1 - 3
pioneer/packages/joy-forum/src/calls.tsx

@@ -19,7 +19,6 @@ const storage: StorageType = 'substrate';
 type EntityMapName = 'categoryById' | 'threadById' | 'replyById';
 
 const getReactValue = (state: ForumState, endpoint: string, paramValue: any): any => {
-
   const getEntityById = (mapName: EntityMapName, constructor: Constructor): any => {
     const id = (paramValue as u64).toNumber();
     const entity = state[mapName].get(id);
@@ -37,10 +36,9 @@ const getReactValue = (state: ForumState, endpoint: string, paramValue: any): an
 
 function withReactCall<P extends ApiProps> (endpoint: string, { paramName, propName }: Options = {}): (Inner: React.ComponentType<ApiProps>) => React.ComponentType<any> {
   return (Inner: React.ComponentType<ApiProps>): React.ComponentType<SubtractProps<P, ApiProps>> => {
-
     const SetProp = (props: P) => {
       const { state } = useForum();
-      const paramValue = paramName ? (props as any)[paramName] as any : undefined;
+      const paramValue = paramName ? (props as any)[paramName] : undefined;
       const propValue = getReactValue(state, endpoint, paramValue);
       const _propName = propName || endpoint;
       const _props = {

+ 24 - 25
pioneer/packages/joy-forum/src/index.tsx

@@ -19,7 +19,6 @@ import { ViewThreadById } from './ViewThread';
 type Props = AppProps & I18nProps & {};
 
 class App extends React.PureComponent<Props> {
-
   private buildTabs (): TabItem[] {
     const { t } = this.props;
     return [
@@ -45,34 +44,34 @@ class App extends React.PureComponent<Props> {
     const tabs = this.buildTabs();
     return (
       <ForumProvider>
-      <ForumSudoProvider>
-      <main className='forum--App'>
-        <header>
-          <Tabs basePath={basePath} items={tabs} />
-        </header>
-        <Switch>
-          <Route path={`${basePath}/sudo`} component={EditForumSudo} />
+        <ForumSudoProvider>
+          <main className='forum--App'>
+            <header>
+              <Tabs basePath={basePath} items={tabs} />
+            </header>
+            <Switch>
+              <Route path={`${basePath}/sudo`} component={EditForumSudo} />
 
-          <Route path={`${basePath}/categories/new`} component={NewCategory} />
-          <Route path={`${basePath}/categories/:id/newSubcategory`} component={NewSubcategory} />
-          <Route path={`${basePath}/categories/:id/newThread`} component={NewThread} />
-          <Route path={`${basePath}/categories/:id/edit`} component={EditCategory} />
-          <Route path={`${basePath}/categories/:id/page/:page`} component={ViewCategoryById} />
-          <Route path={`${basePath}/categories/:id`} component={ViewCategoryById} />
-          <Route path={`${basePath}/categories`} component={CategoryList} />
+              <Route path={`${basePath}/categories/new`} component={NewCategory} />
+              <Route path={`${basePath}/categories/:id/newSubcategory`} component={NewSubcategory} />
+              <Route path={`${basePath}/categories/:id/newThread`} component={NewThread} />
+              <Route path={`${basePath}/categories/:id/edit`} component={EditCategory} />
+              <Route path={`${basePath}/categories/:id/page/:page`} component={ViewCategoryById} />
+              <Route path={`${basePath}/categories/:id`} component={ViewCategoryById} />
+              <Route path={`${basePath}/categories`} component={CategoryList} />
 
-          <Route path={`${basePath}/threads/:id/reply`} component={NewReply} />
-          <Route path={`${basePath}/threads/:id/edit`} component={EditThread} />
-          <Route path={`${basePath}/threads/:id/page/:page`} component={ViewThreadById} />
-          <Route path={`${basePath}/threads/:id`} component={ViewThreadById} />
+              <Route path={`${basePath}/threads/:id/reply`} component={NewReply} />
+              <Route path={`${basePath}/threads/:id/edit`} component={EditThread} />
+              <Route path={`${basePath}/threads/:id/page/:page`} component={ViewThreadById} />
+              <Route path={`${basePath}/threads/:id`} component={ViewThreadById} />
 
-          <Route path={`${basePath}/replies/:id/edit`} component={EditReply} />
-          {/* <Route path={`${basePath}/replies/:id`} component={ViewReplyById} /> */}
+              <Route path={`${basePath}/replies/:id/edit`} component={EditReply} />
+              {/* <Route path={`${basePath}/replies/:id`} component={ViewReplyById} /> */}
 
-          <Route component={CategoryList} />
-        </Switch>
-      </main>
-      </ForumSudoProvider>
+              <Route component={CategoryList} />
+            </Switch>
+          </main>
+        </ForumSudoProvider>
       </ForumProvider>
     );
   }

+ 21 - 21
pioneer/packages/joy-forum/src/utils.tsx

@@ -10,10 +10,10 @@ export const ThreadsPerPage = 10;
 export const RepliesPerPage = 10;
 
 type PaginationProps = {
-  currentPage?: number,
-  totalItems: number,
-  itemsPerPage?: number,
-  onPageChange: (activePage?: string | number) => void
+  currentPage?: number;
+  totalItems: number;
+  itemsPerPage?: number;
+  onPageChange: (activePage?: string | number) => void;
 };
 
 export const Pagination = (p: PaginationProps) => {
@@ -32,19 +32,12 @@ export const Pagination = (p: PaginationProps) => {
 };
 
 type CategoryCrumbsProps = {
-  categoryId?: CategoryId
-  category?: Category
-  threadId?: ThreadId
-  thread?: Thread
+  categoryId?: CategoryId;
+  category?: Category;
+  threadId?: ThreadId;
+  thread?: Thread;
 };
 
-const CategoryCrumb = withMulti(
-  InnerCategoryCrumb,
-  withForumCalls<CategoryCrumbsProps>(
-    ['categoryById', { propName: 'category', paramName: 'categoryId' }]
-  )
-);
-
 function InnerCategoryCrumb (p: CategoryCrumbsProps) {
   const { category } = p;
 
@@ -64,10 +57,10 @@ function InnerCategoryCrumb (p: CategoryCrumbsProps) {
   return null;
 }
 
-const ThreadCrumb = withMulti(
-  InnerThreadCrumb,
+const CategoryCrumb = withMulti(
+  InnerCategoryCrumb,
   withForumCalls<CategoryCrumbsProps>(
-    ['threadById', { propName: 'thread', paramName: 'threadId' }]
+    ['categoryById', { propName: 'category', paramName: 'categoryId' }]
   )
 );
 
@@ -90,6 +83,13 @@ function InnerThreadCrumb (p: CategoryCrumbsProps) {
   return null;
 }
 
+const ThreadCrumb = withMulti(
+  InnerThreadCrumb,
+  withForumCalls<CategoryCrumbsProps>(
+    ['threadById', { propName: 'thread', paramName: 'threadId' }]
+  )
+);
+
 export const CategoryCrumbs = (p: CategoryCrumbsProps) => {
   return (
     <div className='ui breadcrumb'>
@@ -108,7 +108,7 @@ export const CategoryCrumbs = (p: CategoryCrumbsProps) => {
 export type UrlHasIdProps = {
   match: {
     params: {
-      id: string
-    }
-  }
+      id: string;
+    };
+  };
 };

+ 11 - 8
pioneer/packages/joy-forum/src/validation.tsx

@@ -2,14 +2,15 @@ import React from 'react';
 import { withMulti } from '@polkadot/react-api/with';
 import { InputValidationLengthConstraint } from '@joystream/types/forum';
 import { withForumCalls } from './calls';
+import { componentName } from '@polkadot/joy-utils/react/helpers';
 
 export type ValidationProps = {
-  categoryTitleConstraint?: InputValidationLengthConstraint,
-  categoryDescriptionConstraint?: InputValidationLengthConstraint,
-  threadTitleConstraint?: InputValidationLengthConstraint,
-  postTextConstraint?: InputValidationLengthConstraint,
-  threadModerationRationaleConstraint?: InputValidationLengthConstraint,
-  postModerationRationaleConstraint?: InputValidationLengthConstraint
+  categoryTitleConstraint?: InputValidationLengthConstraint;
+  categoryDescriptionConstraint?: InputValidationLengthConstraint;
+  threadTitleConstraint?: InputValidationLengthConstraint;
+  postTextConstraint?: InputValidationLengthConstraint;
+  threadModerationRationaleConstraint?: InputValidationLengthConstraint;
+  postModerationRationaleConstraint?: InputValidationLengthConstraint;
 };
 
 const loadAllValidationConstraints = withForumCalls<ValidationProps>(
@@ -25,8 +26,8 @@ function waitForRequiredConstraints (
   requiredConstraintNames: Array<keyof ValidationProps>
 ) {
   return function (Component: React.ComponentType<any>) {
-    return function (props: ValidationProps) {
-      let nonEmptyProps = requiredConstraintNames
+    const ResultComponent: React.FunctionComponent<ValidationProps> = (props: ValidationProps) => {
+      const nonEmptyProps = requiredConstraintNames
         .filter(name => props[name] !== undefined)
         .length;
       if (nonEmptyProps !== requiredConstraintNames.length) {
@@ -34,6 +35,8 @@ function waitForRequiredConstraints (
       }
       return <Component {...props} />;
     };
+    ResultComponent.displayName = `waitForRequiredConstraints(${componentName(Component)})`;
+    return ResultComponent;
   };
 }
 

+ 1 - 1
pioneer/packages/joy-help/src/Help.tsx

@@ -32,7 +32,7 @@ const renderMemo = (accId: string) => {
 export const Component = (_props: Props) => {
   return (<>
     <div style={{ marginBottom: '1rem' }}>
-      Visit our <a href='https://github.com/Joystream/helpdesk' target='_blank'>helpdesk</a>{' '}
+      Visit our <a href='https://github.com/Joystream/helpdesk' target='_blank' rel="noopener noreferrer">helpdesk</a>{' '}
       for instructions and guides to get started!
     </div>
     <Grid divided='vertically'>

+ 0 - 1
pioneer/packages/joy-help/src/index.tsx

@@ -18,7 +18,6 @@ type Props = AppProps & I18nProps & {};
 type State = {};
 
 class App extends React.PureComponent<Props, State> {
-
   state: State = {};
 
   private buildTabs (): TabItem[] {

+ 52 - 46
pioneer/packages/joy-media/src/DiscoveryProvider.tsx

@@ -1,57 +1,61 @@
 import React, { useState, useEffect, useContext, createContext } from 'react';
 import { Message } from 'semantic-ui-react';
 import axios, { CancelToken } from 'axios';
-import { parse as parseUrl } from 'url';
 
 import { AccountId } from '@polkadot/types/interfaces';
 import { Vec } from '@polkadot/types';
-import { Url } from '@joystream/types/discovery'
+import { Url } from '@joystream/types/discovery';
 import ApiContext from '@polkadot/react-api/ApiContext';
 import { ApiProps } from '@polkadot/react-api/types';
 import { JoyInfo } from '@polkadot/joy-utils/JoyStatus';
+import { componentName } from '@polkadot/joy-utils/react/helpers';
 
 export type BootstrapNodes = {
-  bootstrapNodes?: Url[],
+  bootstrapNodes?: Url[];
 };
 
 export type DiscoveryProvider = {
-  resolveAssetEndpoint: (provider: AccountId, contentId?: string, cancelToken?: CancelToken) => Promise<string>
-  reportUnreachable: (provider: AccountId) => void
+  resolveAssetEndpoint: (provider: AccountId, contentId?: string, cancelToken?: CancelToken) => Promise<string>;
+  reportUnreachable: (provider: AccountId) => void;
 };
 
 export type DiscoveryProviderProps = {
-  discoveryProvider: DiscoveryProvider
+  discoveryProvider: DiscoveryProvider;
 };
 
 // return string Url with last `/` removed
-function normalizeUrl(url: string | Url): string {
-  let st = new String(url)
+function normalizeUrl (url: string | Url): string {
+  const st: string = url.toString();
   if (st.endsWith('/')) {
     return st.substring(0, st.length - 1);
   }
-  return st.toString()
+  return st.toString();
 }
 
 type ProviderStats = {
-  assetApiEndpoint: string,
-  unreachableReports: number,
-  resolvedAt: number,
+  assetApiEndpoint: string;
+  unreachableReports: number;
+  resolvedAt: number;
 }
 
-function newDiscoveryProvider({ bootstrapNodes }: BootstrapNodes): DiscoveryProvider {
-  let stats: Map<string, ProviderStats> = new Map();
+function newDiscoveryProvider ({ bootstrapNodes }: BootstrapNodes): DiscoveryProvider {
+  const stats: Map<string, ProviderStats> = new Map();
 
   const resolveAssetEndpoint = async (storageProvider: AccountId, contentId?: string, cancelToken?: CancelToken) => {
     const providerKey = storageProvider.toString();
 
     let stat = stats.get(providerKey);
 
-    if (!stat || (stat && (Date.now() > (stat.resolvedAt + (10 * 60 * 1000))))) {
-      for (let n = 0; bootstrapNodes && n < bootstrapNodes.length; n++) {
-        let discoveryUrl = normalizeUrl(bootstrapNodes[n])
+    if (
+      (!stat || (stat && (Date.now() > (stat.resolvedAt + (10 * 60 * 1000))))) &&
+      bootstrapNodes
+    ) {
+      for (let n = 0; n < bootstrapNodes.length; n++) {
+        const discoveryUrl = normalizeUrl(bootstrapNodes[n]);
 
         try {
-          parseUrl(discoveryUrl);
+          // eslint-disable-next-line no-new
+          new URL(discoveryUrl);
         } catch (err) {
           continue;
         }
@@ -61,7 +65,7 @@ function newDiscoveryProvider({ bootstrapNodes }: BootstrapNodes): DiscoveryProv
         try {
           console.log(`Resolving ${providerKey} using ${discoveryUrl}`);
 
-          const serviceInfo = await axios.get(serviceInfoQuery, { cancelToken }) as any
+          const serviceInfo = await axios.get(serviceInfoQuery, { cancelToken }) as any;
 
           if (!serviceInfo) {
             continue;
@@ -70,7 +74,7 @@ function newDiscoveryProvider({ bootstrapNodes }: BootstrapNodes): DiscoveryProv
           stats.set(providerKey, {
             assetApiEndpoint: normalizeUrl(JSON.parse(serviceInfo.data.serialized).asset.endpoint),
             unreachableReports: 0,
-            resolvedAt: Date.now(),
+            resolvedAt: Date.now()
           });
           break;
         } catch (err) {
@@ -85,50 +89,50 @@ function newDiscoveryProvider({ bootstrapNodes }: BootstrapNodes): DiscoveryProv
 
     stat = stats.get(providerKey);
 
-    console.log(stat)
+    console.log(stat);
 
     if (stat) {
       return `${stat.assetApiEndpoint}/asset/v0/${contentId || ''}`;
     }
 
-    throw new Error("Resolving failed.");
+    throw new Error('Resolving failed.');
   };
 
   const reportUnreachable = (provider: AccountId) => {
     const key = provider.toString();
-    let stat = stats.get(key);
+    const stat = stats.get(key);
     if (stat) {
       stat.unreachableReports = stat.unreachableReports + 1;
     }
-  }
+  };
 
   return { resolveAssetEndpoint, reportUnreachable };
 }
 
-const DiscoveryProviderContext = createContext<DiscoveryProvider>(undefined as unknown as DiscoveryProvider)
+const DiscoveryProviderContext = createContext<DiscoveryProvider>(undefined as unknown as DiscoveryProvider);
 
 export const DiscoveryProviderProvider = (props: React.PropsWithChildren<{}>) => {
   const api: ApiProps = useContext(ApiContext);
-  const [provider, setProvider] = useState<DiscoveryProvider | undefined>()
-  const [loaded, setLoaded] = useState<boolean | undefined>()
+  const [provider, setProvider] = useState<DiscoveryProvider | undefined>();
+  const [loaded, setLoaded] = useState<boolean | undefined>();
 
   useEffect(() => {
     const load = async () => {
-      if (loaded || !api) return
+      if (loaded || !api) return;
 
-      console.log('Discovery Provider: Loading bootstrap node from Substrate...')
+      console.log('Discovery Provider: Loading bootstrap node from Substrate...');
       const bootstrapNodes = await api.api.query.discovery.bootstrapEndpoints() as Vec<Url>;
-      setProvider(newDiscoveryProvider({ bootstrapNodes }))
-      setLoaded(true)
-      console.log('Discovery Provider: Initialized')
-    }
+      setProvider(newDiscoveryProvider({ bootstrapNodes }));
+      setLoaded(true);
+      console.log('Discovery Provider: Initialized');
+    };
 
     load();
-  }, [loaded])
+  }, [loaded]);
 
   if (!api || !api.isApiReady) {
     // Substrate API is not ready yet.
-    return null
+    return null;
   }
 
   if (!provider) {
@@ -139,30 +143,32 @@ export const DiscoveryProviderProvider = (props: React.PropsWithChildren<{}>) =>
           Loading bootstrap nodes... Please wait.
         </div>
       </Message>
-    )
+    );
   }
 
   return (
     <DiscoveryProviderContext.Provider value={provider}>
       {props.children}
     </DiscoveryProviderContext.Provider>
-  )
-}
+  );
+};
 
 export const useDiscoveryProvider = () =>
-  useContext(DiscoveryProviderContext)
+  useContext(DiscoveryProviderContext);
 
-export function withDiscoveryProvider(Component: React.ComponentType<DiscoveryProviderProps>) {
-  return (props: React.PropsWithChildren<{}>) => {
-    const discoveryProvider = useDiscoveryProvider()
+export function withDiscoveryProvider (Component: React.ComponentType<DiscoveryProviderProps>) {
+  const ResultComponent: React.FunctionComponent<{}> = (props: React.PropsWithChildren<{}>) => {
+    const discoveryProvider = useDiscoveryProvider();
     if (!discoveryProvider) {
-      return <JoyInfo title={`Please wait...`}>Loading discovery provider.</JoyInfo>
+      return <JoyInfo title={'Please wait...'}>Loading discovery provider.</JoyInfo>;
     }
 
     return (
       <Component {...props} discoveryProvider={discoveryProvider}>
         {props.children}
       </Component>
-    )
-  }
-}
+    );
+  };
+  ResultComponent.displayName = `withDiscoveryProvider(${componentName(Component)})`;
+  return ResultComponent;
+}

+ 48 - 48
pioneer/packages/joy-media/src/IterableFile.ts

@@ -2,60 +2,60 @@
 // https://gist.github.com/grishgrigoryan/bf6222d16d72cb28620399d27e83eb22
 
 interface IConfig{
-    chunkSize:number
+  chunkSize: number;
 }
 
-const DEFAULT_CHUNK_SIZE : number = 64 * 1024; // 64K
-
-export class IterableFile implements AsyncIterable<Buffer>{
-    private reader: FileReader;
-    private file: File
-    private config: IConfig = { chunkSize : DEFAULT_CHUNK_SIZE }
-
-    constructor(file: File, config :Partial<IConfig> = {}) {
-        this.file = file
-        this.reader = new FileReader();
-        Object.assign(this.config, config)
-    }
-
-    [Symbol.asyncIterator]() {
-        return this.readFile();
-    }
-
-    get chunkSize() {
-        return this.config.chunkSize;
-    }
-
-    get fileSize() {
-        return this.file.size;
-    }
-
-    readBlobAsBuffer(blob: Blob) : Promise<Buffer>{
-        return new Promise((resolve,reject)=>{
-            this.reader.onload = (e:any)=>{
-                e.target.result && resolve(Buffer.from(e.target.result));
-                e.target.error && reject(e.target.error);
-            };
-            this.reader.readAsArrayBuffer(blob);
-        })
-    }
-
-    async* readFile() {
-        let offset = 0;
-        let blob;
-        let result;
-
-        while (offset < this.fileSize) {
-            blob = this.file.slice(offset, this.chunkSize + offset);
-            result = await this.readBlobAsBuffer(blob);
-            offset += result.length;
-            yield result;
-        }
+const DEFAULT_CHUNK_SIZE: number = 64 * 1024; // 64K
+
+export class IterableFile implements AsyncIterable<Buffer> {
+  private reader: FileReader;
+  private file: File
+  private config: IConfig = { chunkSize: DEFAULT_CHUNK_SIZE }
+
+  constructor (file: File, config: Partial<IConfig> = {}) {
+    this.file = file;
+    this.reader = new FileReader();
+    Object.assign(this.config, config);
+  }
+
+  [Symbol.asyncIterator] () {
+    return this.readFile();
+  }
+
+  get chunkSize () {
+    return this.config.chunkSize;
+  }
+
+  get fileSize () {
+    return this.file.size;
+  }
+
+  readBlobAsBuffer (blob: Blob): Promise<Buffer> {
+    return new Promise((resolve, reject) => {
+      this.reader.onload = (e: any) => {
+        e.target.result && resolve(Buffer.from(e.target.result));
+        e.target.error && reject(e.target.error);
+      };
+      this.reader.readAsArrayBuffer(blob);
+    });
+  }
+
+  async * readFile () {
+    let offset = 0;
+    let blob;
+    let result;
+
+    while (offset < this.fileSize) {
+      blob = this.file.slice(offset, this.chunkSize + offset);
+      result = await this.readBlobAsBuffer(blob);
+      offset += result.length;
+      yield result;
     }
+  }
 }
 
 // Usage:
 //  let iterableFile = new IterableFile(file)
 //  for await (const chunk: Buffer of iterableFile) {
 //      doSomethingWithBuffer(chunk)
-//  }
+//  }

+ 29 - 33
pioneer/packages/joy-media/src/MediaView.tsx

@@ -6,66 +6,64 @@ import { useTransportContext } from './TransportContext';
 import { withMembershipRequired } from '@polkadot/joy-utils/MyAccount';
 
 type InitialPropsWithMembership<A> = A & {
-  myAddress?: string
-  myMemberId?: MemberId
+  myAddress?: string;
+  myMemberId?: MemberId;
 }
 
 type ResolverProps<A> = InitialPropsWithMembership<A> & {
-  transport: MediaTransport
+  transport: MediaTransport;
 }
 
 type BaseProps<A, B> = {
-  component: React.ComponentType<A & B>
-  unresolvedView?: React.ReactElement
-  resolveProps?: (props: ResolverProps<A>) => Promise<B>
+  component: React.ComponentType<A & B>;
+  unresolvedView?: React.ReactElement;
+  resolveProps?: (props: ResolverProps<A>) => Promise<B>;
 
   /**
    * Array of property names that can trigger re-render of the view,
-   * if values of such properties changed. 
+   * if values of such properties changed.
    */
-  triggers?: (keyof A)[]
+  triggers?: (keyof A)[];
 
   /** Set `true` if only members should have access to this component. `false` by default. */
-  membersOnly?: boolean
+  membersOnly?: boolean;
 }
 
-function serializeTrigger(val: any): any {
-  if (['number', 'boolean', 'string'].indexOf(typeof val) >= 0) {
-    return val
+function serializeTrigger (val: any): any {
+  if (['number', 'boolean', 'string'].includes(typeof val)) {
+    return val;
   } else if (typeof val === 'object' && typeof val.toString === 'function') {
-    return val.toString()
+    return val.toString();
   } else {
-    return undefined
+    return undefined;
   }
 }
 
 export function MediaView<A = {}, B = {}> (baseProps: BaseProps<A, B>) {
-
   function InnerView (initialProps: A & B) {
     const { component: Component, resolveProps, triggers = [], unresolvedView = null } = baseProps;
 
     const transport = useTransportContext();
     const { myAddress, myMemberId } = useMyMembership();
-    const resolverProps = {...initialProps, transport, myAddress, myMemberId }
-    
-    const [ resolvedProps, setResolvedProps ] = useState({} as B);
-    const [ propsResolved, setPropsResolved ] = useState(false);
+    const resolverProps = { ...initialProps, transport, myAddress, myMemberId };
 
-    const initialDeps = triggers.map(propName => serializeTrigger(initialProps[propName]))
-    const rerenderDeps = [ ...initialDeps, myAddress ]
+    const [resolvedProps, setResolvedProps] = useState({} as B);
+    const [propsResolved, setPropsResolved] = useState(false);
 
-    useEffect(() => {
+    const initialDeps = triggers.map(propName => serializeTrigger(initialProps[propName]));
+    const rerenderDeps = [...initialDeps, myAddress];
 
+    useEffect(() => {
       async function doResolveProps () {
         if (typeof resolveProps !== 'function') return;
-        
+
         console.log('Resolving props of media view');
 
         // Transport session allows us to cache loaded channels, entites and classes
         // during the render of this view:
-        transport.openSession()
+        transport.openSession();
         setResolvedProps(await resolveProps(resolverProps));
-        transport.closeSession()
+        transport.closeSession();
         setPropsResolved(true);
       }
 
@@ -75,23 +73,21 @@ export function MediaView<A = {}, B = {}> (baseProps: BaseProps<A, B>) {
         doResolveProps();
       }
     }, rerenderDeps);
-    
+
     console.log('Rerender deps of Media View:', rerenderDeps);
 
     const renderResolving = () => {
-      return unresolvedView
-        ? unresolvedView
-        : <div className='ui active centered inline loader' />
-    }
+      return unresolvedView || <div className='ui active centered inline loader' />;
+    };
 
     return propsResolved
       ? <Component {...initialProps} {...resolvedProps} />
-      : renderResolving()
+      : renderResolving();
   }
 
-  const { membersOnly = false } = baseProps
+  const { membersOnly = false } = baseProps;
 
   return membersOnly
     ? withMembershipRequired(InnerView)
-    : InnerView
+    : InnerView;
 }

+ 12 - 15
pioneer/packages/joy-media/src/TransportContext.tsx

@@ -8,36 +8,33 @@ import { ApiProps } from '@polkadot/react-api/types';
 export const TransportContext = createContext<MediaTransport>(undefined as unknown as MediaTransport);
 
 export const useTransportContext = () =>
-  useContext(TransportContext)
+  useContext(TransportContext);
 
 export const MockTransportProvider = (props: React.PropsWithChildren<{}>) =>
   <TransportContext.Provider value={new MockTransport()}>
     {props.children}
-  </TransportContext.Provider>
+  </TransportContext.Provider>;
 
 export const SubstrateTransportProvider = (props: React.PropsWithChildren<{}>) => {
   const api: ApiProps = useContext(ApiContext);
-  const [ transport, setTransport ] = useState<SubstrateTransport>()
-  const [ loaded, setLoaded ] = useState<boolean>()
-  
+  const [transport, setTransport] = useState<SubstrateTransport>();
+  const [loaded, setLoaded] = useState<boolean>();
+
   useEffect(() => {
-    const load = async () => {
-      if (!loaded && api && api.isApiReady) {
-        setTransport(new SubstrateTransport(api))
-        setLoaded(true)
-      }
+    if (!loaded && api && api.isApiReady) {
+      setTransport(new SubstrateTransport(api));
+      setLoaded(true);
     }
-    load()
-  }, [loaded])
+  }, [loaded]);
 
   if (!transport) {
     // Substrate API is not ready yet.
-    return null
+    return null;
   }
 
   return (
     <TransportContext.Provider value={transport}>
       {props.children}
     </TransportContext.Provider>
-  )
-}
+  );
+};

+ 42 - 41
pioneer/packages/joy-media/src/Upload.tsx

@@ -29,25 +29,25 @@ const MAX_FILE_SIZE_MB = 500;
 const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
 
 type Props = ApiProps & I18nProps & DiscoveryProviderProps & {
-  channelId: ChannelId
-  history?: History
+  channelId: ChannelId;
+  history?: History;
   match: {
     params: {
-      channelId: string
-    }
-  }
+      channelId: string;
+    };
+  };
 };
 
 type State = {
-  error?: any,
-  file?: File,
-  computingHash: boolean,
-  ipfs_cid?: string,
-  newContentId: ContentId,
-  discovering: boolean,
-  uploading: boolean,
-  progress: number,
-  cancelSource: CancelTokenSource
+  error?: any;
+  file?: File;
+  computingHash: boolean;
+  ipfs_cid?: string;
+  newContentId: ContentId;
+  discovering: boolean;
+  uploading: boolean;
+  progress: number;
+  cancelSource: CancelTokenSource;
 };
 
 const defaultState = (): State => ({
@@ -63,13 +63,12 @@ const defaultState = (): State => ({
 });
 
 class Component extends React.PureComponent<Props, State> {
-
   state = defaultState();
 
   componentWillUnmount () {
     this.setState({
       discovering: false,
-      uploading: false,
+      uploading: false
     });
 
     const { cancelSource } = this.state;
@@ -118,7 +117,7 @@ class Component extends React.PureComponent<Props, State> {
     if (!file || !file.name) return <JoyInfo title='Loading...' />;
 
     const success = !error && progress >= 100;
-    const { history, match: { params: { channelId } } } = this.props
+    const { history, match: { params: { channelId } } } = this.props;
 
     return <div style={{ width: '100%' }}>
       {this.renderProgress()}
@@ -134,7 +133,7 @@ class Component extends React.PureComponent<Props, State> {
   }
 
   private renderDiscovering () {
-    return <JoyInfo title={`Please wait...`}>Contacting storage provider.</JoyInfo>;
+    return <JoyInfo title={'Please wait...'}>Contacting storage provider.</JoyInfo>;
   }
 
   private renderProgress () {
@@ -144,9 +143,9 @@ class Component extends React.PureComponent<Props, State> {
 
     let label = '';
     if (active) {
-      label = `Your file is uploading. Please keep this page open until it's done.`;
+      label = 'Your file is uploading. Please keep this page open until it\'s done.';
     } else if (success) {
-      label = `Uploaded! Click "Publish" button to make your file live.`;
+      label = 'Uploaded! Click "Publish" button to make your file live.';
     }
 
     return <Progress
@@ -196,20 +195,21 @@ class Component extends React.PureComponent<Props, State> {
     </div>;
   }
 
-  private onFileSelected = async (file: File) => {
+  private onFileSelected = (file: File) => {
     if (!file.size) {
-      this.setState({ error: `You cannot upload an empty file.` });
+      this.setState({ error: 'You cannot upload an empty file.' });
     } else if (file.size > MAX_FILE_SIZE_BYTES) {
-      this.setState({ error:
+      this.setState({
+        error:
         `You can't upload files larger than ${MAX_FILE_SIZE_MB} MBytes in size.`
       });
     } else {
-      this.setState({ file, computingHash: true })
+      this.setState({ file, computingHash: true });
       this.startComputingHash();
     }
   }
 
-  private async startComputingHash() {
+  private async startComputingHash () {
     const { file } = this.state;
 
     if (!file) {
@@ -220,26 +220,26 @@ class Component extends React.PureComponent<Props, State> {
       const iterableFile = new IterableFile(file, { chunkSize: 65535 });
       const ipfs_cid = await IpfsHash.of(iterableFile);
 
-      this.hashComputationComplete(ipfs_cid)
+      this.hashComputationComplete(ipfs_cid);
     } catch (err) {
       return this.hashComputationComplete(undefined, err);
     }
   }
 
-  private hashComputationComplete(ipfs_cid: string | undefined, error?: string) {
+  private hashComputationComplete (ipfs_cid: string | undefined, error?: string) {
     if (!error) {
-      console.log('Computed IPFS hash:', ipfs_cid)
+      console.log('Computed IPFS hash:', ipfs_cid);
     }
 
     this.setState({
       computingHash: false,
       ipfs_cid,
       error
-    })
+    });
   }
 
-  private renderComputingHash() {
-    return <JoyInfo title='Processing your file. Please wait...' />
+  private renderComputingHash () {
+    return <JoyInfo title='Processing your file. Please wait...' />;
   }
 
   private buildTxParams = () => {
@@ -249,16 +249,17 @@ class Component extends React.PureComponent<Props, State> {
     // TODO get corresponding data type id based on file content
     const dataObjectTypeId = new BN(1);
 
-    return [ newContentId, dataObjectTypeId, new BN(file.size), ipfs_cid];
+    return [newContentId, dataObjectTypeId, new BN(file.size), ipfs_cid];
   }
 
   private onDataObjectCreated = async (_txResult: SubmittableResult) => {
-    this.setState({ discovering: true});
+    this.setState({ discovering: true });
 
     const { api } = this.props;
     const { newContentId } = this.state;
+    let dataObject: Option<DataObject>;
     try {
-      var dataObject = await api.query.dataDirectory.dataObjectByContentId(newContentId) as Option<DataObject>;
+      dataObject = await api.query.dataDirectory.dataObjectByContentId(newContentId) as Option<DataObject>;
     } catch (err) {
       this.setState({
         error: err,
@@ -289,7 +290,7 @@ class Component extends React.PureComponent<Props, State> {
     if (!file || !file.size) {
       this.setState({
         error: new Error('No file to upload!'),
-        discovering: false,
+        discovering: false
       });
       return;
     }
@@ -304,7 +305,7 @@ class Component extends React.PureComponent<Props, State> {
       },
       cancelToken: cancelSource.token,
       onUploadProgress: (progressEvent: any) => {
-        const percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total );
+        const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
         this.setState({
           progress: percentCompleted
         });
@@ -312,13 +313,13 @@ class Component extends React.PureComponent<Props, State> {
     };
 
     const { discoveryProvider } = this.props;
-
+    let url: string;
     try {
-      var url = await discoveryProvider.resolveAssetEndpoint(storageProvider, contentId, cancelSource.token);
+      url = await discoveryProvider.resolveAssetEndpoint(storageProvider, contentId, cancelSource.token);
     } catch (err) {
       return this.setState({
         error: new Error(`Failed to contact storage provider: ${err.message}`),
-        discovering: false,
+        discovering: false
       });
     }
 
@@ -334,7 +335,7 @@ class Component extends React.PureComponent<Props, State> {
 
     try {
       await axios.put<{ message: string }>(url, file, config);
-    } catch(err) {
+    } catch (err) {
       this.setState({ progress: 0, error: err, uploading: false });
       if (axios.isCancel(err)) {
         return;
@@ -353,4 +354,4 @@ export const UploadWithRouter = withMulti(
   withApi,
   withMembershipRequired,
   withDiscoveryProvider
-)
+);

+ 5 - 5
pioneer/packages/joy-media/src/channels/ChannelAvatar.tsx

@@ -9,8 +9,8 @@ const defaultSizePx = 75;
 export type ChannelAvatarSize = 'big' | 'default' | 'small';
 
 type Props = {
-  channel: ChannelEntity,
-  size?: ChannelAvatarSize
+  channel: ChannelEntity;
+  size?: ChannelAvatarSize;
 }
 
 function sizeToPx (size: ChannelAvatarSize): number {
@@ -28,10 +28,10 @@ export function ChannelAvatar (props: Props) {
   return (
     <Link to={`/media/channels/${channel.id}`}>
       <BgImg
-        className={`ChannelAvatar ` + size}
+        className={'ChannelAvatar ' + size}
         url={channel.avatar || DEFAULT_THUMBNAIL_URL}
         size={sizeToPx(size)}
       />
     </Link>
-  )
-}
+  );
+}

+ 4 - 4
pioneer/packages/joy-media/src/channels/ChannelAvatarAndName.tsx

@@ -4,13 +4,13 @@ import { ChannelAvatar } from './ChannelAvatar';
 import { ChannelNameAsLink } from './ChannelNameAsLink';
 
 type Props = {
-  channel: ChannelEntity
+  channel: ChannelEntity;
 }
 
 export const ChannelAvatarAndName = (props: Props) => {
   const { channel } = props;
   return (
-    <div className={`ChannelPreview small`}>
+    <div className={'ChannelPreview small'}>
       <ChannelAvatar channel={channel} size='small' />
       <div className='ChannelDetails'>
         <h3 className='ChannelTitle' style={{ display: 'block' }}>
@@ -18,5 +18,5 @@ export const ChannelAvatarAndName = (props: Props) => {
         </h3>
       </div>
     </div>
-  )
-}
+  );
+};

+ 1 - 1
pioneer/packages/joy-media/src/channels/ChannelHeader.tsx

@@ -4,7 +4,7 @@ import { ChannelEntity } from '../entities/ChannelEntity';
 import { ChannelPreview } from './ChannelPreview';
 
 type Props = {
-  channel: ChannelEntity
+  channel: ChannelEntity;
 }
 
 export function ChannelHeader (props: Props) {

+ 9 - 9
pioneer/packages/joy-media/src/channels/ChannelHelpers.ts

@@ -1,10 +1,10 @@
 import { AccountId } from '@polkadot/types/interfaces';
-import { ChannelType } from "../schemas/channel/Channel";
-import { ChannelPublicationStatusAllValues } from "@joystream/types/content-working-group";
+import { ChannelType } from '../schemas/channel/Channel';
+import { ChannelPublicationStatusAllValues } from '@joystream/types/content-working-group';
 
 export const ChannelPublicationStatusDropdownOptions =
   ChannelPublicationStatusAllValues
-    .map(x => ({ key: x, value: x, text: x }))
+    .map(x => ({ key: x, value: x, text: x }));
 
 export const isVideoChannel = (channel: ChannelType) => {
   return channel.content === 'Video';
@@ -15,20 +15,20 @@ export const isMusicChannel = (channel: ChannelType) => {
 };
 
 export const isAccountAChannelOwner = (channel?: ChannelType, account?: AccountId | string): boolean => {
-  return (channel && account) ? channel.roleAccount.eq(account) : false
+  return (channel && account) ? channel.roleAccount.eq(account) : false;
 };
 
-export function isPublicChannel(channel: ChannelType): boolean {
+export function isPublicChannel (channel: ChannelType): boolean {
   return (
     channel.publicationStatus === 'Public' &&
     channel.curationStatus !== 'Censored'
   );
 }
 
-export function isCensoredChannel(channel: ChannelType): boolean {
-  return channel.curationStatus == 'Censored'
+export function isCensoredChannel (channel: ChannelType): boolean {
+  return channel.curationStatus === 'Censored';
 }
 
-export function isVerifiedChannel(channel: ChannelType): boolean {
-  return channel.verified
+export function isVerifiedChannel (channel: ChannelType): boolean {
+  return channel.verified;
 }

+ 5 - 5
pioneer/packages/joy-media/src/channels/ChannelNameAsLink.tsx

@@ -3,9 +3,9 @@ import { Link } from 'react-router-dom';
 import { ChannelEntity } from '../entities/ChannelEntity';
 
 type Props = {
-  channel: ChannelEntity
-  className?: string
-  style?: React.CSSProperties
+  channel: ChannelEntity;
+  className?: string;
+  style?: React.CSSProperties;
 }
 
 export const ChannelNameAsLink = (props: Props) => {
@@ -14,5 +14,5 @@ export const ChannelNameAsLink = (props: Props) => {
     <Link to={`/media/channels/${channel.id}`} className={className} style={style}>
       {channel.title || channel.handle}
     </Link>
-  )
-}
+  );
+};

+ 12 - 12
pioneer/packages/joy-media/src/channels/ChannelPreview.tsx

@@ -4,18 +4,18 @@ import ReactMarkdown from 'react-markdown';
 import { Icon, Label, SemanticICONS, SemanticCOLORS } from 'semantic-ui-react';
 import { ChannelEntity } from '../entities/ChannelEntity';
 import { ChannelAvatar, ChannelAvatarSize } from './ChannelAvatar';
-import { isPublicChannel } from './ChannelHelpers';
-import { isMusicChannel, isVideoChannel, isAccountAChannelOwner, isVerifiedChannel } from './ChannelHelpers';
+import { isPublicChannel, isMusicChannel, isVideoChannel, isAccountAChannelOwner, isVerifiedChannel } from './ChannelHelpers';
+
 import { useMyMembership } from '@polkadot/joy-utils/MyMembershipContext';
 import { nonEmptyStr } from '@polkadot/joy-utils/index';
 import { CurationPanel } from './CurationPanel';
 import { ChannelNameAsLink } from './ChannelNameAsLink';
 
 type ChannelPreviewProps = {
-  channel: ChannelEntity
-  size?: ChannelAvatarSize
-  withSubtitle?: boolean
-  withDescription?: boolean
+  channel: ChannelEntity;
+  size?: ChannelAvatarSize;
+  withSubtitle?: boolean;
+  withDescription?: boolean;
 };
 
 export const ChannelPreview = (props: ChannelPreviewProps) => {
@@ -26,11 +26,11 @@ export const ChannelPreview = (props: ChannelPreviewProps) => {
   let icon: 'music' | 'film' | undefined;
 
   if (isMusicChannel(channel)) {
-    subtitle = 'Music channel',
-    icon = 'music'
+    subtitle = 'Music channel';
+    icon = 'music';
   } else if (isVideoChannel(channel)) {
-    subtitle = 'Video channel'
-    icon = 'film'
+    subtitle = 'Video channel';
+    icon = 'film';
   }
 
   let visibilityIcon: SemanticICONS = 'eye';
@@ -110,5 +110,5 @@ export const ChannelPreview = (props: ChannelPreviewProps) => {
       {/* <ChannelStats channel={channel} /> */}
 
     </div>
-  </>
-}
+  </>;
+};

+ 6 - 6
pioneer/packages/joy-media/src/channels/ChannelPreviewStats.tsx

@@ -5,18 +5,18 @@ import { ChannelEntity } from '../entities/ChannelEntity';
 import { formatNumber } from '@polkadot/util';
 
 type Props = {
-  channel: ChannelEntity
+  channel: ChannelEntity;
 };
 
 export const ChannelPreviewStats = (props: Props) => {
   const { channel } = props;
   const statSize = 'tiny';
 
-  let itemsPublishedLabel = ''
+  let itemsPublishedLabel = '';
   if (channel.content === 'Video') {
-    itemsPublishedLabel = 'Videos'
+    itemsPublishedLabel = 'Videos';
   } else if (channel.content === 'Music') {
-    itemsPublishedLabel = 'Music tracks'
+    itemsPublishedLabel = 'Music tracks';
   }
 
   return (
@@ -38,5 +38,5 @@ export const ChannelPreviewStats = (props: Props) => {
         </Statistic>
       </div>
     </div>
-  )
-}
+  );
+};

+ 11 - 11
pioneer/packages/joy-media/src/channels/ChannelsByOwner.tsx

@@ -8,14 +8,14 @@ import { ChannelContentTypeValue } from '@joystream/types/content-working-group'
 import { ChannelPreview } from './ChannelPreview';
 
 export type ChannelsByOwnerProps = {
-  accountId: AccountId,
-  suspended?: boolean,
-  channels?: ChannelEntity[]
+  accountId: AccountId;
+  suspended?: boolean;
+  channels?: ChannelEntity[];
 };
 
 const TabsAndChannels = (props: ChannelsByOwnerProps) => {
   const { channels: allChannels = [] } = props;
-  const [ channels, setChannels ] = useState(allChannels);
+  const [channels, setChannels] = useState(allChannels);
 
   let videoChannelsCount = 0;
   let musicChannelsCount = 0;
@@ -34,18 +34,18 @@ const TabsAndChannels = (props: ChannelsByOwnerProps) => {
   ];
 
   const contentTypeByTabIndex: Array<ChannelContentTypeValue | undefined> =
-    [ undefined, 'Video', 'Music' ];
+    [undefined, 'Video', 'Music'];
 
   const switchTab = (activeIndex: number) => {
     const activeContentType = contentTypeByTabIndex[activeIndex];
     if (activeContentType === undefined) {
-      setChannels(allChannels)
+      setChannels(allChannels);
     } else {
       setChannels(allChannels.filter(
         (x) => x.content === activeContentType)
-      )
+      );
     }
-  }
+  };
 
   return <>
     <Tab
@@ -54,7 +54,7 @@ const TabsAndChannels = (props: ChannelsByOwnerProps) => {
       style={{ display: 'inline-flex', margin: '0 2rem 1rem 0' }}
       onTabChange={(_e, data) => switchTab(data.activeIndex as number)}
     />
-    <Link to={`/media/channels/new`} className='ui button'>
+    <Link to={'/media/channels/new'} className='ui button'>
       <i className='icon plus' />
       Create Channel
     </Link>
@@ -63,8 +63,8 @@ const TabsAndChannels = (props: ChannelsByOwnerProps) => {
         <ChannelPreview channel={channel} withDescription />
       </Segment>
     )}
-  </>
-}
+  </>;
+};
 
 export function ChannelsByOwner (props: ChannelsByOwnerProps) {
   const { suspended = false, channels = [] } = props;

+ 3 - 3
pioneer/packages/joy-media/src/channels/ChannelsByOwner.view.tsx

@@ -18,7 +18,7 @@ export const ChannelsByOwnerView = MediaView<Props>({
 });
 
 export const ChannelsByOwnerWithRouter = (props: Props & RouteComponentProps<any>) => {
-  const { match: { params: { account }}} = props;
+  const { match: { params: { account } } } = props;
 
   if (account) {
     try {
@@ -28,5 +28,5 @@ export const ChannelsByOwnerWithRouter = (props: Props & RouteComponentProps<any
     }
   }
 
-  return <JoyError title={`Invalid account address in URL`}>{account}</JoyError>
-}
+  return <JoyError title={'Invalid account address in URL'}>{account}</JoyError>;
+};

+ 15 - 15
pioneer/packages/joy-media/src/channels/CurationPanel.tsx

@@ -7,7 +7,7 @@ import { ChannelCurationStatus } from '@joystream/types/content-working-group';
 import { AccountId } from '@polkadot/types/interfaces';
 
 type ChannelCurationPanelProps = {
-  channel: ChannelEntity
+  channel: ChannelEntity;
 };
 
 export const CurationPanel = (props: ChannelCurationPanelProps) => {
@@ -16,18 +16,18 @@ export const CurationPanel = (props: ChannelCurationPanelProps) => {
 
   const canUseAccount = (account: AccountId) => {
     if (!allAccounts || !Object.keys(allAccounts).length) {
-      return false
+      return false;
     }
 
     const ix = Object.keys(allAccounts).findIndex((key) => {
-      return account.eq(allAccounts[key].json.address)
+      return account.eq(allAccounts[key].json.address);
     });
 
-    return ix != -1
-  }
+    return ix !== -1;
+  };
 
   const renderToggleCensorshipButton = () => {
-    if (!curationActor) { return null }
+    if (!curationActor) { return null; }
 
     const [curation_actor, role_account] = curationActor;
     const accountAvailable = canUseAccount(role_account);
@@ -52,11 +52,11 @@ export const CurationPanel = (props: ChannelCurationPanelProps) => {
         new_curation_status // toggled curation status
       ]}
       tx={'contentWorkingGroup.updateChannelAsCurationActor'}
-    />
-  }
+    />;
+  };
 
   const renderToggleVerifiedButton = () => {
-    if (!curationActor) { return null }
+    if (!curationActor) { return null; }
 
     const [curation_actor, role_account] = curationActor;
     const accountAvailable = canUseAccount(role_account);
@@ -76,13 +76,13 @@ export const CurationPanel = (props: ChannelCurationPanelProps) => {
         null // not changing curation status
       ]}
       tx={'contentWorkingGroup.updateChannelAsCurationActor'}
-    />
-  }
+    />;
+  };
 
   return <>
     <div style={{ float: 'right' }}>
-    {renderToggleCensorshipButton()}
-    {renderToggleVerifiedButton()}
+      {renderToggleCensorshipButton()}
+      {renderToggleVerifiedButton()}
     </div>
-  </>
-}
+  </>;
+};

+ 20 - 24
pioneer/packages/joy-media/src/channels/EditChannel.tsx

@@ -19,11 +19,11 @@ import { ChannelValidationConstraints } from '../transport';
 import { JoyError } from '@polkadot/joy-utils/JoyStatus';
 
 export type OuterProps = {
-  history?: History,
-  id?: ChannelId,
-  entity?: ChannelType,
-  constraints?: ChannelValidationConstraints,
-  opts?: MediaDropdownOptions
+  history?: History;
+  id?: ChannelId;
+  entity?: ChannelType;
+  constraints?: ChannelValidationConstraints;
+  opts?: MediaDropdownOptions;
 };
 
 type FormValues = ChannelFormValues;
@@ -57,7 +57,7 @@ const InnerForm = (props: MediaFormProps<OuterProps, FormValues>) => {
   const { myAccountId, myMemberId } = useMyMembership();
 
   if (entity && !isAccountAChannelOwner(entity, myAccountId)) {
-    return <JoyError title={`Only owner can edit channel`} />
+    return <JoyError title={'Only owner can edit channel'} />;
   }
 
   const { avatar } = values;
@@ -67,19 +67,17 @@ const InnerForm = (props: MediaFormProps<OuterProps, FormValues>) => {
   // return null
 
   const onTxSuccess: TxCallback = (txResult: SubmittableResult) => {
-    setSubmitting(false)
-    if (!history) return
+    setSubmitting(false);
+    if (!history) return;
 
-    const id = existingId
-      ? existingId
-      : findFirstParamOfSubstrateEvent<ChannelId>(txResult, 'ChannelCreated')
+    const id = existingId || findFirstParamOfSubstrateEvent<ChannelId>(txResult, 'ChannelCreated');
 
-    console.log('Channel id:', id?.toString())
+    console.log('Channel id:', id?.toString());
 
     if (id) {
-      history.push('/media/channels/' + id.toString())
+      history.push('/media/channels/' + id.toString());
     }
-  }
+  };
 
   const buildTxParams = () => {
     if (!isValid) return [];
@@ -88,7 +86,6 @@ const InnerForm = (props: MediaFormProps<OuterProps, FormValues>) => {
     const publicationStatus = new ChannelPublicationStatus('Public');
 
     if (!entity) {
-
       // Create a new channel
 
       const channelOwner = myMemberId;
@@ -107,7 +104,6 @@ const InnerForm = (props: MediaFormProps<OuterProps, FormValues>) => {
         publicationStatus
       ];
     } else {
-
       // Update an existing channel
 
       const updOptText = (field: ChannelGenericProp): Option<OptionalText> => {
@@ -115,20 +111,20 @@ const InnerForm = (props: MediaFormProps<OuterProps, FormValues>) => {
           isFieldChanged(field)
             ? newOptionalText(values[field.id])
             : null
-        )
-      }
+        );
+      };
 
       const updHandle = new Option(Text,
         isFieldChanged(Fields.handle)
           ? values[Fields.handle.id]
           : null
-      )
+      );
 
       const updPublicationStatus = new Option(ChannelPublicationStatus,
         isFieldChanged(Fields.publicationStatus)
           ? new ChannelPublicationStatus(values[Fields.publicationStatus.id] as any)
           : null
-      )
+      );
 
       return [
         new ChannelId(entity.id),
@@ -173,7 +169,7 @@ const InnerForm = (props: MediaFormProps<OuterProps, FormValues>) => {
       onClick={onSubmit}
       txFailedCb={onTxFailed}
       txSuccessCb={onTxSuccess}
-    />
+    />;
 
   return <div className='EditMetaBox'>
     <div className='EditMetaThumb'>
@@ -207,10 +203,10 @@ export const EditForm = withFormik<OuterProps, FormValues>({
   },
 
   validationSchema: (props: OuterProps): any => {
-    const { constraints } = props
-    if (!constraints) return null
+    const { constraints } = props;
+    if (!constraints) return null;
 
-    return buildChannelValidationSchema(constraints)
+    return buildChannelValidationSchema(constraints);
   },
 
   handleSubmit: () => {

+ 6 - 6
pioneer/packages/joy-media/src/channels/EditChannel.view.tsx

@@ -10,19 +10,19 @@ type Props = OuterProps;
 export const EditChannelView = MediaView<Props>({
   component: EditForm,
   membersOnly: true,
-  triggers: [ 'id' ],
+  triggers: ['id'],
   resolveProps: async (props) => {
     const { transport, id } = props;
     const entity = id && await transport.channelById(id);
-    const constraints = await transport.channelValidationConstraints()
+    const constraints = await transport.channelValidationConstraints();
     return { entity, constraints };
   }
-})
+});
 
 type WithRouterProps = Props & RouteComponentProps<any>
 
 export const EditChannelWithRouter = (props: WithRouterProps) => {
-  const { match: { params: { id }}} = props;
+  const { match: { params: { id } } } = props;
 
   if (id) {
     try {
@@ -32,5 +32,5 @@ export const EditChannelWithRouter = (props: WithRouterProps) => {
     }
   }
 
-  return <JoyError title={`Invalid channel id in URL`}>{id}</JoyError>
-}
+  return <JoyError title={'Invalid channel id in URL'}>{id}</JoyError>;
+};

+ 7 - 7
pioneer/packages/joy-media/src/channels/ViewChannel.tsx

@@ -11,22 +11,22 @@ import { isVideoChannel, isMusicChannel } from './ChannelHelpers';
 import { JoyError } from '@polkadot/joy-utils/JoyStatus';
 
 export type ViewChannelProps = {
-  id: ChannelId,
-  channel?: ChannelEntity,
+  id: ChannelId;
+  channel?: ChannelEntity;
 
   // Video channel specific:
-  videos?: VideoType[],
+  videos?: VideoType[];
 
   // Music channel specific:
-  albums?: MusicAlbumPreviewProps[],
-  tracks?: MusicTrackReaderPreviewProps[]
+  albums?: MusicAlbumPreviewProps[];
+  tracks?: MusicTrackReaderPreviewProps[];
 }
 
 export function ViewChannel (props: ViewChannelProps) {
   const { channel, videos = [], albums = [], tracks = [] } = props;
 
   if (!channel) {
-    return <JoyError title={`Channel was not found`} />
+    return <JoyError title={'Channel was not found'} />;
   }
 
   if (isVideoChannel(channel)) {
@@ -35,6 +35,6 @@ export function ViewChannel (props: ViewChannelProps) {
   } else if (isMusicChannel(channel)) {
     return <ViewMusicChannel channel={channel} albums={albums} tracks={tracks} />;
   } else {
-    return <JoyError title={`Unsupported channel type`}>{channel.content}</JoyError>
+    return <JoyError title={'Unsupported channel type'}>{channel.content}</JoyError>;
   }
 }

+ 4 - 4
pioneer/packages/joy-media/src/channels/ViewChannel.view.tsx

@@ -9,7 +9,7 @@ type Props = ViewChannelProps;
 
 export const ViewChannelView = MediaView<Props>({
   component: ViewChannel,
-  triggers: [ 'id' ],
+  triggers: ['id'],
   resolveProps: async (props) => {
     const { transport, id } = props;
     const channel = await transport.channelById(id);
@@ -19,7 +19,7 @@ export const ViewChannelView = MediaView<Props>({
 });
 
 export const ViewChannelWithRouter = (props: Props & RouteComponentProps<any>) => {
-  const { match: { params: { id }}} = props;
+  const { match: { params: { id } } } = props;
 
   if (id) {
     try {
@@ -29,5 +29,5 @@ export const ViewChannelWithRouter = (props: Props & RouteComponentProps<any>) =
     }
   }
 
-  return <JoyError title={`Invalid channel id in URL`}>{id}</JoyError>
-}
+  return <JoyError title={'Invalid channel id in URL'}>{id}</JoyError>;
+};

+ 14 - 14
pioneer/packages/joy-media/src/channels/ViewMusicChannel.tsx

@@ -7,41 +7,41 @@ import { MusicTrackReaderPreview, MusicTrackReaderPreviewProps } from '../music/
 import NoContentYet from '../common/NoContentYet';
 
 type Props = {
-  channel: ChannelEntity,
-  albums?: MusicAlbumPreviewProps[],
-  tracks?: MusicTrackReaderPreviewProps[]
+  channel: ChannelEntity;
+  albums?: MusicAlbumPreviewProps[];
+  tracks?: MusicTrackReaderPreviewProps[];
 };
 
 function NoAlbums () {
-  return <NoContentYet>Channel has no music albums yet.</NoContentYet>
+  return <NoContentYet>Channel has no music albums yet.</NoContentYet>;
 }
 
 function NoTracks () {
-  return <NoContentYet>Channel has no music tracks yet.</NoContentYet>
+  return <NoContentYet>Channel has no music tracks yet.</NoContentYet>;
 }
 
 export function ViewMusicChannel (props: Props) {
   const { channel, albums = [], tracks = [] } = props;
-  
+
   const renderAlbumsSection = () => (
     !albums.length
       ? <NoAlbums />
-      : <Section title={`Music albums`}>
-          {albums.map(x => <MusicAlbumPreview {...x} />)}
-        </Section>
+      : <Section title={'Music albums'}>
+        {albums.map(x => <MusicAlbumPreview key={x.id} {...x} />)}
+      </Section>
   );
 
   const renderTracksSection = () => (
     !tracks.length
       ? <NoTracks />
-      : <Section title={`Music tracks`}>
-          {tracks.map(x => <MusicTrackReaderPreview {...x} />)}
-        </Section>
+      : <Section title={'Music tracks'}>
+        {tracks.map(x => <MusicTrackReaderPreview key={x.id} {...x} />)}
+      </Section>
   );
-  
+
   return <div className='JoyViewChannel'>
     <ChannelHeader channel={channel} />
     {renderAlbumsSection()}
     {renderTracksSection()}
-  </div>
+  </div>;
 }

+ 10 - 10
pioneer/packages/joy-media/src/channels/ViewVideoChannel.tsx

@@ -6,12 +6,12 @@ import { VideoPreview, VideoPreviewProps } from '../video/VideoPreview';
 import NoContentYet from '../common/NoContentYet';
 
 type Props = {
-  channel: ChannelEntity,
-  videos?: VideoPreviewProps[]
+  channel: ChannelEntity;
+  videos?: VideoPreviewProps[];
 };
 
 function NoVideosYet () {
-  return <NoContentYet>Channel has no videos yet.</NoContentYet>
+  return <NoContentYet>Channel has no videos yet.</NoContentYet>;
 }
 
 export function ViewVideoChannel (props: Props) {
@@ -20,15 +20,15 @@ export function ViewVideoChannel (props: Props) {
   const renderVideosSection = () => (
     !videos.length
       ? <NoVideosYet />
-      : <Section title={`Videos`}>
-          {videos.map((x) =>
-            <VideoPreview key={x.id.toString()} {...x} channel={channel} />
-          )}
-        </Section>
+      : <Section title={'Videos'}>
+        {videos.map((x) =>
+          <VideoPreview key={x.id.toString()} {...x} channel={channel} />
+        )}
+      </Section>
   );
-  
+
   return <div className='JoyViewChannel'>
     <ChannelHeader channel={channel} />
     {renderVideosSection()}
-  </div>
+  </div>;
 }

+ 7 - 7
pioneer/packages/joy-media/src/channels/YouHaveNoChannels.tsx

@@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
 import { Message } from 'semantic-ui-react';
 
 type Props = {
-  suspended?: boolean
+  suspended?: boolean;
 };
 
 export function YouHaveNoChannels (props: Props) {
@@ -18,10 +18,10 @@ export function YouHaveNoChannels (props: Props) {
       content='Please try again later'
       className='JoyInlineMsg'
     />
-  )
+  );
 
   const renderCreateButton = () => (
-    <Link to={`/media/channels/new`}>
+    <Link to={'/media/channels/new'}>
       <Message
         compact
         success
@@ -31,15 +31,15 @@ export function YouHaveNoChannels (props: Props) {
         className='JoyInlineMsg CreateBtn'
       />
     </Link>
-  )
+  );
 
   return <>
     <h2 style={{ marginTop: '2rem', marginBottom: '.5rem' }}>
       Build your following on Joystream
     </h2>
-    
+
     <p style={{ marginBottom: '2rem' }}>
-      A channel is a way to organize related content for the benefit 
+      A channel is a way to organize related content for the benefit
       of both the publisher and the audience.
     </p>
 
@@ -48,4 +48,4 @@ export function YouHaveNoChannels (props: Props) {
       : renderCreateButton()
     }
   </>;
-}
+}

+ 11 - 11
pioneer/packages/joy-media/src/common/BgImg.tsx

@@ -1,13 +1,13 @@
 import React, { CSSProperties } from 'react';
 
 type Props = {
-  url: string,
-  size?: number,
-  width?: number,
-  height?: number,
-  circle?: boolean,
-  className?: string,
-  style?: CSSProperties
+  url: string;
+  size?: number;
+  width?: number;
+  height?: number;
+  circle?: boolean;
+  className?: string;
+  style?: CSSProperties;
 };
 
 export function BgImg (props: Props) {
@@ -16,7 +16,7 @@ export function BgImg (props: Props) {
   const fullClass = 'JoyBgImg ' + className;
 
   let fullStyle: CSSProperties = {
-    backgroundImage: `url(${url})`,
+    backgroundImage: `url(${url})`
   };
 
   if (!width || !height) {
@@ -29,15 +29,15 @@ export function BgImg (props: Props) {
     height,
     minWidth: width,
     minHeight: height
-  })
+  });
 
   if (circle) {
     fullStyle = Object.assign(fullStyle, {
       borderRadius: '50%'
-    })
+    });
   }
 
   fullStyle = Object.assign(fullStyle, style);
 
   return <div className={fullClass} style={fullStyle} />;
-}
+}

+ 10 - 11
pioneer/packages/joy-media/src/common/FormTabs.tsx

@@ -4,15 +4,15 @@ import { FormikErrors } from 'formik';
 import { GenericMediaProp } from './MediaForms';
 
 type FormTab<FormValues> = {
-  id: string,
-  fields?: GenericMediaProp<FormValues>[],
-  renderTitle?: () => React.ReactNode,
-  render?: () => React.ReactNode,
+  id: string;
+  fields?: GenericMediaProp<FormValues>[];
+  renderTitle?: () => React.ReactNode;
+  render?: () => React.ReactNode;
 }
 
 type FormTabsProps<FormValues> = {
-  errors: FormikErrors<FormValues>,
-  panes: FormTab<FormValues>[],
+  errors: FormikErrors<FormValues>;
+  panes: FormTab<FormValues>[];
 }
 
 export function FormTabs <FormValues> (props: FormTabsProps<FormValues>) {
@@ -21,25 +21,24 @@ export function FormTabs <FormValues> (props: FormTabsProps<FormValues>) {
   return <Tab
     menu={{ secondary: true, pointing: true, color: 'blue' }}
     panes={panes.map(tab => {
-
       const {
         id,
         fields = [],
         renderTitle = () => id,
         render = () => null
       } = tab;
-      
+
       const tabErrors: any[] = [];
       fields.forEach(f => {
         const err = errors[f.id];
         if (err) {
           tabErrors.push(err);
         }
-      })
-    
+      });
+
       // Currently we don't show error counter because it's markup is broken:
       // a red circle with a number is shifted quite far from the right border of its tab.
-      const showErrorCounter = false
+      const showErrorCounter = false;
 
       const errCount = tabErrors.length;
       const errTooltip = 'Number of errors on this tab';

+ 3 - 4
pioneer/packages/joy-media/src/common/MediaDropdownOptions.tsx

@@ -5,13 +5,12 @@ import { TextValueEntity } from '@joystream/types/versioned-store/EntityCodec';
 import { InternalEntities } from '../transport';
 
 const buildOptions = (entities: TextValueEntity[]): DropdownItemProps[] =>
-  entities.map(x => ({ key: x.id, value: x.id, text: x.value }))
+  entities.map(x => ({ key: x.id, value: x.id, text: x.value }));
 
 const buildLanguageOptions = (entities: LanguageType[]): DropdownItemProps[] =>
-  entities.map(x => ({ key: x.id, value: x.id, text: ISO6391.getName(x.value) }))
+  entities.map(x => ({ key: x.id, value: x.id, text: ISO6391.getName(x.value) }));
 
 export class MediaDropdownOptions {
-
   public languageOptions: DropdownItemProps[]
   public contentLicenseOptions: DropdownItemProps[]
   public curationStatusOptions: DropdownItemProps[]
@@ -40,6 +39,6 @@ export class MediaDropdownOptions {
     musicMoods: [],
     musicThemes: [],
     publicationStatuses: [],
-    videoCategories: [],
+    videoCategories: []
   });
 }

+ 103 - 96
pioneer/packages/joy-media/src/common/MediaForms.tsx

@@ -6,31 +6,32 @@ import { SubmittableResult } from '@polkadot/api';
 import { TxFailedCallback, TxCallback } from '@polkadot/react-components/Status/types';
 import { MediaDropdownOptions } from './MediaDropdownOptions';
 import { OnTxButtonClick } from '@polkadot/joy-utils/TxButton';
-import isEqual from 'lodash/isEqual'
+import isEqual from 'lodash/isEqual';
+import { componentName } from '@polkadot/joy-utils/react/helpers';
 
 export const datePlaceholder = 'Date in format yyyy-mm-dd';
 
 export type FormCallbacks = {
-  onSubmit: OnTxButtonClick,
-  onTxSuccess: TxCallback,
-  onTxFailed: TxFailedCallback
+  onSubmit: OnTxButtonClick;
+  onTxSuccess: TxCallback;
+  onTxFailed: TxFailedCallback;
 };
 
 export type GenericMediaProp<FormValues> = {
-  id: keyof FormValues,
-  type: string,
-  name: string,
-  description?: string,
-  required?: boolean,
-  minItems?: number,
-  maxItems?: number,
-  minTextLength?: number,
-  maxTextLength?: number,
-  classId?: any
+  id: keyof FormValues;
+  type: string;
+  name: string;
+  description?: string;
+  required?: boolean;
+  minItems?: number;
+  maxItems?: number;
+  minTextLength?: number;
+  maxTextLength?: number;
+  classId?: any;
 };
 
 type BaseFieldProps<OuterProps, FormValues> = OuterProps & FormikProps<FormValues> & {
-  field: GenericMediaProp<FormValues>
+  field: GenericMediaProp<FormValues>;
 };
 
 type MediaTextProps<OuterProps, FormValues> =
@@ -39,21 +40,21 @@ type MediaTextProps<OuterProps, FormValues> =
 type MediaFieldProps<OuterProps, FormValues> =
   BaseFieldProps<OuterProps, FormValues> &
   JoyForms.LabelledProps<FormValues> & {
-    fieldProps: any
+    fieldProps: any;
   }
 
 type MediaDropdownProps<OuterProps, FormValues> =
   BaseFieldProps<OuterProps, FormValues> &
-{
-  options: DropdownItemProps[]
-};
+  {
+    options: DropdownItemProps[];
+  };
 
 type FormFields<OuterProps, FormValues> = {
-  LabelledText: React.FunctionComponent<JoyForms.LabelledProps<FormValues>>
-  LabelledField: React.FunctionComponent<JoyForms.LabelledProps<FormValues>>
-  MediaText: React.FunctionComponent<MediaTextProps<OuterProps, FormValues>>
-  MediaField: React.FunctionComponent<MediaFieldProps<OuterProps, FormValues>>
-  MediaDropdown: React.FunctionComponent<MediaDropdownProps<OuterProps, FormValues>>
+  LabelledText: React.FunctionComponent<JoyForms.LabelledProps<FormValues>>;
+  LabelledField: React.FunctionComponent<JoyForms.LabelledProps<FormValues>>;
+  MediaText: React.FunctionComponent<MediaTextProps<OuterProps, FormValues>>;
+  MediaField: React.FunctionComponent<MediaFieldProps<OuterProps, FormValues>>;
+  MediaDropdown: React.FunctionComponent<MediaDropdownProps<OuterProps, FormValues>>;
 };
 
 export type MediaFormProps<OuterProps, FormValues> =
@@ -61,21 +62,20 @@ export type MediaFormProps<OuterProps, FormValues> =
   FormikProps<FormValues> &
   FormFields<OuterProps, FormValues> &
   FormCallbacks & {
-    opts: MediaDropdownOptions
-    isFieldChanged: (field: keyof FormValues | GenericMediaProp<FormValues>) => boolean 
+    opts: MediaDropdownOptions;
+    isFieldChanged: (field: keyof FormValues | GenericMediaProp<FormValues>) => boolean;
   };
 
 export function withMediaForm<OuterProps, FormValues>
-  (Component: React.ComponentType<MediaFormProps<OuterProps, FormValues>>)
-{
+(Component: React.ComponentType<MediaFormProps<OuterProps, FormValues>>) {
   type FieldName = keyof FormValues
 
   type FieldObject = GenericMediaProp<FormValues>
 
   const LabelledText = JoyForms.LabelledText<FormValues>();
-  
+
   const LabelledField = JoyForms.LabelledField<FormValues>();
-  
+
   function MediaText (props: MediaTextProps<OuterProps, FormValues>) {
     const { field: f } = props;
     return !f ? null : <LabelledText name={f.id} label={f.name} tooltip={f.description} required={f.required} {...props} />;
@@ -87,7 +87,11 @@ export function withMediaForm<OuterProps, FormValues>
     const { id } = f;
 
     const allFieldProps = {
-      name: id, id, placeholder, className, style, 
+      name: id,
+      id,
+      placeholder,
+      className,
+      style,
       disabled: otherProps.isSubmitting,
       ...fieldProps
     };
@@ -97,7 +101,7 @@ export function withMediaForm<OuterProps, FormValues>
         <Field {...allFieldProps} />
       </LabelledField>
     );
-  }
+  };
 
   const MediaDropdown = (props: MediaDropdownProps<OuterProps, FormValues>) => {
     const { field: f, options = [] } = props;
@@ -116,70 +120,73 @@ export function withMediaForm<OuterProps, FormValues>
       onChange: (_event: any, data: DropdownProps) => {
         props.setFieldValue(id, data.value);
       }
-    }} />
-  }
-
-  return function (props: MediaFormProps<OuterProps, FormValues>) {
-    const {
-      initialValues,
-      values,
-      dirty,
-      touched,
-      errors,
-      isValid,
-      setSubmitting,
-      opts = MediaDropdownOptions.Empty,
-    } = props;
-
-    const isFieldChanged = (field: FieldName | FieldObject): boolean => {
-      const fieldName = typeof field === 'string' ? field : (field as FieldObject).id
-      return (
-        dirty &&
-        touched[fieldName] === true &&
-        !isEqual(values[fieldName], initialValues[fieldName])
-      );
-    };
-
-    const onSubmit = (sendTx: () => void) => {
-      if (isValid) {
-        sendTx();
-      } else {
-        console.log('Form is invalid. Errors:', errors)
-      }
-    };
-    
-    const onTxSuccess: TxCallback = (_txResult: SubmittableResult) => {
-      setSubmitting(false);
-    };
+    }} />;
+  };
 
-    const onTxFailed: TxFailedCallback = (txResult: SubmittableResult | null) => {
-      setSubmitting(false);
-      if (txResult === null) {
-        // Tx cancelled
-        return;
-      }
+  const ResultComponent: React.FunctionComponent<MediaFormProps<OuterProps, FormValues>> =
+    (props: MediaFormProps<OuterProps, FormValues>) => {
+      const {
+        initialValues,
+        values,
+        dirty,
+        touched,
+        errors,
+        isValid,
+        setSubmitting,
+        opts = MediaDropdownOptions.Empty
+      } = props;
+
+      const isFieldChanged = (field: FieldName | FieldObject): boolean => {
+        const fieldName = typeof field === 'string' ? field : (field as FieldObject).id;
+        return (
+          dirty &&
+          touched[fieldName] === true &&
+          !isEqual(values[fieldName], initialValues[fieldName])
+        );
+      };
+
+      const onSubmit = (sendTx: () => void) => {
+        if (isValid) {
+          sendTx();
+        } else {
+          console.log('Form is invalid. Errors:', errors);
+        }
+      };
+
+      const onTxSuccess: TxCallback = (_txResult: SubmittableResult) => {
+        setSubmitting(false);
+      };
+
+      const onTxFailed: TxFailedCallback = (txResult: SubmittableResult | null) => {
+        setSubmitting(false);
+        if (txResult === null) {
+          // Tx cancelled
+
+        }
+      };
+
+      const allProps = {
+        ...props,
+
+        // Callbacks:
+        onSubmit,
+        onTxSuccess,
+        onTxFailed,
+
+        // Components:
+        LabelledText,
+        LabelledField,
+        MediaText,
+        MediaField,
+        MediaDropdown,
+
+        // Other
+        opts,
+        isFieldChanged
+      };
+
+      return <Component {...allProps} />;
     };
-
-    const allProps = {
-      ...props,
-
-      // Callbacks:
-      onSubmit,
-      onTxSuccess,
-      onTxFailed,
-
-      // Components:
-      LabelledText,
-      LabelledField,
-      MediaText,
-      MediaField,
-      MediaDropdown,
-
-      // Other
-      opts,
-      isFieldChanged
-    }
-
-    return <Component {...allProps} />;
-  };
+  ResultComponent.displayName = `withMediaForm(${componentName(Component)})`;
+  return ResultComponent;
 }

+ 31 - 31
pioneer/packages/joy-media/src/common/MediaPlayerView.tsx

@@ -21,45 +21,45 @@ const PLAYER_COMMON_PARAMS = {
   lang: 'en',
   autoplay: true,
   theme: '#2185d0'
-}
+};
 
 // This is just a part of Player's methods that are used in this component.
 // To see all the methods available on APlayer and DPlayer visit the next URLs:
 // http://aplayer.js.org/#/home?id=api
 // http://dplayer.js.org/#/home?id=api
 interface PartOfPlayer {
-  pause: () => void
-  destroy: () => void
+  pause: () => void;
+  destroy: () => void;
 }
 
 export type RequiredMediaPlayerProps = {
-  channel: ChannelEntity
-  video: VideoType
-  contentId: ContentId
+  channel: ChannelEntity;
+  video: VideoType;
+  contentId: ContentId;
 }
 
 type ContentProps = {
-  contentType?: string
-  dataObjectOpt?: Option<DataObject>
-  resolvedAssetUrl?: string
+  contentType?: string;
+  dataObjectOpt?: Option<DataObject>;
+  resolvedAssetUrl?: string;
 }
 
 type MediaPlayerViewProps = ApiProps & I18nProps &
-  DiscoveryProviderProps & RequiredMediaPlayerProps & ContentProps
+DiscoveryProviderProps & RequiredMediaPlayerProps & ContentProps
 
 type PlayerProps = RequiredMediaPlayerProps & ContentProps
 
-function Player(props: PlayerProps) {
-  const { video, resolvedAssetUrl: url, contentType = 'video/video' } = props
-  const { thumbnail: cover } = video
-  const prefix = contentType.substring(0, contentType.indexOf('/'))
+function Player (props: PlayerProps) {
+  const { video, resolvedAssetUrl: url, contentType = 'video/video' } = props;
+  const { thumbnail: cover } = video;
+  const prefix = contentType.substring(0, contentType.indexOf('/'));
 
-  const [ player, setPlayer ] = useState<PartOfPlayer>()
+  const [player, setPlayer] = useState<PartOfPlayer>();
 
   const onPlayerCreated = (newPlayer: PartOfPlayer) => {
-    console.log('onPlayerCreated:', newPlayer)
-    setPlayer(newPlayer)
-  }
+    console.log('onPlayerCreated:', newPlayer);
+    setPlayer(newPlayer);
+  };
 
   const destroyPlayer = () => {
     if (!player) return;
@@ -67,14 +67,14 @@ function Player(props: PlayerProps) {
     console.log('Destroy the current player');
     player.pause();
     player.destroy();
-    setPlayer(undefined)
-  }
+    setPlayer(undefined);
+  };
 
   useEffect(() => {
     return () => {
-      destroyPlayer()
-    }
-  }, [ url ])
+      destroyPlayer();
+    };
+  }, [url]);
 
   if (prefix === 'video') {
     const video = { url, name, pic: cover };
@@ -94,22 +94,22 @@ function Player(props: PlayerProps) {
     />;
   }
 
-  return <JoyError title={`Unsupported type of content`}>{contentType}</JoyError>
+  return <JoyError title={'Unsupported type of content'}>{contentType}</JoyError>;
 }
 
-function InnerComponent(props: MediaPlayerViewProps) {
-  const { video, resolvedAssetUrl: url } = props
+function InnerComponent (props: MediaPlayerViewProps) {
+  const { video, resolvedAssetUrl: url } = props;
 
   const { dataObjectOpt, channel } = props;
-  if (!dataObjectOpt || dataObjectOpt.isNone ) {
+  if (!dataObjectOpt || dataObjectOpt.isNone) {
     return null;
   }
 
   // TODO extract and show the next info from dataObject:
   // {"owner":"5GSMNn8Sy8k64mGUWPDafjMZu9bQNX26GujbBQ1LeJpNbrfg","added_at":{"block":2781,"time":1582750854000},"type_id":1,"size":3664485,"liaison":"5HN528fspu4Jg3KXWm7Pu7aUK64RSBz2ZSbwo1XKR9iz3hdY","liaison_judgement":1,"ipfs_content_id":"QmNk4QczoJyPTAKdfoQna6KhAz3FwfjpKyRBXAZHG5djYZ"}
 
-  const { myAccountId } = useMyMembership()
-  const iAmOwner = isAccountAChannelOwner(channel, myAccountId)
+  const { myAccountId } = useMyMembership();
+  const iAmOwner = isAccountAChannelOwner(channel, myAccountId);
 
   return (
     <div className='PlayBox'>
@@ -138,6 +138,6 @@ export const MediaPlayerView = withMulti(
   translate,
   withCalls<MediaPlayerViewProps>(
     ['query.dataDirectory.dataObjectByContentId',
-      { paramName: 'contentId', propName: 'dataObjectOpt' } ]
+      { paramName: 'contentId', propName: 'dataObjectOpt' }]
   )
-)
+);

+ 32 - 33
pioneer/packages/joy-media/src/common/MediaPlayerWithResolver.tsx

@@ -17,44 +17,35 @@ import { JoyInfo } from '@polkadot/joy-utils/JoyStatus';
 
 type Props = ApiProps & I18nProps & DiscoveryProviderProps & RequiredMediaPlayerProps;
 
-function newCancelSource(): CancelTokenSource {
-  return axios.CancelToken.source()
+function newCancelSource (): CancelTokenSource {
+  return axios.CancelToken.source();
 }
 
-function InnerComponent(props: Props) {
-  const { contentId, api, discoveryProvider } = props
+function InnerComponent (props: Props) {
+  const { contentId, api, discoveryProvider } = props;
 
-  const [ error, setError ] = useState<Error>()
-  const [ resolvedAssetUrl, setResolvedAssetUrl ] = useState<string>()
-  const [ contentType, setContentType ] = useState<string>()
-  const [ cancelSource, setCancelSource ] = useState<CancelTokenSource>(newCancelSource())
-
-  useEffect(() => {
-
-    resolveAsset()
-
-    return () => {
-      cancelSource.cancel()
-    }
-  }, [ contentId.encode() ])
+  const [error, setError] = useState<Error>();
+  const [resolvedAssetUrl, setResolvedAssetUrl] = useState<string>();
+  const [contentType, setContentType] = useState<string>();
+  const [cancelSource, setCancelSource] = useState<CancelTokenSource>(newCancelSource());
 
   const resolveAsset = async () => {
-    setError(undefined)
-    setCancelSource(newCancelSource())
+    setError(undefined);
+    setCancelSource(newCancelSource());
 
     const rids: DataObjectStorageRelationshipId[] = await api.query.dataObjectStorageRegistry.relationshipsByContentId(contentId) as any;
 
     const allRelationships: Option<DataObjectStorageRelationship>[] = await Promise.all(rids.map((id) => api.query.dataObjectStorageRegistry.relationships(id))) as any;
 
     let readyProviders = allRelationships.filter(r => r.isSome).map(r => r.unwrap())
-        .filter(r => r.ready)
-        .map(r => r.storage_provider);
+      .filter(r => r.ready)
+      .map(r => r.storage_provider);
 
     // runtime doesn't currently guarantee unique set
     readyProviders = _.uniqBy(readyProviders, provider => provider.toString());
 
     if (!readyProviders.length) {
-      setError(new Error('No Storage Providers found storing this content'))
+      setError(new Error('No Storage Providers found storing this content'));
       return;
     }
 
@@ -75,7 +66,7 @@ function InnerComponent(props: Props) {
       const provider = readyProviders.shift();
       if (!provider) continue;
 
-      let assetUrl: string | undefined
+      let assetUrl: string | undefined;
       try {
         assetUrl = await discoveryProvider.resolveAssetEndpoint(provider, contentId.encode(), cancelSource.token);
       } catch (err) {
@@ -90,8 +81,8 @@ function InnerComponent(props: Props) {
         console.log('Check URL of resolved asset:', assetUrl);
         const response = await axios.head(assetUrl, { cancelToken: cancelSource.token });
 
-        setContentType(response.headers['content-type'] || 'video/video')
-        setResolvedAssetUrl(assetUrl)
+        setContentType(response.headers['content-type'] || 'video/video');
+        setResolvedAssetUrl(assetUrl);
 
         return;
       } catch (err) {
@@ -109,11 +100,19 @@ function InnerComponent(props: Props) {
       }
     }
 
-    setError(new Error('Unable to reach any provider serving this content'))
-  }
+    setError(new Error('Unable to reach any provider serving this content'));
+  };
+
+  useEffect(() => {
+    resolveAsset();
+
+    return () => {
+      cancelSource.cancel();
+    };
+  }, [contentId.encode()]);
 
-  console.log('Content id:', contentId.encode())
-  console.log('Resolved asset URL:', resolvedAssetUrl)
+  console.log('Content id:', contentId.encode());
+  console.log('Resolved asset URL:', resolvedAssetUrl);
 
   if (error) {
     return (
@@ -126,15 +125,15 @@ function InnerComponent(props: Props) {
   }
 
   if (!resolvedAssetUrl) {
-    return <JoyInfo title={`Please wait...`}>Resolving media content.</JoyInfo>
+    return <JoyInfo title={'Please wait...'}>Resolving media content.</JoyInfo>;
   }
 
-  const playerProps = { ...props, contentType, resolvedAssetUrl }
-  return <MediaPlayerView {...playerProps} />
+  const playerProps = { ...props, contentType, resolvedAssetUrl };
+  return <MediaPlayerView {...playerProps} />;
 }
 
 export const MediaPlayerWithResolver = withMulti(
   InnerComponent,
   translate,
   withDiscoveryProvider
-)
+);

+ 2 - 2
pioneer/packages/joy-media/src/common/NoContentYet.tsx

@@ -1,7 +1,7 @@
 import React from 'react';
 
 const NoContentYet: React.FunctionComponent = (props) => {
-  return <div className='NoContentYet'>{props.children}</div>
+  return <div className='NoContentYet'>{props.children}</div>;
 };
 
-export default NoContentYet;
+export default NoContentYet;

+ 17 - 17
pioneer/packages/joy-media/src/common/TypeHelpers.ts

@@ -1,6 +1,6 @@
-import BN from 'bn.js'
-import { ChannelId } from "@joystream/types/content-working-group"
-import { EntityId, ClassId } from "@joystream/types/versioned-store"
+import BN from 'bn.js';
+import { ChannelId } from '@joystream/types/content-working-group';
+import { EntityId, ClassId } from '@joystream/types/versioned-store';
 
 export type AnyChannelId = ChannelId | BN | number | string
 
@@ -8,36 +8,36 @@ export type AnyEntityId = EntityId | BN | number | string
 
 export type AnyClassId = ClassId | BN | number | string
 
-function canBeId(id: BN | number | string): boolean {
-  return id instanceof BN || typeof id === 'number' || typeof id === 'string'
+function canBeId (id: BN | number | string): boolean {
+  return id instanceof BN || typeof id === 'number' || typeof id === 'string';
 }
 
-export function asChannelId(id: AnyChannelId): ChannelId {
+export function asChannelId (id: AnyChannelId): ChannelId {
   if (id instanceof ChannelId) {
-    return id
+    return id;
   } else if (canBeId(id)) {
-    return new ChannelId(id)
+    return new ChannelId(id);
   } else {
-    throw new Error(`Not supported format for Channel id: ${id}`)
+    throw new Error(`Not supported format for Channel id: ${id}`);
   }
 }
 
-export function asEntityId(id: AnyEntityId): EntityId {
+export function asEntityId (id: AnyEntityId): EntityId {
   if (id instanceof EntityId) {
-    return id
+    return id;
   } else if (canBeId(id)) {
-    return new EntityId(id)
+    return new EntityId(id);
   } else {
-    throw new Error(`Not supported format for Entity id: ${id}`)
+    throw new Error(`Not supported format for Entity id: ${id}`);
   }
 }
 
-export function asClassId(id: AnyClassId): ClassId {
+export function asClassId (id: AnyClassId): ClassId {
   if (id instanceof ClassId) {
-    return id
+    return id;
   } else if (canBeId(id)) {
-    return new ClassId(id)
+    return new ClassId(id);
   } else {
-    throw new Error(`Not supported format for Class id: ${id}`)
+    throw new Error(`Not supported format for Class id: ${id}`);
   }
 }

+ 3 - 3
pioneer/packages/joy-media/src/entities/ChannelEntity.ts

@@ -5,6 +5,6 @@ import { ChannelType } from '../schemas/channel/Channel';
 export type ChannelEntity = ChannelType & {
 
   // Stats:
-  rewardEarned: BN,
-  contentItemsCount: number,
-};
+  rewardEarned: BN;
+  contentItemsCount: number;
+};

+ 7 - 7
pioneer/packages/joy-media/src/entities/EntityHelpers.ts

@@ -2,14 +2,14 @@ import moment from 'moment';
 import ISO6391 from 'iso-639-1';
 import { LanguageType } from '../schemas/general/Language';
 
-export function printExplicit(explicit?: boolean): string {
-  return explicit === true ? 'Yes' : 'No'
+export function printExplicit (explicit?: boolean): string {
+  return explicit === true ? 'Yes' : 'No';
 }
 
-export function printReleaseDate(linuxTimestamp?: number): string {
-  return !linuxTimestamp ? '' : moment(linuxTimestamp * 1000).format('YYYY-MM-DD')
+export function printReleaseDate (linuxTimestamp?: number): string {
+  return !linuxTimestamp ? '' : moment(linuxTimestamp * 1000).format('YYYY-MM-DD');
 }
 
-export function printLanguage(language?: LanguageType): string {
-  return !language ? '' : ISO6391.getName(language.value)
-}
+export function printLanguage (language?: LanguageType): string {
+  return !language ? '' : ISO6391.getName(language.value);
+}

+ 17 - 17
pioneer/packages/joy-media/src/entities/MusicAlbumEntity.ts

@@ -1,25 +1,25 @@
 export type MusicAlbumEntity = {
-  title: string,
-  artist: string,
-  thumbnail: string,
-  description: string,
+  title: string;
+  artist: string;
+  thumbnail: string;
+  description: string;
 
-  explicit: boolean,
-  license: string,
+  explicit: boolean;
+  license: string;
 
-  year: number,
-  month?: number,
-  date?: number,
+  year: number;
+  month?: number;
+  date?: number;
 
-  genre?: string,
-  mood?: string,
-  theme?: string,
+  genre?: string;
+  mood?: string;
+  theme?: string;
 
-  language?: string,
-  links?: string[],
-  lyrics?: string,
-  composer?: string,
-  reviews?: string,
+  language?: string;
+  links?: string[];
+  lyrics?: string;
+  composer?: string;
+  reviews?: string;
 
   // publicationStatus: ...
   // curationStatus: ...

+ 12 - 12
pioneer/packages/joy-media/src/entities/MusicTrackEntity.ts

@@ -1,18 +1,18 @@
 export type MusicTrackEntity = {
 
   // Basic:
-  title: string,
-  description?: string,
-  thumbnail?: string,
-  visibility?: string,
-  album?: string,
+  title: string;
+  description?: string;
+  thumbnail?: string;
+  visibility?: string;
+  album?: string;
 
   // Additional:
-  artist?: string,
-  composer?: string,
-  genre?: string,
-  mood?: string,
-  theme?: string,
-  explicit?: boolean,
-  license?: string,
+  artist?: string;
+  composer?: string;
+  genre?: string;
+  mood?: string;
+  theme?: string;
+  explicit?: boolean;
+  license?: string;
 };

+ 6 - 6
pioneer/packages/joy-media/src/explore/AllChannels.tsx

@@ -5,7 +5,7 @@ import { ChannelEntity } from '../entities/ChannelEntity';
 import { ChannelPreview } from '../channels/ChannelPreview';
 
 export type Props = {
-  channels?: ChannelEntity[]
+  channels?: ChannelEntity[];
 }
 
 export function AllChannels (props: Props) {
@@ -14,10 +14,10 @@ export function AllChannels (props: Props) {
   return channels.length === 0
     ? <em>No channels found</em>
     : <Section title={`All channels (${channels.length})`} className='ListOfChannels'>
-        {channels.map((x) =>
-          <ChannelPreview key={x.id} channel={x} withSubtitle={false} />
-        )}
-      </Section>
+      {channels.map((x) =>
+        <ChannelPreview key={x.id} channel={x} withSubtitle={false} />
+      )}
+    </Section>;
 }
 
 export const AllChannelsView = MediaView<Props>({
@@ -25,4 +25,4 @@ export const AllChannelsView = MediaView<Props>({
   resolveProps: async ({ transport }) => ({
     channels: await transport.allPublicVideoChannels()
   })
-})
+});

+ 6 - 6
pioneer/packages/joy-media/src/explore/AllVideos.tsx

@@ -4,7 +4,7 @@ import { VideoPreviewProps, VideoPreview } from '../video/VideoPreview';
 import { MediaView } from '../MediaView';
 
 export type Props = {
-  videos?: VideoPreviewProps[]
+  videos?: VideoPreviewProps[];
 }
 
 export function AllVideos (props: Props) {
@@ -13,10 +13,10 @@ export function AllVideos (props: Props) {
   return videos.length === 0
     ? <em>No videos found</em>
     : <Section title={`All videos (${videos.length})`} className='ListOfVideos'>
-        {videos.map((x) =>
-          <VideoPreview key={x.id} {...x} withChannel />
-        )}
-      </Section>
+      {videos.map((x) =>
+        <VideoPreview key={x.id} {...x} withChannel />
+      )}
+    </Section>;
 }
 
 export const AllVideosView = MediaView<Props>({
@@ -24,4 +24,4 @@ export const AllVideosView = MediaView<Props>({
   resolveProps: async ({ transport }) => ({
     videos: await transport.allPublicVideos()
   })
-})
+});

+ 10 - 10
pioneer/packages/joy-media/src/explore/ExploreContent.tsx

@@ -8,29 +8,29 @@ import { ChannelPreview } from '../channels/ChannelPreview';
 const LatestVideosTitle = () => (
   <div>
     Latest videos
-    <Link to={`/media/videos`} className='ViewAllLink'>All videos</Link>
+    <Link to={'/media/videos'} className='ViewAllLink'>All videos</Link>
   </div>
-)
+);
 
 const LatestChannelsTitle = () => (
   <div>
     Latest video channels
-    <Link to={`/media/channels`} className='ViewAllLink'>All channels</Link>
+    <Link to={'/media/channels'} className='ViewAllLink'>All channels</Link>
   </div>
-)
+);
 
 export type ExploreContentProps = {
-  featuredVideos?: VideoPreviewProps[]
-  latestVideos?: VideoPreviewProps[]
-  latestVideoChannels?: ChannelEntity[]
+  featuredVideos?: VideoPreviewProps[];
+  latestVideos?: VideoPreviewProps[];
+  latestVideoChannels?: ChannelEntity[];
 }
 
 export function ExploreContent (props: ExploreContentProps) {
-  const { featuredVideos = [], latestVideos = [], latestVideoChannels = [] } = props
+  const { featuredVideos = [], latestVideos = [], latestVideoChannels = [] } = props;
 
   return <div>
     {featuredVideos.length > 0 &&
-      <Section title={`Featured videos`} className='ListOfVideos'>
+      <Section title={'Featured videos'} className='ListOfVideos'>
         {featuredVideos.map((x) =>
           <VideoPreview key={x.id} {...x} withChannel />
         )}
@@ -50,5 +50,5 @@ export function ExploreContent (props: ExploreContentProps) {
         )}
       </Section>
     }
-  </div>
+  </div>;
 }

+ 3 - 3
pioneer/packages/joy-media/src/explore/ExploreContent.view.tsx

@@ -14,10 +14,10 @@ export const ExploreContentView = MediaView<ExploreContentProps>({
       transport.latestPublicVideoChannels(),
       transport.latestPublicVideos(),
       transport.featuredVideos()
-    ])
+    ]);
 
-    return { featuredVideos, latestVideos, latestVideoChannels }
+    return { featuredVideos, latestVideos, latestVideoChannels };
   }
 });
 
-export default ExploreContentView;
+export default ExploreContentView;

+ 17 - 17
pioneer/packages/joy-media/src/explore/PlayContent.tsx

@@ -7,22 +7,22 @@ import { ChannelEntity } from '../entities/ChannelEntity';
 import { ChannelPreview } from '../channels/ChannelPreview';
 
 type Props = {
-  channel: ChannelEntity,
-  tracks: MusicTrackReaderPreviewProps[],
-  currentTrackIndex?: number,
-  featuredAlbums?: MusicAlbumPreviewProps[],
+  channel: ChannelEntity;
+  tracks: MusicTrackReaderPreviewProps[];
+  currentTrackIndex?: number;
+  featuredAlbums?: MusicAlbumPreviewProps[];
 };
 
 // TODO get meta from track item
 const meta = {
   artist: 'Berlin Philharmonic',
-	composer: 'Wolfgang Amadeus Mozart',
-	genre: 'Classical Music',
-	mood: 'Relaxing',
-	theme: 'Dark',
-	explicit: false,
-	license: 'Public Domain'
-}
+  composer: 'Wolfgang Amadeus Mozart',
+  genre: 'Classical Music',
+  mood: 'Relaxing',
+  theme: 'Dark',
+  explicit: false,
+  license: 'Public Domain'
+};
 
 export function PlayContent (props: Props) {
   const { channel, tracks = [], currentTrackIndex = 0, featuredAlbums = [] } = props;
@@ -33,7 +33,7 @@ export function PlayContent (props: Props) {
     <Table.Row>
       <Table.Cell width={2}>{label}</Table.Cell>
       <Table.Cell>{value}</Table.Cell>
-    </Table.Row>
+    </Table.Row>;
 
   const metaTable = <>
     <h3>Track Info</h3>
@@ -48,7 +48,7 @@ export function PlayContent (props: Props) {
         {metaField('License', meta.license)}
       </Table.Body>
     </Table>
-  </>
+  </>;
 
   const albumTracks = (
     <div className='JoyPlayAlbum_AlbumTracks'>
@@ -57,10 +57,10 @@ export function PlayContent (props: Props) {
         <Table.Body>
           {tracks.map((x, i) => {
             const isCurrent = x.id === currentTrack.id;
-            const className = `TrackRow ` + (isCurrent ? 'Current' : '');
+            const className = 'TrackRow ' + (isCurrent ? 'Current' : '');
 
             return (
-              <Table.Row className={className} onClick={() => setCurrentTrack(x)}>
+              <Table.Row key={x.id} className={className} onClick={() => setCurrentTrack(x)}>
                 <Table.Cell className='TrackNumber' width={1}>{i + 1}</Table.Cell>
                 <Table.Cell className='TrackTitle'>{x.title}</Table.Cell>
               </Table.Row>
@@ -85,8 +85,8 @@ export function PlayContent (props: Props) {
     {featuredAlbums.length > 0 &&
       <div className='JoyPlayAlbum_RightSidePanel'>
         <h3>Featured albums</h3>
-        {featuredAlbums.map(x => <MusicAlbumPreview {...x} size={170} />)}
+        {featuredAlbums.map(x => <MusicAlbumPreview key={x.id} {...x} size={170} />)}
       </div>
     }
   </div>;
-}
+}

+ 2 - 2
pioneer/packages/joy-media/src/index.tsx

@@ -27,7 +27,7 @@ import { AllChannelsView } from './explore/AllChannels';
 
 type Props = AppProps & I18nProps & ApiProps & DiscoveryProviderProps & {};
 
-function App(props: Props) {
+function App (props: Props) {
   const { t, basePath } = props;
   const { state: { address: myAddress } } = useMyAccount();
 
@@ -44,7 +44,7 @@ function App(props: Props) {
     {
       name: 'channels/new',
       text: t('New channel')
-    },
+    }
     // !myAddress ? undefined : {
     //   name: `account/${myAddress}/videos`,
     //   text: t('My videos')

+ 2 - 2
pioneer/packages/joy-media/src/mocks/ContentLicense.mock.ts

@@ -5,10 +5,10 @@ const values = [
   'Public Domain',
   'Share Alike',
   'No Derivatives',
-  'No Commercial',
+  'No Commercial'
 ];
 
 export const AllContentLicenses: ContentLicenseType[] =
-  values.map(value => ({ id: newEntityId(), value })) as unknown as ContentLicenseType[] // A hack to fix TS compilation.
+  values.map(value => ({ id: newEntityId(), value })) as unknown as ContentLicenseType[]; // A hack to fix TS compilation.
 
 export const ContentLicense = AllContentLicenses[0];

+ 1 - 1
pioneer/packages/joy-media/src/mocks/CurationStatus.mock.ts

@@ -2,7 +2,7 @@ import { newEntityId } from './EntityId.mock';
 import { CurationStatusType } from '../schemas/general/CurationStatus';
 
 function newEntity (value: string): CurationStatusType {
-  return { id: newEntityId(), value } as unknown as CurationStatusType // A hack to fix TS compilation.
+  return { id: newEntityId(), value } as unknown as CurationStatusType; // A hack to fix TS compilation.
 }
 
 export const CurationStatus = {

+ 1 - 1
pioneer/packages/joy-media/src/mocks/FeaturedContent.mock.ts

@@ -7,4 +7,4 @@ export const FeaturedContent: FeaturedContentType = {
   topVideo: Video,
   featuredVideos: AllVideos,
   featuredAlbums: AllMusicAlbums
-} as unknown as FeaturedContentType // A hack to fix TS compilation.
+} as unknown as FeaturedContentType; // A hack to fix TS compilation.

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff