Gleb Urvanov hace 4 años
padre
commit
8288e51af7
Se han modificado 100 ficheros con 1367 adiciones y 1060 borrados
  1. 6 5
      .travis.yml
  2. 37 1
      Cargo.lock
  3. 2 2
      README.md
  4. 1 2
      devops/dockerfiles/node-and-runtime/Dockerfile
  5. 1 1
      devops/dockerfiles/rust-builder/Dockerfile
  6. 5 5
      devops/git-hooks/pre-push
  7. 3 4
      node/Cargo.toml
  8. 3 3
      node/README.md
  9. 17 1
      node/src/chain_spec/content_config.rs
  10. 5 1
      node/src/chain_spec/forum_config.rs
  11. 18 0
      node/src/chain_spec/initial_balances.rs
  12. 2 0
      node/src/chain_spec/initial_members.rs
  13. 19 11
      node/src/chain_spec/mod.rs
  14. 2 2
      node/src/chain_spec/proposals_config.rs
  15. 7 11
      package.json
  16. 0 9
      pioneer/.eslintignore
  17. 2 0
      pioneer/.eslintrc.js
  18. 39 73
      pioneer/.storybook/webpack.config.js
  19. 5 4
      pioneer/package.json
  20. 90 90
      pioneer/packages/apps-config/src/settings/endpoints.ts
  21. 18 0
      pioneer/packages/apps-routing/src/index.ts
  22. 17 0
      pioneer/packages/apps-routing/src/joy-election.ts
  23. 15 0
      pioneer/packages/apps-routing/src/joy-forum.ts
  24. 15 0
      pioneer/packages/apps-routing/src/joy-media.ts
  25. 16 0
      pioneer/packages/apps-routing/src/joy-proposals.ts
  26. 6 8
      pioneer/packages/apps-routing/src/joy-roles.ts
  27. 20 0
      pioneer/packages/apps-routing/src/memo.ts
  28. 2 0
      pioneer/packages/apps-routing/src/types.ts
  29. 0 0
      pioneer/packages/apps/public/images/default-thumbnail.png
  30. 1 0
      pioneer/packages/apps/public/locales/en/index.json
  31. 0 1
      pioneer/packages/apps/public/locales/en/joy-settings.json
  32. 0 1
      pioneer/packages/apps/public/locales/en/joy-utils-old.json
  33. 4 1
      pioneer/packages/apps/public/locales/en/joy-utils.json
  34. 1 1
      pioneer/packages/apps/src/Content/NotFound.tsx
  35. 24 6
      pioneer/packages/apps/src/Content/index.tsx
  36. 5 2
      pioneer/packages/apps/src/SideBar/Item.tsx
  37. 0 2
      pioneer/packages/apps/src/SideBar/index.tsx
  38. 14 7
      pioneer/packages/apps/src/initSettings.ts
  39. 14 0
      pioneer/packages/apps/webpack.base.config.js
  40. 0 0
      pioneer/packages/joy-election/.skip-build
  41. 3 3
      pioneer/packages/joy-election/package.json
  42. 9 6
      pioneer/packages/joy-election/src/Applicant.tsx
  43. 44 24
      pioneer/packages/joy-election/src/Applicants.tsx
  44. 19 21
      pioneer/packages/joy-election/src/ApplyForm.tsx
  45. 2 2
      pioneer/packages/joy-election/src/CandidatePreview.tsx
  46. 8 7
      pioneer/packages/joy-election/src/Council.tsx
  47. 83 53
      pioneer/packages/joy-election/src/Dashboard.tsx
  48. 21 18
      pioneer/packages/joy-election/src/Reveals.tsx
  49. 15 6
      pioneer/packages/joy-election/src/SealedVote.tsx
  50. 25 16
      pioneer/packages/joy-election/src/SealedVotes.tsx
  51. 35 0
      pioneer/packages/joy-election/src/SidebarSubtitle.tsx
  52. 36 32
      pioneer/packages/joy-election/src/VoteForm.tsx
  53. 30 8
      pioneer/packages/joy-election/src/Votes.tsx
  54. 0 28
      pioneer/packages/joy-election/src/index.css
  55. 17 10
      pioneer/packages/joy-election/src/index.tsx
  56. 10 6
      pioneer/packages/joy-election/src/myVotesStore.ts
  57. 21 0
      pioneer/packages/joy-election/src/style.ts
  58. 9 3
      pioneer/packages/joy-election/src/utils.tsx
  59. 0 0
      pioneer/packages/joy-forum/.skip-build
  60. 3 3
      pioneer/packages/joy-forum/package.json
  61. 33 32
      pioneer/packages/joy-forum/src/CategoryList.tsx
  62. 35 22
      pioneer/packages/joy-forum/src/Context.tsx
  63. 24 17
      pioneer/packages/joy-forum/src/EditCategory.tsx
  64. 17 15
      pioneer/packages/joy-forum/src/EditReply.tsx
  65. 24 16
      pioneer/packages/joy-forum/src/EditThread.tsx
  66. 20 13
      pioneer/packages/joy-forum/src/ForumRoot.tsx
  67. 13 180
      pioneer/packages/joy-forum/src/ForumSudo.tsx
  68. 3 0
      pioneer/packages/joy-forum/src/LegacyPagingRedirect.tsx
  69. 10 12
      pioneer/packages/joy-forum/src/Moderate.tsx
  70. 14 11
      pioneer/packages/joy-forum/src/ViewReply.tsx
  71. 69 74
      pioneer/packages/joy-forum/src/ViewThread.tsx
  72. 18 15
      pioneer/packages/joy-forum/src/calls.tsx
  73. 10 13
      pioneer/packages/joy-forum/src/index.tsx
  74. 12 8
      pioneer/packages/joy-forum/src/style.ts
  75. 14 7
      pioneer/packages/joy-forum/src/utils.tsx
  76. 6 2
      pioneer/packages/joy-forum/src/validation.tsx
  77. 0 0
      pioneer/packages/joy-media/.skip-build
  78. 2 0
      pioneer/packages/joy-media/aplayer.d.ts
  79. 2 0
      pioneer/packages/joy-media/dplayer.d.ts
  80. 4 4
      pioneer/packages/joy-media/package.json
  81. 33 7
      pioneer/packages/joy-media/src/DiscoveryProvider.tsx
  82. 4 3
      pioneer/packages/joy-media/src/IterableFile.ts
  83. 15 10
      pioneer/packages/joy-media/src/MediaView.tsx
  84. 2 2
      pioneer/packages/joy-media/src/TransportContext.tsx
  85. 73 30
      pioneer/packages/joy-media/src/Upload.tsx
  86. 1 1
      pioneer/packages/joy-media/src/channels/ChannelAvatar.tsx
  87. 1 0
      pioneer/packages/joy-media/src/channels/ChannelAvatarAndName.tsx
  88. 1 1
      pioneer/packages/joy-media/src/channels/ChannelHelpers.ts
  89. 1 0
      pioneer/packages/joy-media/src/channels/ChannelNameAsLink.tsx
  90. 4 4
      pioneer/packages/joy-media/src/channels/ChannelPreview.tsx
  91. 1 0
      pioneer/packages/joy-media/src/channels/ChannelPreviewStats.tsx
  92. 3 1
      pioneer/packages/joy-media/src/channels/ChannelsByOwner.tsx
  93. 6 4
      pioneer/packages/joy-media/src/channels/ChannelsByOwner.view.tsx
  94. 23 17
      pioneer/packages/joy-media/src/channels/CurationPanel.tsx
  95. 25 26
      pioneer/packages/joy-media/src/channels/EditChannel.tsx
  96. 6 4
      pioneer/packages/joy-media/src/channels/EditChannel.view.tsx
  97. 2 1
      pioneer/packages/joy-media/src/channels/ViewChannel.tsx
  98. 6 4
      pioneer/packages/joy-media/src/channels/ViewChannel.view.tsx
  99. 3 3
      pioneer/packages/joy-media/src/channels/ViewMusicChannel.tsx
  100. 1 1
      pioneer/packages/joy-media/src/channels/ViewVideoChannel.tsx

+ 6 - 5
.travis.yml

@@ -24,7 +24,7 @@ before_install:
     fi
 
 install:
-  - rustup install nightly-2020-05-23
+  - rustup install nightly-2020-05-23 --force
   - rustup target add wasm32-unknown-unknown --toolchain nightly-2020-05-23
   # travis installs rust using rustup with the "minimal" profile so these tools are not installed by default
   - rustup component add rustfmt
@@ -34,9 +34,10 @@ before_script:
   - cargo fmt --all -- --check
 
 script:
-  # we set release as build type for all steps to benefit from already compiled packages in prior steps
-  - BUILD_DUMMY_WASM_BINARY=1 cargo clippy --release -- -D warnings
-  - BUILD_DUMMY_WASM_BINARY=1 cargo test --release --verbose --all
-  - TRIGGER_WASM_BUILD=1 WASM_BUILD_TOOLCHAIN=nightly-2020-05-23 cargo build --release -p joystream-node
+  - export WASM_BUILD_TOOLCHAIN=nightly-2020-05-23
+  - BUILD_DUMMY_WASM_BINARY=1 cargo clippy --release --all -- -D warnings
+  - travis_wait 50 cargo test --release --verbose --all
+  - cargo build --release
   - ls -l ./target/release/wbuild/joystream-node-runtime/
   - ./target/release/joystream-node --version
+  - ./target/release/chain-spec-builder --version

+ 37 - 1
Cargo.lock

@@ -565,6 +565,7 @@ name = "chain-spec-builder"
 version = "3.0.0"
 dependencies = [
  "ansi_term 0.12.1",
+ "enum-utils",
  "joystream-node",
  "rand 0.7.3",
  "sc-chain-spec",
@@ -927,6 +928,30 @@ dependencies = [
  "syn 0.11.11",
 ]
 
+[[package]]
+name = "enum-utils"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed327f716d0d351d86c9fd3398d20ee39ad8f681873cc081da2ca1c10fed398a"
+dependencies = [
+ "enum-utils-from-str",
+ "failure",
+ "proc-macro2",
+ "quote 1.0.7",
+ "serde_derive_internals",
+ "syn 1.0.17",
+]
+
+[[package]]
+name = "enum-utils-from-str"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d49be08bad6e4ca87b2b8e74146987d4e5cb3b7512efa50ef505b51a22227ee1"
+dependencies = [
+ "proc-macro2",
+ "quote 1.0.7",
+]
+
 [[package]]
 name = "env_logger"
 version = "0.7.1"
@@ -1968,7 +1993,7 @@ dependencies = [
 
 [[package]]
 name = "joystream-node"
-version = "3.0.0"
+version = "3.0.1"
 dependencies = [
  "frame-benchmarking",
  "frame-benchmarking-cli",
@@ -5823,6 +5848,17 @@ dependencies = [
  "syn 1.0.17",
 ]
 
+[[package]]
+name = "serde_derive_internals"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dbab34ca63057a1f15280bdf3c39f2b1eb1b54c17e98360e511637aef7418c6"
+dependencies = [
+ "proc-macro2",
+ "quote 1.0.7",
+ "syn 1.0.17",
+]
+
 [[package]]
 name = "serde_json"
 version = "1.0.57"

+ 2 - 2
README.md

@@ -93,8 +93,8 @@ You can also run your our own joystream-node:
 
 ```sh
 git checkout master
-cargo build --release
-cargo run --release -- --pruning archive --chain testnets/rome.json
+WASM_BUILD_TOOLCHAIN=nightly-2020-05-23 cargo build --release
+./target/release/joystream-node -- --pruning archive --chain testnets/rome.json
 ```
 
 Wait for the node to sync to the latest block, then change pioneer settings "remote node" option to "Local Node", or follow the link below:

+ 1 - 2
devops/dockerfiles/node-and-runtime/Dockerfile

@@ -4,8 +4,7 @@ WORKDIR /joystream
 COPY . /joystream
 
 # Build joystream-node and its dependencies - runtime
-RUN rustup override set 1.45.2
-RUN cargo build --release -p joystream-node
+RUN WASM_BUILD_TOOLCHAIN=nightly-2020-05-23 cargo build --release -p joystream-node
 
 FROM debian:stretch
 LABEL description="Joystream node"

+ 1 - 1
devops/dockerfiles/rust-builder/Dockerfile

@@ -1,4 +1,4 @@
-FROM liuchong/rustup:nightly AS builder
+FROM liuchong/rustup:1.46.0 AS builder
 LABEL description="Rust and WASM build environment for joystream and substrate"
 
 WORKDIR /setup

+ 5 - 5
devops/git-hooks/pre-push

@@ -1,10 +1,10 @@
 #!/bin/sh
 set -e
 
-echo '+cargo test --release --all'
-BUILD_DUMMY_WASM_BINARY=1 cargo test --all
-
-echo '+cargo clippy --release --all -- -D warnings'
-BUILD_DUMMY_WASM_BINARY=1 cargo clippy --all -- -D warnings
+export WASM_BUILD_TOOLCHAIN=nightly-2020-05-23
 
+echo '+cargo clippy --all -- -D warnings'
+BUILD_DUMMY_WASM_BINARY=1 cargo clippy --release --all -- -D warnings
 
+echo '+cargo test --all'
+cargo test --release --all

+ 3 - 4
node/Cargo.toml

@@ -3,7 +3,7 @@ authors = ['Joystream contributors']
 build = 'build.rs'
 edition = '2018'
 name = 'joystream-node'
-version = '3.0.0'
+version = '3.0.1'
 default-run = "joystream-node"
 
 [[bin]]
@@ -20,6 +20,8 @@ futures = { version = "0.3.1", features = ["compat"] }
 jsonrpc-core = "14.2.0"
 structopt = { version = "0.3.8", optional = true}
 serde_json = '1.0'
+codec = { package = "parity-scale-codec", version = "1.3.1" }
+hex = { package = "hex", version = "0.4.2" }
 
 # primitives
 sp-authority-discovery = { package = 'sp-authority-discovery', git = 'https://github.com/paritytech/substrate.git', rev = '00768a1f21a579c478fe5d4f51e1fa71f7db9fd4' }
@@ -72,9 +74,6 @@ wasm-bindgen = { version = "0.2.57", optional = true }
 wasm-bindgen-futures = { version = "0.4.7", optional = true }
 browser-utils = { package = 'substrate-browser-utils', git = 'https://github.com/paritytech/substrate.git', rev = '00768a1f21a579c478fe5d4f51e1fa71f7db9fd4', optional = true}
 
-codec = { package = "parity-scale-codec", version = "1.3.1" }
-hex = { package = "hex", version = "0.4.2" }
-
 [dev-dependencies]
 tempfile = "3.1.0"
 sp-timestamp = { package = 'sp-timestamp', default-features = false, git = 'https://github.com/paritytech/substrate.git', rev = '00768a1f21a579c478fe5d4f51e1fa71f7db9fd4' }

+ 3 - 3
node/README.md

@@ -26,7 +26,7 @@ cd joystream/
 Compile the node and runtime:
 
 ```bash
-cargo build --release
+WASM_BUILD_TOOLCHAIN=nightly-2020-05-23 cargo build --release
 ```
 
 This produces the binary in `./target/release/joystream-node`
@@ -34,7 +34,7 @@ This produces the binary in `./target/release/joystream-node`
 ### Running local development chain
 
 ```bash
-cargo run --release -- --dev
+./target/release/joystream-node --dev
 ```
 
 If you repeatedly need to restart a new chain,
@@ -49,7 +49,7 @@ this script will build and run a fresh new local development chain (purging exis
 Use the `--chain` argument, and specify the path to the genesis `chain.json` file for that public network. The JSON "chain spec" files for Joystream public networks can be found in [../testnets/](../testnets/).
 
 ```bash
-cargo run --release -- --chain testnets/rome.json
+./target/release/joystream-node --chain testnets/rome.json
 ```
 
 ### Tests and code quality

+ 17 - 1
node/src/chain_spec/content_config.rs

@@ -184,6 +184,9 @@ impl EncodedContentData {
     }
 }
 
+/// Generates a VersionedStoreConfig genesis config
+/// with pre-populated classes and entities parsed from a json file serialized
+/// as a ContentData struct.
 pub fn versioned_store_config_from_json(data_file: &Path) -> VersionedStoreConfig {
     let content = parse_content_data(data_file).decode();
     let base_config = empty_versioned_store_config();
@@ -201,7 +204,6 @@ pub fn versioned_store_config_from_json(data_file: &Path) -> VersionedStoreConfi
         .map_or(first_id, |entity_and_maintainer| {
             entity_and_maintainer.entity.id + 1
         });
-    assert_eq!(next_entity_id, (content.entities.len() + 1) as EntityId);
 
     VersionedStoreConfig {
         class_by_id: content
@@ -227,6 +229,7 @@ pub fn versioned_store_config_from_json(data_file: &Path) -> VersionedStoreConfi
     }
 }
 
+/// Generates basic empty VersionedStoreConfig genesis config
 pub fn empty_versioned_store_config() -> VersionedStoreConfig {
     VersionedStoreConfig {
         class_by_id: vec![],
@@ -240,6 +243,7 @@ pub fn empty_versioned_store_config() -> VersionedStoreConfig {
     }
 }
 
+/// Generates a basic empty VersionedStorePermissionsConfig genesis config
 pub fn empty_versioned_store_permissions_config() -> VersionedStorePermissionsConfig {
     VersionedStorePermissionsConfig {
         class_permissions_by_class_id: vec![],
@@ -247,6 +251,9 @@ pub fn empty_versioned_store_permissions_config() -> VersionedStorePermissionsCo
     }
 }
 
+/// Generates a `VersionedStorePermissionsConfig` genesis config
+/// pre-populated with permissions and entity maintainers parsed from
+/// a json file serialized as a `ContentData` struct.
 pub fn versioned_store_permissions_config_from_json(
     data_file: &Path,
 ) -> VersionedStorePermissionsConfig {
@@ -270,6 +277,7 @@ pub fn versioned_store_permissions_config_from_json(
     }
 }
 
+/// Generates a basic empty `DataDirectoryConfig` genesis config
 pub fn empty_data_directory_config() -> DataDirectoryConfig {
     DataDirectoryConfig {
         data_object_by_content_id: vec![],
@@ -277,6 +285,9 @@ pub fn empty_data_directory_config() -> DataDirectoryConfig {
     }
 }
 
+/// Generates a `DataDirectoryConfig` genesis config
+/// pre-populated with data objects and known content ids parsed from
+/// a json file serialized as a `ContentData` struct
 pub fn data_directory_config_from_json(data_file: &Path) -> DataDirectoryConfig {
     let content = parse_content_data(data_file).decode();
 
@@ -294,6 +305,8 @@ pub fn data_directory_config_from_json(data_file: &Path) -> DataDirectoryConfig
     }
 }
 
+/// Generates a basic `ContentWorkingGroupConfig` genesis config without any active curators
+/// curator lead or channels.
 pub fn empty_content_working_group_config() -> ContentWorkingGroupConfig {
     ContentWorkingGroupConfig {
         mint_capacity: 100_000,
@@ -321,6 +334,9 @@ pub fn empty_content_working_group_config() -> ContentWorkingGroupConfig {
     }
 }
 
+/// Generates a `ContentWorkingGroupConfig` genesis config
+/// pre-populated with channels and corresponding princial channel owners
+/// parsed from a json file serialized as a `ContentData` struct
 pub fn content_working_group_config_from_json(data_file: &Path) -> ContentWorkingGroupConfig {
     let content = parse_content_data(data_file).decode();
     let first_channel_id = 1;

+ 5 - 1
node/src/chain_spec/forum_config.rs

@@ -67,11 +67,15 @@ fn parse_forum_json(data_file: &Path) -> EncodedForumData {
     serde_json::from_str(&data).expect("failed parsing members data")
 }
 
+/// Generates a `ForumConfig` geneis config pre-populated with
+/// categories, threads and posts parsed
+/// from a json file serialized as `EncodedForumData`
 pub fn from_json(forum_sudo: AccountId, data_file: &Path) -> ForumConfig {
     let forum_data = parse_forum_json(data_file);
     create(forum_sudo, forum_data)
 }
 
+/// Generates a basic empty `ForumConfig` geneis config
 pub fn empty(forum_sudo: AccountId) -> ForumConfig {
     let forum_data = EncodedForumData {
         categories: vec![],
@@ -138,7 +142,7 @@ fn create(forum_sudo: AccountId, forum_data: EncodedForumData) -> ForumConfig {
         category_title_constraint: new_validation(10, 90),
         category_description_constraint: new_validation(10, 490),
         thread_title_constraint: new_validation(10, 90),
-        post_text_constraint: new_validation(10, 990),
+        post_text_constraint: new_validation(10, 2990),
         thread_moderation_rationale_constraint: new_validation(10, 290),
         post_moderation_rationale_constraint: new_validation(10, 290),
     }

+ 18 - 0
node/src/chain_spec/initial_balances.rs

@@ -0,0 +1,18 @@
+use node_runtime::{AccountId, Balance};
+use serde::Deserialize;
+use std::{fs, path::Path};
+
+#[derive(Deserialize)]
+struct SerializedInitialBalances {
+    balances: Vec<(AccountId, Balance)>,
+}
+
+fn parse_json(data_file: &Path) -> SerializedInitialBalances {
+    let data = fs::read_to_string(data_file).expect("Failed reading file");
+    serde_json::from_str(&data).expect("failed parsing balances data")
+}
+
+/// Deserializes initial balances from json file
+pub fn from_json(data_file: &Path) -> Vec<(AccountId, Balance)> {
+    parse_json(data_file).balances
+}

+ 2 - 0
node/src/chain_spec/initial_members.rs

@@ -1,11 +1,13 @@
 use node_runtime::{membership, AccountId, Moment};
 use std::{fs, path::Path};
 
+/// Generates a Vec of genesis members parsed from a json file
 pub fn from_json(data_file: &Path) -> Vec<membership::genesis::Member<u64, AccountId, Moment>> {
     let data = fs::read_to_string(data_file).expect("Failed reading file");
     serde_json::from_str(&data).expect("failed parsing members data")
 }
 
+/// Generates an empty Vec of genesis members
 pub fn none() -> Vec<membership::genesis::Member<u64, AccountId, Moment>> {
     vec![]
 }

+ 19 - 11
node/src/chain_spec/mod.rs

@@ -42,6 +42,7 @@ pub use node_runtime::{AccountId, GenesisConfig};
 
 pub mod content_config;
 pub mod forum_config;
+pub mod initial_balances;
 pub mod initial_members;
 pub mod proposals_config;
 
@@ -138,6 +139,7 @@ impl Alternative {
                         content_config::empty_versioned_store_permissions_config(),
                         content_config::empty_data_directory_config(),
                         content_config::empty_content_working_group_config(),
+                        vec![],
                     )
                 },
                 Vec::new(),
@@ -178,6 +180,7 @@ impl Alternative {
                         content_config::empty_versioned_store_permissions_config(),
                         content_config::empty_data_directory_config(),
                         content_config::empty_content_working_group_config(),
+                        vec![],
                     )
                 },
                 Vec::new(),
@@ -202,7 +205,8 @@ pub fn chain_spec_properties() -> json::map::Map<String, json::Value> {
     );
     properties
 }
-
+// This method should be refactored after Alexandria to reduce number of arguments
+// as more args will likely be needed
 #[allow(clippy::too_many_arguments)]
 pub fn testnet_genesis(
     initial_authorities: Vec<(
@@ -222,11 +226,10 @@ pub fn testnet_genesis(
     versioned_store_permissions_config: VersionedStorePermissionsConfig,
     data_directory_config: DataDirectoryConfig,
     content_working_group_config: ContentWorkingGroupConfig,
+    initial_balances: Vec<(AccountId, Balance)>,
 ) -> GenesisConfig {
-    const CENTS: Balance = 1;
-    const DOLLARS: Balance = 100 * CENTS;
-    const STASH: Balance = 20 * DOLLARS;
-    const ENDOWMENT: Balance = 100_000 * DOLLARS;
+    const STASH: Balance = 5_000;
+    const ENDOWMENT: Balance = 100_000_000;
 
     let default_text_constraint = node_runtime::working_group::default_text_constraint();
 
@@ -241,11 +244,16 @@ pub fn testnet_genesis(
                 .cloned()
                 .map(|k| (k, ENDOWMENT))
                 .chain(initial_authorities.iter().map(|x| (x.0.clone(), STASH)))
+                .chain(
+                    initial_balances
+                        .iter()
+                        .map(|(account, balance)| (account.clone(), *balance)),
+                )
                 .collect(),
         }),
         pallet_staking: Some(StakingConfig {
             validator_count: 20,
-            minimum_validator_count: 1,
+            minimum_validator_count: initial_authorities.len() as u32,
             stakers: initial_authorities
                 .iter()
                 .map(|x| (x.0.clone(), x.1.clone(), STASH, StakerStatus::Validator))
@@ -282,14 +290,14 @@ pub fn testnet_genesis(
         election: Some(CouncilElectionConfig {
             auto_start: true,
             election_parameters: ElectionParameters {
-                announcing_period: 3 * DAYS,
+                announcing_period: 2 * DAYS,
                 voting_period: 1 * DAYS,
                 revealing_period: 1 * DAYS,
-                council_size: 12,
+                council_size: 6,
                 candidacy_limit: 25,
-                min_council_stake: 10 * DOLLARS,
-                new_term_duration: 14 * DAYS,
-                min_voting_stake: 1 * DOLLARS,
+                min_council_stake: 1_000,
+                new_term_duration: 10 * DAYS,
+                min_voting_stake: 100,
             },
         }),
         membership: Some(MembersConfig {

+ 2 - 2
node/src/chain_spec/proposals_config.rs

@@ -8,10 +8,10 @@ pub fn development() -> ProposalsConfigParameters {
 
 /// Staging chain config. Shorter grace periods and voting periods than default.
 pub fn staging() -> ProposalsConfigParameters {
-    ProposalsConfigParameters::with_grace_and_voting_periods(200, 600)
+    ProposalsConfigParameters::with_grace_and_voting_periods(20, 30)
 }
 
 /// The default configuration as defined in the runtime module
-pub fn default() -> ProposalsConfigParameters {
+pub fn production() -> ProposalsConfigParameters {
     ProposalsConfigParameters::default()
 }

+ 7 - 11
package.json

@@ -20,24 +20,20 @@
     "devops/eslint-config",
     "devops/prettier-config",
     "pioneer",
-    "pioneer/packages/apps*",
-    "pioneer/packages/page*",
-    "pioneer/packages/react*",
-    "pioneer/packages/joy-utils",
-    "pioneer/packages/joy-members",
-    "pioneer/packages/joy-pages",
+    "pioneer/packages/*",
     "utils/api-examples"
   ],
   "resolutions": {
     "@polkadot/api": "1.26.1",
     "@polkadot/api-contract": "1.26.1",
-    "@polkadot/keyring": "3.0.1",
+    "@polkadot/keyring": "^3.0.1",
     "@polkadot/types": "1.26.1",
-    "@polkadot/util": "3.0.1",
-    "@polkadot/util-crypto": "3.0.1",
-    "@polkadot/wasm-crypto": "1.2.1",
+    "@polkadot/util": "^3.0.1",
+    "@polkadot/util-crypto": "^3.0.1",
+    "@polkadot/wasm-crypto": "^1.2.1",
     "babel-core": "^7.0.0-bridge.0",
-    "typescript": "^3.9.7"
+    "typescript": "^3.9.7",
+    "bn.js": "^5.1.2"
   },
   "devDependencies": {
     "husky": "^4.2.5",

+ 0 - 9
pioneer/.eslintignore

@@ -1,14 +1,5 @@
 **/build/*
 **/coverage/*
 **/node_modules/*
-packages/old-apps/*
-packages/joy-election/*
-packages/joy-forum/*
-packages/joy-help/*
-packages/joy-media/*
-packages/joy-proposals/*
-packages/joy-roles/*
-packages/joy-settings/*
-packages/joy-utils-old/*
 .eslintrc.js
 i18next-scanner.config.js

+ 2 - 0
pioneer/.eslintrc.js

@@ -27,6 +27,8 @@ module.exports = {
     'react/jsx-max-props-per-line': 'off',
     'sort-destructure-keys/sort-destructure-keys': 'off',
     '@typescript-eslint/unbound-method': 'warn', // Doesn't work well with our version of Formik, see: https://github.com/formium/formik/issues/2589
+    'react-hooks/exhaustive-deps': 'warn', // Causes more issues than it solves currently
+    'no-void': 'off' // Otherwise we cannot mark unhandles promises
   },
   // isolate pioneer from monorepo eslint rules
   root: true

+ 39 - 73
pioneer/.storybook/webpack.config.js

@@ -1,81 +1,47 @@
 const path = require('path')
 const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
 module.exports = ({ config }) => {
+  // Styles (replace the provided rule):
+  const originalCssRuleIndex = config.module.rules.findIndex(rule => rule.test.source.includes('.css'));
+  config.module.rules[originalCssRuleIndex] = {
+    test: /\.(sa|sc|c)ss$/i,
+    use: [
+      // Creates `style` nodes from JS strings
+      'style-loader',
+      // Translates CSS into CommonJS
+      'css-loader',
+      // Compiles Sass to CSS
+      'sass-loader'
+    ]
+  };
 
-// Post CSS loader for sources:
-config.module.rules.push({
-  test: /\.css$/,
-  include: path.resolve(__dirname, '../packages'),
-  exclude: /(node_modules)/,
-  use: [
-    {
-      loader: require.resolve('postcss-loader'),
-      options: {
-        // Set postcss.config.js config path && ctx
-        config: {
-          path: '../postcss.config.js',
-        },
-        ident: 'postcss',
-        plugins: () => [
-          require('precss'),
-          require('autoprefixer'),
-          require('postcss-simple-vars'),
-          require('postcss-nested'),
-          require('postcss-import'),
-          require('postcss-clean')(),
-          require('postcss-flexbugs-fixes')
-        ]
-      }
-    }
-  ]
-});
+  // TypeScript loader (via Babel to match polkadot/apps)
+  config.module.rules.push({
+    test: /\.(js|ts|tsx)$/,
+    exclude: /(node_modules)/,
+    use: [
+      {
+        loader: require.resolve('babel-loader'),
+        options: require('@polkadot/dev/config/babel')
+      },
+    ],
+  });
+  config.resolve.extensions.push('.js', '.ts', '.tsx');
 
-// TypeScript loader (via Babel to match polkadot/apps)
-config.module.rules.push({
-  test: /\.(js|ts|tsx)$/,
-  exclude: /(node_modules)/,
-  use: [
-    {
-      loader: require.resolve('babel-loader'),
-      options: require('@polkadot/dev-react/config/babel')
-    },
-  ],
-});
-config.resolve.extensions.push('.js', '.ts', '.tsx');
+  // TSConfig, uses the same file as packages
+  config.resolve.plugins = config.resolve.plugins || [];
+  config.resolve.plugins.push(
+    new TsconfigPathsPlugin({
+      configFile: path.resolve(__dirname, '../tsconfig.json'),
+    })
+  );
 
-// TSConfig, uses the same file as packages
-config.resolve.plugins = config.resolve.plugins || [];
-config.resolve.plugins.push(
-  new TsconfigPathsPlugin({
-    configFile: path.resolve(__dirname, '../tsconfig.json'),
-  })
-);
+  // Stories parser
+  config.module.rules.push({
+      test: /\.stories\.tsx?$/,
+      loaders: [require.resolve('@storybook/source-loader')],
+      enforce: 'pre',
+  });
 
-// Stories parser
-config.module.rules.push({
-    test: /\.stories\.tsx?$/,
-    loaders: [require.resolve('@storybook/source-loader')],
-    enforce: 'pre',
-});
-
-// CSS preprocessors
-config.module.rules.push(
-    {
-        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' ]
-    }
-);
-
-return config;
+  return config;
 };

+ 5 - 4
pioneer/package.json

@@ -25,7 +25,6 @@
     "test": "echo \"skipping tests\"",
     "vanitygen": "node packages/app-accounts/scripts/vanitygen.js",
     "start": "yarn clean && cd packages/apps && webpack --config webpack.config.js",
-    "generate-schemas": "json2ts -i packages/joy-types/src/schemas/role.schema.json -o packages/joy-types/src/schemas/role.schema.ts",
     "build-storybook": "build-storybook -c .storybook",
     "storybook": "start-storybook -s ./packages/apps/public -p 3001"
   },
@@ -40,7 +39,7 @@
     "@types/chart.js": "^2.9.23",
     "@types/file-saver": "^2.0.1",
     "@types/i18next": "^13.0.0",
-    "@types/jest": "^26.0.7",
+    "@types/jest": "^26.0.10",
     "@types/react-beautiful-dnd": "^13.0.0",
     "@types/react-copy-to-clipboard": "^4.3.0",
     "@types/react-dom": "^16.9.8",
@@ -74,11 +73,13 @@
     "@storybook/addon-actions": "^5.2.5",
     "@storybook/addon-console": "^1.2.1",
     "@storybook/react": "^5.2.5",
-    "json-schema-to-typescript": "^7.1.0",
     "storybook-react-router": "^1.0.8",
     "typescript": "^3.9.7",
     "eslint-plugin-header": "^3.0.0",
-    "eslint-plugin-sort-destructure-keys": "^1.3.5"
+    "eslint-plugin-sort-destructure-keys": "^1.3.5",
+    "jest": "^26.4.1",
+    "ts-jest": "^26.2.0",
+    "tsconfig-paths-webpack-plugin": "^3.2.0"
   },
   "dependencies": {
     "@types/lodash": "^4.14.138",

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

@@ -33,94 +33,94 @@ function createLive (t: TFunction): LinkOption[] {
       info: 'joystream',
       text: t<string>('rpc.joystream', 'Joystream (Current Testnet, hosted by Jsgenesis)', { ns: 'apps-config' }),
       value: 'wss://rome-rpc-endpoint.joystream.org:9944'
-    },
-    {
-      dnslink: 'polkadot',
-      info: 'polkadot',
-      text: t<string>('rpc.polkadot.parity', 'Polkadot (Live, hosted by Parity)', { ns: 'apps-config' }),
-      value: 'wss://rpc.polkadot.io'
-    },
-    {
-      dnslink: 'polkadot',
-      info: 'polkadot',
-      text: t<string>('rpc.polkadot.w3f', 'Polkadot (Live, hosted by Web3 Foundation)', { ns: 'apps-config' }),
-      value: 'wss://cc1-1.polkadot.network'
-    },
-    {
-      dnslink: 'kusama',
-      info: 'kusama',
-      text: t<string>('rpc.kusama.parity', 'Kusama (Polkadot Canary, hosted by Parity)', { ns: 'apps-config' }),
-      value: 'wss://kusama-rpc.polkadot.io/'
-    },
-    {
-      dnslink: 'kusama',
-      info: 'kusama',
-      text: t<string>('rpc.kusama.w3f', 'Kusama (Polkadot Canary, hosted by Web3 Foundation)', { ns: 'apps-config' }),
-      value: 'wss://cc3-5.kusama.network/'
-    },
-    {
-      dnslink: 'kusama',
-      info: 'kusama',
-      text: t<string>('rpc.kusama.ava', 'Kusama (Polkadot Canary, user-run public nodes; see https://status.cloud.ava.do/)', { ns: 'apps-config' }),
-      value: 'wss://kusama.polkadot.cloud.ava.do/'
-    },
-    {
-      dnslink: 'centrifuge',
-      info: 'centrifuge',
-      text: t<string>('rpc.centrifuge', 'Centrifuge (Mainnet, hosted by Centrifuge)', { ns: 'apps-config' }),
-      value: 'wss://fullnode.centrifuge.io'
-    },
-    {
-      dnslink: 'edgeware',
-      info: 'edgeware',
-      text: t<string>('rpc.edgeware', 'Edgeware (Mainnet, hosted by Commonwealth Labs)', { ns: 'apps-config' }),
-      value: 'wss://mainnet1.edgewa.re'
-    },
-    {
-      dnslink: 'kulupu',
-      info: 'substrate',
-      text: t<string>('rpc.kulupu', 'Kulupu (Kulupu Mainnet, hosted by Kulupu)', { ns: 'apps-config' }),
-      value: 'wss://rpc.kulupu.network/ws'
     }
+    // {
+    //   dnslink: 'polkadot',
+    //   info: 'polkadot',
+    //   text: t<string>('rpc.polkadot.parity', 'Polkadot (Live, hosted by Parity)', { ns: 'apps-config' }),
+    //   value: 'wss://rpc.polkadot.io'
+    // },
+    // {
+    //   dnslink: 'polkadot',
+    //   info: 'polkadot',
+    //   text: t<string>('rpc.polkadot.w3f', 'Polkadot (Live, hosted by Web3 Foundation)', { ns: 'apps-config' }),
+    //   value: 'wss://cc1-1.polkadot.network'
+    // },
+    // {
+    //   dnslink: 'kusama',
+    //   info: 'kusama',
+    //   text: t<string>('rpc.kusama.parity', 'Kusama (Polkadot Canary, hosted by Parity)', { ns: 'apps-config' }),
+    //   value: 'wss://kusama-rpc.polkadot.io/'
+    // },
+    // {
+    //   dnslink: 'kusama',
+    //   info: 'kusama',
+    //   text: t<string>('rpc.kusama.w3f', 'Kusama (Polkadot Canary, hosted by Web3 Foundation)', { ns: 'apps-config' }),
+    //   value: 'wss://cc3-5.kusama.network/'
+    // },
+    // {
+    //   dnslink: 'kusama',
+    //   info: 'kusama',
+    //   text: t<string>('rpc.kusama.ava', 'Kusama (Polkadot Canary, user-run public nodes; see https://status.cloud.ava.do/)', { ns: 'apps-config' }),
+    //   value: 'wss://kusama.polkadot.cloud.ava.do/'
+    // },
+    // {
+    //   dnslink: 'centrifuge',
+    //   info: 'centrifuge',
+    //   text: t<string>('rpc.centrifuge', 'Centrifuge (Mainnet, hosted by Centrifuge)', { ns: 'apps-config' }),
+    //   value: 'wss://fullnode.centrifuge.io'
+    // },
+    // {
+    //   dnslink: 'edgeware',
+    //   info: 'edgeware',
+    //   text: t<string>('rpc.edgeware', 'Edgeware (Mainnet, hosted by Commonwealth Labs)', { ns: 'apps-config' }),
+    //   value: 'wss://mainnet1.edgewa.re'
+    // },
+    // {
+    //   dnslink: 'kulupu',
+    //   info: 'substrate',
+    //   text: t<string>('rpc.kulupu', 'Kulupu (Kulupu Mainnet, hosted by Kulupu)', { ns: 'apps-config' }),
+    //   value: 'wss://rpc.kulupu.network/ws'
+    // }
   ];
 }
 
-function createTest (t: TFunction): LinkOption[] {
-  return [
-    {
-      dnslink: 'westend',
-      info: 'westend',
-      text: t<string>('rpc.westend', 'Westend (Polkadot Testnet, hosted by Parity)', { ns: 'apps-config' }),
-      value: 'wss://westend-rpc.polkadot.io'
-    },
-    {
-      info: 'acala',
-      text: t<string>('rpc.mandala', 'Mandala (Acala Testnet, hosted by Acala)', { ns: 'apps-config' }),
-      value: 'wss://node-6684611762228215808.jm.onfinality.io/ws'
-    },
-    {
-      info: 'edgeware',
-      text: t<string>('rpc.berlin', 'Berlin (Edgeware Testnet, hosted by Commonwealth Labs)', { ns: 'apps-config' }),
-      value: 'wss://berlin1.edgewa.re'
-    },
-    {
-      info: 'substrate',
-      text: t<string>('rpc.flamingfir', 'Flaming Fir (Substrate Testnet, hosted by Parity)', { ns: 'apps-config' }),
-      value: 'wss://substrate-rpc.parity.io/'
-    },
-    {
-      info: 'nodle',
-      text: t<string>('rpc.arcadia', 'Arcadia (Nodle Testnet, hosted by Nodle)', { ns: 'apps-config' }),
-      value: 'wss://arcadia1.nodleprotocol.io/'
-    },
-    {
-      info: 'datahighway',
-      isDisabled: true,
-      text: t<string>('rpc.datahighway.harbour', 'Harbour (DataHighway Testnet, hosted by MXC)', { ns: 'apps-config' }),
-      value: 'wss://testnet-harbour.datahighway.com'
-    }
-  ];
-}
+// function createTest (t: TFunction): LinkOption[] {
+//   return [
+//     {
+//       dnslink: 'westend',
+//       info: 'westend',
+//       text: t<string>('rpc.westend', 'Westend (Polkadot Testnet, hosted by Parity)', { ns: 'apps-config' }),
+//       value: 'wss://westend-rpc.polkadot.io'
+//     },
+//     {
+//       info: 'acala',
+//       text: t<string>('rpc.mandala', 'Mandala (Acala Testnet, hosted by Acala)', { ns: 'apps-config' }),
+//       value: 'wss://node-6684611762228215808.jm.onfinality.io/ws'
+//     },
+//     {
+//       info: 'edgeware',
+//       text: t<string>('rpc.berlin', 'Berlin (Edgeware Testnet, hosted by Commonwealth Labs)', { ns: 'apps-config' }),
+//       value: 'wss://berlin1.edgewa.re'
+//     },
+//     {
+//       info: 'substrate',
+//       text: t<string>('rpc.flamingfir', 'Flaming Fir (Substrate Testnet, hosted by Parity)', { ns: 'apps-config' }),
+//       value: 'wss://substrate-rpc.parity.io/'
+//     },
+//     {
+//       info: 'nodle',
+//       text: t<string>('rpc.arcadia', 'Arcadia (Nodle Testnet, hosted by Nodle)', { ns: 'apps-config' }),
+//       value: 'wss://arcadia1.nodleprotocol.io/'
+//     },
+//     {
+//       info: 'datahighway',
+//       isDisabled: true,
+//       text: t<string>('rpc.datahighway.harbour', 'Harbour (DataHighway Testnet, hosted by MXC)', { ns: 'apps-config' }),
+//       value: 'wss://testnet-harbour.datahighway.com'
+//     }
+//   ];
+// }
 
 function createCustom (t: TFunction): LinkOption[] {
   const WS_URL = (
@@ -158,12 +158,12 @@ export default function create (t: TFunction): LinkOption[] {
       value: ''
     },
     ...createLive(t),
-    {
-      isHeader: true,
-      text: t<string>('rpc.header.test', 'Test networks', { ns: 'apps-config' }),
-      value: ''
-    },
-    ...createTest(t),
+    // {
+    //   isHeader: true,
+    //   text: t<string>('rpc.header.test', 'Test networks', { ns: 'apps-config' }),
+    //   value: ''
+    // },
+    // ...createTest(t),
     {
       isHeader: true,
       text: t<string>('rpc.header.dev', 'Development', { ns: 'apps-config' }),

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

@@ -18,29 +18,47 @@ import storage from './storage';
 import sudo from './sudo';
 import toolbox from './toolbox';
 import transfer from './transfer';
+import memo from './memo';
 // Joy packages
 import members from './joy-members';
 import { terms, privacyPolicy } from './joy-pages';
+import election from './joy-election';
+import proposals from './joy-proposals';
+import roles from './joy-roles';
+import media from './joy-media';
+import forum from './joy-forum';
 
 export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Routes {
   return appSettings.uiMode === 'light'
     ? [
+      media(t),
       members(t),
+      roles(t),
+      election(t),
+      proposals(t),
+      forum(t),
       staking(t),
       null,
       transfer(t),
       accounts(t),
+      memo(t),
       settings(t),
       // Those are hidden
       terms(t),
       privacyPolicy(t)
     ]
     : [
+      media(t),
       members(t),
+      roles(t),
+      election(t),
+      proposals(t),
+      forum(t),
       staking(t),
       null,
       transfer(t),
       accounts(t),
+      memo(t),
       settings(t),
       null,
       explorer(t),

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

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

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

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

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

@@ -0,0 +1,15 @@
+import Media from '@polkadot/joy-media/index';
+
+import { Route } from './types';
+
+export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Route {
+  return {
+    Component: Media,
+    display: {
+      needsApi: ['query.storageWorkingGroup.workerById', 'query.dataObjectStorageRegistry.relationshipsByContentId']
+    },
+    text: t<string>('nav.media', 'Media', { ns: 'apps-routing' }),
+    icon: 'play-circle',
+    name: 'media'
+  };
+}

+ 16 - 0
pioneer/packages/apps-routing/src/joy-proposals.ts

@@ -0,0 +1,16 @@
+import { Route } from './types';
+
+import Proposals from '@polkadot/joy-proposals/index';
+
+export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Route {
+  return {
+    Component: Proposals,
+    display: {
+      needsApi: ['query.proposalsEngine.proposalCount']
+    },
+    text: t<string>('nav.proposals', 'Proposals', { ns: 'apps-routing' }),
+    icon: 'tasks',
+    name: 'proposals'
+    // TODO: useCounter with active proposals count? (could be a nice addition)
+  };
+}

+ 6 - 8
pioneer/packages/old-apps/apps-routing/src/joy-roles.ts → pioneer/packages/apps-routing/src/joy-roles.ts

@@ -1,9 +1,9 @@
-import { Routes } from './types';
+import { Route } from './types';
 
 import Roles from '@polkadot/joy-roles/index';
 
-export default ([
-  {
+export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Route {
+  return {
     Component: Roles,
     display: {
       needsApi: [
@@ -11,10 +11,8 @@ export default ([
         'query.storageWorkingGroup.mint'
       ]
     },
-    i18n: {
-      defaultValue: 'Working groups'
-    },
+    text: t<string>('nav.roles', 'Working groups', { ns: 'apps-routing' }),
     icon: 'users',
     name: 'working-groups'
-  }
-] as Routes);
+  };
+}

+ 20 - 0
pioneer/packages/apps-routing/src/memo.ts

@@ -0,0 +1,20 @@
+import { Route } from './types';
+
+import { MemoModal } from '@polkadot/joy-utils/react/components/Memo';
+
+export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Route {
+  return {
+    // Assert to get around the uncecessary requirement for RouteProps
+    Component: MemoModal as React.ComponentType<any>,
+    Modal: MemoModal,
+    display: {
+      isHidden: false,
+      needsApi: [
+        'tx.memo.updateMemo'
+      ]
+    },
+    icon: 'sticky-note',
+    name: 'memo',
+    text: t<string>('nav.memo', 'My memo', { ns: 'apps-routing' })
+  };
+}

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

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

+ 0 - 0
pioneer/packages/old-apps/apps/public/images/default-thumbnail.png → pioneer/packages/apps/public/images/default-thumbnail.png


+ 1 - 0
pioneer/packages/apps/public/locales/en/index.json

@@ -25,6 +25,7 @@
   "joy-media.json",
   "joy-members.json",
   "joy-roles.json",
+  "joy-utils.json",
   "react-components.json",
   "react-params.json",
   "react-query.json",

+ 0 - 1
pioneer/packages/apps/public/locales/en/joy-settings.json

@@ -1 +0,0 @@
-{}

+ 0 - 1
pioneer/packages/apps/public/locales/en/joy-utils-old.json

@@ -1 +0,0 @@
-{}

+ 4 - 1
pioneer/packages/apps/public/locales/en/joy-utils.json

@@ -1 +1,4 @@
-{}
+{
+  "click to select or drag and drop the file here": "click to select or drag and drop the file here",
+  "{{name}} ({{size}} bytes)": "{{name}} ({{size}} bytes)"
+}

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

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

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

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

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

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

+ 0 - 2
pioneer/packages/apps/src/SideBar/index.tsx

@@ -14,7 +14,6 @@ import NetworkModal from '../modals/Network';
 import { useTranslation } from '../translate';
 import ChainInfo from './ChainInfo';
 import Item from './Item';
-import NodeInfo from './NodeInfo';
 
 interface Props {
   className?: string;
@@ -101,7 +100,6 @@ function SideBar ({ className = '', collapse, handleResize, isCollapsed, isMenuO
                 )
             ))}
             <Menu.Divider hidden />
-            {!isCollapsed && <NodeInfo />}
           </div>
           <div className={`apps--SideBar-collapse ${isCollapsed ? 'collapsed' : 'expanded'}`}>
             <Button

+ 14 - 7
pioneer/packages/apps/src/initSettings.ts

@@ -8,11 +8,17 @@ import { createEndpoints } from '@polkadot/apps-config/settings';
 import { extractIpfsDetails } from '@polkadot/react-hooks/useIpfs';
 import settings from '@polkadot/ui-settings';
 
-function getApiUrl (): string {
+// Joystream-specific override in order to include default UIMODE
+function getInitSettings () {
   // we split here so that both these forms are allowed
   //  - http://localhost:3000/?rpc=wss://substrate-rpc.parity.io/#/explorer
   //  - http://localhost:3000/#/explorer?rpc=wss://substrate-rpc.parity.io
   const urlOptions = queryString.parse(location.href.split('?')[1]);
+  const stored = store.get('settings') as Record<string, unknown> || {};
+
+  // uiMode - set to "light" if not in storage
+  const uiMode = stored.uiMode ? stored.uiMode as string : 'light';
+  let apiUrl: string;
 
   // if specified, this takes priority
   if (urlOptions.rpc) {
@@ -20,7 +26,7 @@ function getApiUrl (): string {
       throw new Error('Invalid WS endpoint specified');
     }
 
-    return urlOptions.rpc.split('#')[0]; // https://polkadot.js.org/apps/?rpc=ws://127.0.0.1:9944#/explorer;
+    apiUrl = urlOptions.rpc.split('#')[0]; // https://polkadot.js.org/apps/?rpc=ws://127.0.0.1:9944#/explorer;
   }
 
   const endpoints = createEndpoints(<T = string>(): T => ('' as unknown as T));
@@ -31,24 +37,25 @@ function getApiUrl (): string {
     const option = endpoints.find(({ dnslink }) => dnslink === ipnsChain);
 
     if (option) {
-      return option.value as string;
+      apiUrl = option.value as string;
     }
   }
 
-  const stored = store.get('settings') as Record<string, unknown> || {};
   const fallbackUrl = endpoints.find(({ value }) => !!value);
 
   // via settings, or the default chain
-  return [stored.apiUrl, process.env.WS_URL].includes(settings.apiUrl)
+  apiUrl = [stored.apiUrl, process.env.WS_URL].includes(settings.apiUrl)
     ? settings.apiUrl // keep as-is
     : fallbackUrl
       ? fallbackUrl.value as string // grab the fallback
       : 'ws://127.0.0.1:9944'; // nothing found, go local
+
+  return { apiUrl, uiMode };
 }
 
-const apiUrl = getApiUrl();
+const { apiUrl, uiMode } = getInitSettings();
 
 // set the default as retrieved here
-settings.set({ apiUrl });
+settings.set({ apiUrl, uiMode });
 
 console.log('WS endpoint=', apiUrl);

+ 14 - 0
pioneer/packages/apps/webpack.base.config.js

@@ -48,6 +48,9 @@ function createWebpack (ENV, context) {
     return alias;
   }, {});
 
+  // Add @joystream/types as alias to automatically process any changes:
+  alias['@joystream/types'] = path.resolve(context, '../../../types/src');
+
   return {
     context,
     entry: ['@babel/polyfill', './src/index.tsx'],
@@ -69,6 +72,17 @@ function createWebpack (ENV, context) {
             }
           ]
         },
+        {
+          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'
+          ]
+        },
         {
           include: /node_modules/,
           test: /\.css$/,

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


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

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

+ 9 - 6
pioneer/packages/joy-election/src/Applicant.tsx

@@ -4,24 +4,25 @@ import { Table } from 'semantic-ui-react';
 
 import { I18nProps } from '@polkadot/react-components/types';
 import { ApiProps } from '@polkadot/react-api/types';
-import { withCalls } from '@polkadot/react-api/with';
+import { withCalls } from '@polkadot/react-api/hoc';
 import { AccountId } from '@polkadot/types/interfaces';
 import { formatBalance } from '@polkadot/util';
 import CandidatePreview from './CandidatePreview';
 
 import translate from './translate';
-import { calcTotalStake } from '@polkadot/joy-utils/index';
+import { calcTotalStake } from '@polkadot/joy-utils/functions/misc';
 import { ElectionStake } from '@joystream/types/council';
 
 type Props = ApiProps & I18nProps & {
   index: number;
   accountId: AccountId;
   stake?: ElectionStake;
+  isVotingStage: boolean;
 };
 
 class Applicant extends React.PureComponent<Props> {
   render () {
-    const { index, accountId, stake } = this.props;
+    const { index, accountId, stake, isVotingStage } = this.props;
     const voteUrl = `/council/votes?applicantId=${accountId.toString()}`;
 
     return (
@@ -33,9 +34,11 @@ class Applicant extends React.PureComponent<Props> {
         <Table.Cell style={{ textAlign: 'right' }}>
           {formatBalance(calcTotalStake(stake))}
         </Table.Cell>
-        <Table.Cell>
-          <Link to={voteUrl} className='ui button primary inverted'>Vote</Link>
-        </Table.Cell>
+        { isVotingStage && (
+          <Table.Cell>
+            <Link to={voteUrl} className='ui button primary inverted'>Vote</Link>
+          </Table.Cell>
+        ) }
       </Table.Row>
     );
   }

+ 44 - 24
pioneer/packages/joy-election/src/Applicants.tsx

@@ -1,49 +1,68 @@
 import React from 'react';
-import { Table } from 'semantic-ui-react';
+import { Table, Message } from 'semantic-ui-react';
 import BN from 'bn.js';
 
 import { I18nProps } from '@polkadot/react-components/types';
 import { ApiProps } from '@polkadot/react-api/types';
-import { withCalls } from '@polkadot/react-api/with';
+import { withCalls } from '@polkadot/react-api/hoc';
 import { AccountId } from '@polkadot/types/interfaces';
+import { Option } from '@polkadot/types';
 import { formatNumber } from '@polkadot/util';
 
 import translate from './translate';
 import Applicant from './Applicant';
 import ApplyForm from './ApplyForm';
-import Section from '@polkadot/joy-utils/Section';
-import { queryToProp } from '@polkadot/joy-utils/index';
-import { withMyAccount, MyAccountProps } from '@polkadot/joy-utils/MyAccount';
+import Section from '@polkadot/joy-utils/react/components/Section';
+import { queryToProp } from '@polkadot/joy-utils/functions/misc';
+import { withMyAccount, MyAccountProps } from '@polkadot/joy-utils/react/hocs/accounts';
+import { ElectionStage } from '@joystream/types/src/council';
+import { RouteProps } from 'react-router-dom';
 
-type Props = ApiProps & I18nProps & MyAccountProps & {
+type Props = RouteProps & ApiProps & I18nProps & MyAccountProps & {
   candidacyLimit?: BN;
   applicants?: Array<AccountId>;
+  stage?: Option<ElectionStage>;
 };
 
 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>
-  )
+  private renderTable = (applicants: Array<AccountId>) => {
+    const isVotingStage = this.props.stage?.unwrapOr(undefined)?.isOfType('Voting') || false;
+
+    return (
+      <Table celled selectable compact>
+        <Table.Header>
+          <Table.Row>
+            <Table.HeaderCell>#</Table.HeaderCell>
+            <Table.HeaderCell>Applicant</Table.HeaderCell>
+            <Table.HeaderCell>Total stake</Table.HeaderCell>
+            { isVotingStage && (
+              <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} isVotingStage={isVotingStage}/>
+        ))}</Table.Body>
+      </Table>
+    );
+  }
 
   render () {
-    const { myAddress, applicants = [], candidacyLimit = new BN(0) } = this.props;
+    const { myAddress, applicants = [], candidacyLimit = new BN(0), stage } = this.props;
     const title = <span>Applicants <sup>{applicants.length}/{formatNumber(candidacyLimit)}</sup></span>;
 
     return <>
       <Section title='My application'>
-        <ApplyForm myAddress={myAddress} />
+        { stage?.unwrapOr(undefined)?.isOfType('Announcing')
+          ? (
+            <ApplyForm myAddress={myAddress} />
+          )
+          : (
+            <Message warning>
+              Applying to council is only possible during <i><b>Announcing</b></i> stage.
+            </Message>
+          )
+        }
       </Section>
       <Section title={title}>
         {!applicants.length
@@ -59,6 +78,7 @@ class Applicants extends React.PureComponent<Props> {
 export default translate(
   withCalls<Props>(
     queryToProp('query.councilElection.candidacyLimit'),
-    queryToProp('query.councilElection.applicants')
+    queryToProp('query.councilElection.applicants'),
+    queryToProp('query.councilElection.stage')
   )(withMyAccount(Applicants))
 );

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

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

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

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

+ 8 - 7
pioneer/packages/joy-election/src/Council.tsx

@@ -2,22 +2,22 @@ import React from 'react';
 
 import { ApiProps } from '@polkadot/react-api/types';
 import { I18nProps } from '@polkadot/react-components/types';
-import { withCalls } from '@polkadot/react-api/with';
+import { withCalls } from '@polkadot/react-api/hoc';
 import { Table } from 'semantic-ui-react';
 import { formatBalance } from '@polkadot/util';
 import CouncilCandidate from './CandidatePreview';
 
-import { calcBackersStake } from '@polkadot/joy-utils/index';
+import { calcBackersStake } from '@polkadot/joy-utils/functions/misc';
 import { Seat } from '@joystream/types/council';
 import translate from './translate';
-import Section from '@polkadot/joy-utils/Section';
+import Section from '@polkadot/joy-utils/react/components/Section';
+import { RouteProps } from 'react-router-dom';
 
-type Props = ApiProps &
-I18nProps & {
+type Props = RouteProps & ApiProps & I18nProps & {
   council?: Seat[];
 };
 
-type State = {};
+type State = Record<any, never>;
 
 class Council extends React.PureComponent<Props, State> {
   state: State = {};
@@ -53,9 +53,10 @@ class Council extends React.PureComponent<Props, State> {
 
   render () {
     const { council = [] } = this.props;
+
     // console.log({ council });
     return (
-      <Section title="Active council members">
+      <Section title='Active council members'>
         {!council.length ? <em>Council is not elected yet</em> : this.renderTable(council)}
       </Section>
     );

+ 83 - 53
pioneer/packages/joy-election/src/Dashboard.tsx

@@ -3,18 +3,19 @@ import React from 'react';
 
 import { ApiProps } from '@polkadot/react-api/types';
 import { I18nProps } from '@polkadot/react-components/types';
-import { withCalls } from '@polkadot/react-api/with';
+import { withCalls } from '@polkadot/react-api/hoc';
 import { Option } from '@polkadot/types';
 import { BlockNumber, Balance } from '@polkadot/types/interfaces';
-import { Bubble } from '@polkadot/react-components/index';
+import { Label, Icon } from 'semantic-ui-react';
 import { formatNumber, formatBalance } from '@polkadot/util';
 
-import Section from '@polkadot/joy-utils/Section';
-import { queryToProp } from '@polkadot/joy-utils/index';
+import Section from '@polkadot/joy-utils/react/components/Section';
+import { queryToProp } from '@polkadot/joy-utils/functions/misc';
 import { ElectionStage, Seat } from '@joystream/types/council';
 import translate from './translate';
+import { RouteProps } from 'react-router-dom';
 
-type Props = ApiProps & I18nProps & {
+type Props = RouteProps & ApiProps & I18nProps & {
   bestNumber?: BN;
 
   activeCouncil?: Seat[];
@@ -34,7 +35,7 @@ type Props = ApiProps & I18nProps & {
   stage?: Option<ElectionStage>;
 };
 
-type State = {};
+type State = Record<any, never>;
 
 class Dashboard extends React.PureComponent<Props, State> {
   state: State = {};
@@ -45,12 +46,17 @@ class Dashboard extends React.PureComponent<Props, State> {
     const title = `Council ${activeCouncil.length > 0 ? '' : '(not elected)'}`;
 
     return <Section title={title}>
-      <Bubble label='Council members'>
-        {activeCouncil.length}
-      </Bubble>
-      <Bubble icon='flag checkered' label='Term ends at block #'>
-        {formatNumber(p.termEndsAt)}
-      </Bubble>
+      <Label.Group color='grey' size='large'>
+        <Label>
+          Council members
+          <Label.Detail>{activeCouncil.length}</Label.Detail>
+        </Label>
+        <Label>
+          <Icon name='flag checkered'/>
+          Term ends at block #
+          <Label.Detail>{formatNumber(p.termEndsAt)}</Label.Detail>
+        </Label>
+      </Label.Group>
     </Section>;
   }
 
@@ -59,13 +65,16 @@ class Dashboard extends React.PureComponent<Props, State> {
 
     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
     }
 
     let leftBlocks: BN | undefined;
+
     if (stageEndsAt && bestNumber) {
       leftBlocks = stageEndsAt.sub(bestNumber);
     }
@@ -76,20 +85,28 @@ class Dashboard extends React.PureComponent<Props, State> {
     const title = <>Election (<span className={stateClass}>{stateText}</span>)</>;
 
     return <Section title={title}>
-      <Bubble icon='target' label='Election round #'>
-        {formatNumber(round)}
-      </Bubble>
-      {isRunning && <>
-        <Bubble label='Stage'>
-          {stageName}
-        </Bubble>
-        <Bubble label='Blocks left'>
-          {formatNumber(leftBlocks)}
-        </Bubble>
-        <Bubble icon='flag checkered' label='Stage ends at block #'>
-          {formatNumber(stageEndsAt)}
-        </Bubble>
-      </>}
+      <Label.Group color='grey' size='large'>
+        <Label>
+          <Icon name='target'/>
+          Election round #
+          <Label.Detail>{formatNumber(round)}</Label.Detail>
+        </Label>
+        {isRunning && <>
+          <Label>
+            Stage
+            <Label.Detail>{stageName}</Label.Detail>
+          </Label>
+          <Label>
+            Blocks left
+            <Label.Detail>{formatNumber(leftBlocks)}</Label.Detail>
+          </Label>
+          <Label>
+            <Icon name='flag checkered'/>
+            Stage ends at block #
+            <Label.Detail>{formatNumber(stageEndsAt)}</Label.Detail>
+          </Label>
+        </>}
+      </Label.Group>
     </Section>;
   }
 
@@ -98,33 +115,46 @@ class Dashboard extends React.PureComponent<Props, State> {
     const isAutoStart = (p.autoStart || false).valueOf();
 
     return <Section title='Configuration'>
-      <Bubble label='Auto-start elections'>
-        {isAutoStart ? 'Yes' : 'No'}
-      </Bubble>
-      <Bubble label='New term duration'>
-        {formatNumber(p.newTermDuration)}
-      </Bubble>
-      <Bubble label='Candidacy limit'>
-        {formatNumber(p.candidacyLimit)}
-      </Bubble>
-      <Bubble label='Council size'>
-        {formatNumber(p.councilSize)}
-      </Bubble>
-      <Bubble label='Min. council stake'>
-        {formatBalance(p.minCouncilStake)}
-      </Bubble>
-      <Bubble label='Min. voting stake'>
-        {formatBalance(p.minVotingStake)}
-      </Bubble>
-      <Bubble label='Announcing period'>
-        {formatNumber(p.announcingPeriod)} blocks
-      </Bubble>
-      <Bubble label='Voting period'>
-        {formatNumber(p.votingPeriod)} blocks
-      </Bubble>
-      <Bubble label='Revealing period'>
-        {formatNumber(p.revealingPeriod)} blocks
-      </Bubble>
+      <Label.Group color='grey' size='large'>
+        <Label>
+          Auto-start elections
+          <Label.Detail>{isAutoStart ? 'Yes' : 'No'}</Label.Detail>
+        </Label>
+        <Label>
+          New term duration
+          <Label.Detail>{formatNumber(p.newTermDuration)}</Label.Detail>
+        </Label>
+        <Label>
+          Candidacy limit
+          <Label.Detail>{formatNumber(p.candidacyLimit)}</Label.Detail>
+        </Label>
+        <Label>
+          Council size
+          <Label.Detail>{formatNumber(p.councilSize)}</Label.Detail>
+        </Label>
+        <Label>
+          Min. council stake
+          <Label.Detail>{formatBalance(p.minCouncilStake)}</Label.Detail>
+        </Label>
+        <Label>
+          Min. voting stake
+          <Label.Detail>{formatBalance(p.minVotingStake)}</Label.Detail>
+        </Label>
+      </Label.Group>
+      <Label.Group color='grey' size='large'>
+        <Label>
+          Announcing period
+          <Label.Detail>{formatNumber(p.announcingPeriod)} blocks</Label.Detail>
+        </Label>
+        <Label>
+          Voting period
+          <Label.Detail>{formatNumber(p.votingPeriod)} blocks</Label.Detail>
+        </Label>
+        <Label>
+          Revealing period
+          <Label.Detail>{formatNumber(p.revealingPeriod)} blocks</Label.Detail>
+        </Label>
+      </Label.Group>
     </Section>;
   }
 

+ 21 - 18
pioneer/packages/joy-election/src/Reveals.tsx

@@ -1,23 +1,23 @@
 import React from 'react';
 
-import { AppProps, I18nProps } from '@polkadot/react-components/types';
+import { I18nProps } from '@polkadot/react-components/types';
 import { ApiProps } from '@polkadot/react-api/types';
-import { withCalls, withMulti } from '@polkadot/react-api/with';
+import { withCalls, withMulti } from '@polkadot/react-api/hoc';
 import { AccountId } from '@polkadot/types/interfaces';
 import { Input, Labelled, InputAddress } from '@polkadot/react-components/index';
 
 import translate from './translate';
-import { nonEmptyStr, queryToProp, getUrlParam } from '@polkadot/joy-utils/index';
+import { nonEmptyStr, queryToProp, getUrlParam } from '@polkadot/joy-utils/functions/misc';
 import { accountIdsToOptions, hashVote } from './utils';
-import TxButton from '@polkadot/joy-utils/TxButton';
+import TxButton from '@polkadot/joy-utils/react/components/TxButton';
 import { findVoteByHash } from './myVotesStore';
-import { withOnlyMembers } from '@polkadot/joy-utils/MyAccount';
+import { withOnlyMembers } from '@polkadot/joy-utils/react/hocs/guards';
+import { RouteProps } from 'react-router-dom';
 
 // AppsProps is needed to get a location from the route.
-type Props = AppProps & ApiProps & I18nProps & {
+type Props = RouteProps & ApiProps & I18nProps & {
   applicantId?: string | null;
   applicants?: AccountId[];
-  location: any;
 };
 
 type State = {
@@ -30,8 +30,9 @@ class RevealVoteForm extends React.PureComponent<Props, State> {
   constructor (props: Props) {
     super(props);
     let { applicantId, location } = this.props;
-    applicantId = applicantId || getUrlParam(location, 'applicantId');
-    const hashedVote = getUrlParam(location, 'hashedVote');
+
+    applicantId = applicantId || (location && getUrlParam(location, 'applicantId'));
+    const hashedVote = location && getUrlParam(location, 'hashedVote');
 
     this.state = {
       applicantId,
@@ -45,6 +46,7 @@ class RevealVoteForm extends React.PureComponent<Props, State> {
     const applicantOpts = accountIdsToOptions(this.props.applicants || []);
 
     const myVote = hashedVote ? findVoteByHash(hashedVote) : undefined;
+
     if (myVote) {
       // Try to substitue applicantId and salt from local sotre:
       if (!applicantId) applicantId = myVote.applicantId;
@@ -81,15 +83,16 @@ class RevealVoteForm extends React.PureComponent<Props, State> {
             onChange={this.onChangeSalt}
           />
         </div>}
-        <Labelled style={{ marginTop: '.5rem' }}>
-          <TxButton
-            size='large'
-            isDisabled={!isVoteRevealed}
-            label='Reveal this vote'
-            params={[hashedVote, applicantId, salt]}
-            tx='councilElection.reveal'
-          />
-        </Labelled>
+        <div style={{ marginTop: '.5rem' }}>
+          <Labelled>
+            <TxButton
+              isDisabled={!isVoteRevealed}
+              label='Reveal this vote'
+              params={[hashedVote, applicantId, salt]}
+              tx='councilElection.reveal'
+            />
+          </Labelled>
+        </div>
       </div>
     );
   }

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

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

+ 25 - 16
pioneer/packages/joy-election/src/SealedVotes.tsx

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

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

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

+ 36 - 32
pioneer/packages/joy-election/src/VoteForm.tsx

@@ -4,9 +4,9 @@ import uuid from 'uuid/v4';
 import React from 'react';
 import { Message, Table } from 'semantic-ui-react';
 
-import { AppProps, I18nProps } from '@polkadot/react-components/types';
+import { I18nProps } from '@polkadot/react-components/types';
 import { ApiProps } from '@polkadot/react-api/types';
-import { withCalls, withMulti } from '@polkadot/react-api/with';
+import { withCalls, withMulti } from '@polkadot/react-api/hoc';
 import { AccountId, Balance } from '@polkadot/types/interfaces';
 import { Button, Input, Labelled } from '@polkadot/react-components/index';
 import { SubmittableResult } from '@polkadot/api';
@@ -14,14 +14,16 @@ import { formatBalance } from '@polkadot/util';
 
 import translate from './translate';
 import { hashVote } from './utils';
-import { queryToProp, ZERO, getUrlParam, nonEmptyStr } from '@polkadot/joy-utils/index';
-import TxButton from '@polkadot/joy-utils/TxButton';
-import InputStake from '@polkadot/joy-utils/InputStake';
+import { queryToProp, ZERO, getUrlParam, nonEmptyStr } from '@polkadot/joy-utils/functions/misc';
+import TxButton from '@polkadot/joy-utils/react/components/TxButton';
+import InputStake from '@polkadot/joy-utils/react/components/InputStake';
 import CandidatePreview from './CandidatePreview';
-import { MyAccountProps, withOnlyMembers } from '@polkadot/joy-utils/MyAccount';
-import MembersDropdown from '@polkadot/joy-utils/MembersDropdown';
+import { MyAccountProps } from '@polkadot/joy-utils/react/hocs/accounts';
+import { withOnlyMembers } from '@polkadot/joy-utils/react/hocs/guards';
+import MembersDropdown from '@polkadot/joy-utils/react/components/MembersDropdown';
 import { saveVote, NewVote } from './myVotesStore';
 import { TxFailedCallback } from '@polkadot/react-components/Status/types';
+import { RouteProps } from 'react-router-dom';
 
 // TODO use a crypto-prooven generator instead of UUID 4.
 function randomSalt () {
@@ -29,11 +31,10 @@ function randomSalt () {
 }
 
 // AppsProps is needed to get a location from the route.
-type Props = AppProps & ApiProps & I18nProps & MyAccountProps & {
+type Props = RouteProps & ApiProps & I18nProps & MyAccountProps & {
   applicantId?: string | null;
   minVotingStake?: Balance;
   applicants?: AccountId[];
-  location?: any;
 };
 
 type State = {
@@ -49,7 +50,8 @@ class Component extends React.PureComponent<Props, State> {
     super(props);
 
     let { applicantId, location } = this.props;
-    applicantId = applicantId || getUrlParam(location, 'applicantId');
+
+    applicantId = applicantId || (location && getUrlParam(location, 'applicantId'));
 
     this.state = {
       applicantId,
@@ -103,14 +105,11 @@ class Component extends React.PureComponent<Props, State> {
               </Table.Row>
             </Table.Body>
           </Table>
-          <Labelled style={{ marginTop: '.5rem' }}>
-            <Button
-              size='large'
-              label='Submit another vote'
-              onClick={this.resetForm}
-              icon=''
-            />
-          </Labelled>
+          <Button
+            label='Submit another vote'
+            onClick={this.resetForm}
+            icon='arrow-left'
+          />
         </div>
 
       // New vote form:
@@ -120,7 +119,7 @@ class Component extends React.PureComponent<Props, State> {
               onChange={ (event, data) => this.onChangeApplicant(data.value as string) }
               accounts={this.props.applicants || []}
               value={applicantId || ''}
-              placeholder="Select an applicant you support"
+              placeholder='Select an applicant you support'
             />
           </div>
           <InputStake
@@ -137,7 +136,7 @@ class Component extends React.PureComponent<Props, State> {
               onChange={this.onChangeSalt}
             />
             <div className='medium' style={{ margin: '.5rem' }}>
-              <Button onClick={this.newRandomSalt} icon=''>Generate</Button>
+              <Button onClick={this.newRandomSalt} icon='cubes' label='Generate' />
               <Message compact warning size='tiny' content='You need to remember this salt!' />
             </div>
           </div>
@@ -148,18 +147,19 @@ class Component extends React.PureComponent<Props, State> {
               value={hashedVote}
             />
           </div>
-          <Labelled style={{ marginTop: '.5rem' }}>
-            <TxButton
-              size='large'
-              isDisabled={!isFormValid}
-              label='Submit my vote'
-              params={[hashedVote, stake]}
-              tx='councilElection.vote'
-              txStartCb={this.onFormSubmitted}
-              txFailedCb={this.onTxFailed}
-              txSuccessCb={(txResult: SubmittableResult) => this.onTxSuccess(buildNewVote() as NewVote, txResult)}
-            />
-          </Labelled>
+          <div style={{ marginTop: '.5rem' }}>
+            <Labelled>
+              <TxButton
+                isDisabled={!isFormValid}
+                label='Submit my vote'
+                params={[hashedVote, stake]}
+                tx='councilElection.vote'
+                txStartCb={this.onFormSubmitted}
+                txFailedCb={this.onTxFailed}
+                txSuccessCb={(txResult: SubmittableResult) => this.onTxSuccess(buildNewVote() as NewVote, txResult)}
+              />
+            </Labelled>
+          </div>
         </div>}
       </>
     );
@@ -181,12 +181,15 @@ class Component extends React.PureComponent<Props, State> {
 
   private onTxSuccess = (vote: NewVote, txResult: SubmittableResult): void => {
     let hasVotedEvent = false;
+
     txResult.events.forEach((event, i) => {
       const { section, method } = event.event;
+
       if (section === 'councilElection' && method === 'Voted') {
         hasVotedEvent = true;
       }
     });
+
     if (hasVotedEvent) {
       saveVote(vote);
       this.setState({ isFormSubmitted: true });
@@ -203,6 +206,7 @@ class Component extends React.PureComponent<Props, State> {
 
   private onChangeStake = (stake?: BN) => {
     const isStakeValid = stake && stake.gte(this.minStake());
+
     this.setState({ stake, isStakeValid });
   }
 

+ 30 - 8
pioneer/packages/joy-election/src/Votes.tsx

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

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

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

+ 17 - 10
pioneer/packages/joy-election/src/index.tsx

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

+ 10 - 6
pioneer/packages/joy-election/src/myVotesStore.ts

@@ -1,5 +1,5 @@
 import store from 'store';
-import { nonEmptyArr } from '@polkadot/joy-utils/index';
+import { nonEmptyArr } from '@polkadot/joy-utils/functions/misc';
 
 const MY_VOTES = 'joy.myVotes';
 
@@ -19,23 +19,26 @@ export type SavedVote = NewVote & {
 
 /** Get all votes that are stored in a local sotrage.  */
 export const getAllVotes = (): SavedVote[] => {
-  const votes = store.get(MY_VOTES);
+  const votes = store.get(MY_VOTES) as unknown;
+
   return nonEmptyArr(votes) ? votes as SavedVote[] : [];
 };
 
 export const getVotesByVoter = (voterId: string): SavedVote[] => {
-  return getAllVotes().filter(v => v.voterId === voterId);
+  return getAllVotes().filter((v) => v.voterId === voterId);
 };
 
 export const findVoteByHash = (hash: string): SavedVote | undefined => {
-  return getAllVotes().find(v => v.hash === hash);
+  return getAllVotes().find((v) => v.hash === hash);
 };
 
 export const saveVote = (vote: NewVote): void => {
   const votes = getAllVotes();
-  const similarVote = votes.find(v => v.hash === vote.hash);
+  const similarVote = votes.find((v) => v.hash === vote.hash);
+
   if (similarVote) {
     console.log('There is a vote with the same hash in a storage:', similarVote);
+
     return;
   }
 
@@ -45,7 +48,8 @@ export const saveVote = (vote: NewVote): void => {
 
 export const revealVote = (hash: string): void => {
   const votes = getAllVotes();
-  const savedVote = votes.find(v => v.hash === hash);
+  const savedVote = votes.find((v) => v.hash === hash);
+
   if (savedVote && !savedVote.isRevealed) {
     savedVote.isRevealed = true;
     savedVote.revealedOnTime = Date.now();

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

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

+ 9 - 3
pioneer/packages/joy-election/src/utils.tsx

@@ -1,10 +1,11 @@
+// TODO: Move to joy-utils?
 import { AccountId } from '@polkadot/types/interfaces';
 
 // Keyring / identicon / address
 // -----------------------------------
 
 import createItem from '@polkadot/ui-keyring/options/item';
-import { findNameByAddress } from '@polkadot/joy-utils/index';
+import { findNameByAddress } from '@polkadot/joy-utils/functions/misc';
 
 // Hash
 // -----------------------------------
@@ -21,16 +22,19 @@ export type HashedVote = {
 
 const createAddressOption = (address: string) => {
   const name = findNameByAddress(address);
+
   return createItem(address, name);
 };
 
-export const accountIdsToOptions = (applicants: Array<AccountId>): any => {
+export const accountIdsToOptions = (applicants: Array<AccountId>) => {
   if (applicants && applicants.length) {
-    return applicants.map(a => {
+    return applicants.map((a) => {
       const addr = a.toString();
+
       return createAddressOption(addr);
     });
   }
+
   return [];
 };
 
@@ -44,10 +48,12 @@ export const hashVote = (accountId?: string | null, salt?: string): string | nul
   const accountU8a = decodeAddress(accountId);
   const saltU8a = stringToU8a(salt);
   const voteU8a = new Uint8Array(accountU8a.length + saltU8a.length);
+
   voteU8a.set(accountU8a);
   voteU8a.set(saltU8a, accountU8a.length);
 
   const hash = blake2AsHex(voteU8a, 256);
+
   // console.log('Vote hash:', hash, 'for', { accountId, salt });
   return hash;
 };

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


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

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

+ 33 - 32
pioneer/packages/joy-forum/src/CategoryList.tsx

@@ -1,26 +1,22 @@
 import React, { useEffect, useState } from 'react';
 import { Link } from 'react-router-dom';
 import ReactMarkdown from 'react-markdown';
-import { Table, Dropdown, Button, Segment, Label } from 'semantic-ui-react';
+import { Table, Dropdown, Button, Segment, Label, SemanticICONS, Icon } from 'semantic-ui-react';
 import styled from 'styled-components';
 import orderBy from 'lodash/orderBy';
 import BN from 'bn.js';
-
-import { Option, bool } from '@polkadot/types';
 import { ThreadId } from '@joystream/types/common';
 import { CategoryId, Category, Thread } from '@joystream/types/forum';
 import { ViewThread } from './ViewThread';
-import { MutedSpan } from '@polkadot/joy-utils/MutedText';
+import { MutedSpan, Section, JoyWarn, SemanticTxButton } from '@polkadot/joy-utils/react/components';
 import { UrlHasIdProps, CategoryCrumbs, Pagination, ThreadsPerPage, usePagination } from './utils';
-import Section from '@polkadot/joy-utils/Section';
-import { JoyWarn } from '@polkadot/joy-utils/JoyStatus';
 import { withForumCalls } from './calls';
 import { withMulti, withApi } from '@polkadot/react-api';
 import { ApiProps } from '@polkadot/react-api/types';
-import { bnToStr, isEmptyArr } from '@polkadot/joy-utils/index';
-import TxButton from '@polkadot/joy-utils/TxButton';
+import { bnToStr, isEmptyArr } from '@polkadot/joy-utils/functions/misc';
 import { IfIAmForumSudo } from './ForumSudo';
-import { MemberPreview } from '@polkadot/joy-members/MemberPreview';
+import MemberPreview from '@polkadot/joy-utils/react/components/MemberByAccountPreview';
+import { useApi } from '@polkadot/react-hooks';
 
 type CategoryActionsProps = {
   id: CategoryId;
@@ -29,23 +25,25 @@ type CategoryActionsProps = {
 
 function CategoryActions (props: CategoryActionsProps) {
   const { id, category } = props;
+  const { api } = useApi();
   const className = 'ui button ActionButton';
 
   type BtnProps = {
     label: string;
-    icon?: string;
+    icon?: SemanticICONS;
     archive?: boolean;
     delete?: boolean;
   };
 
   const UpdateCategoryButton = (btnProps: BtnProps) => {
-    return <TxButton
+    return <SemanticTxButton
       className='item'
-      isPrimary={false}
-      label={<><i className={`${btnProps.icon} icon`} />{btnProps.label}</>}
-      params={[id, new Option(bool, btnProps.archive), new Option(bool, btnProps.delete)]}
+      params={[id, api.createType('Option<bool>', btnProps.archive), api.createType('Option<bool>', btnProps.delete)]}
       tx={'forum.updateCategory'}
-    />;
+    >
+      <Icon name={btnProps.icon}/>
+      { btnProps.label }
+    </SemanticTxButton>;
   };
 
   if (category.archived) {
@@ -160,8 +158,9 @@ function InnerViewCategory (props: InnerViewCategoryProps) {
     }
 
     <Segment>
-      <div>
-        <MemberPreview accountId={category.moderator_id} prefixLabel='Creator:' />
+      <div style={{ display: 'flex', alignItems: 'center' }}>
+        <div style={{ marginRight: '0.5em', color: '#777' }}>Creator:</div>
+        <MemberPreview accountId={category.moderator_id} showCouncilBadge showId={false}/>
       </div>
       <div style={{ marginTop: '1rem' }}>
         <ReactMarkdown className='JoyMemo--full' source={category.description} linkTarget='_blank' />
@@ -207,13 +206,8 @@ type CategoryThreadsProps = ApiProps & InnerCategoryThreadsProps & {
 
 function InnerCategoryThreads (props: CategoryThreadsProps) {
   const { api, category, nextThreadId } = props;
-  const [currentPage, setCurrentPage] = usePagination();
-
-  if (!category.hasUnmoderatedThreads) {
-    return <em>No threads in this category</em>;
-  }
-
   const threadCount = category.num_threads_created.toNumber();
+  const [currentPage, setCurrentPage] = usePagination();
   const [loaded, setLoaded] = useState(false);
   const [threads, setThreads] = useState(new Array<Thread>());
 
@@ -221,16 +215,17 @@ function InnerCategoryThreads (props: CategoryThreadsProps) {
     const loadThreads = async () => {
       if (!nextThreadId || threadCount === 0) return;
 
-      const newId = (id: number | BN) => new ThreadId(id);
+      const newId = (id: number | BN) => api.createType('ThreadId', id);
       const apiCalls: Promise<Thread>[] = [];
       let id = newId(1);
+
       while (nextThreadId.gt(id)) {
         apiCalls.push(api.query.forum.threadById(id) as Promise<Thread>);
         id = newId(id.add(newId(1)));
       }
 
       const allThreads = await Promise.all<Thread>(apiCalls);
-      const threadsInThisCategory = allThreads.filter(item =>
+      const threadsInThisCategory = allThreads.filter((item) =>
         !item.isEmpty &&
         item.category_id.eq(category.id)
       );
@@ -238,9 +233,9 @@ function InnerCategoryThreads (props: CategoryThreadsProps) {
         threadsInThisCategory,
         // TODO UX: Replace sort by id with sort by blocktime of the last reply.
         [
-          x => x.moderated,
+          (x) => x.moderated,
           // x => x.pinned,
-          x => x.nr_in_category.toNumber()
+          (x) => x.nr_in_category.toNumber()
         ],
         [
           'asc',
@@ -253,9 +248,12 @@ function InnerCategoryThreads (props: CategoryThreadsProps) {
       setLoaded(true);
     };
 
-    loadThreads();
+    void loadThreads();
   }, [bnToStr(category.id), bnToStr(nextThreadId)]);
 
+  if (!category.hasUnmoderatedThreads) {
+    return <em>No threads in this category</em>;
+  }
   // console.log({ nextThreadId: bnToStr(nextThreadId), loaded, threads });
 
   if (!loaded) {
@@ -319,8 +317,10 @@ type ViewCategoryByIdProps = UrlHasIdProps & {
 
 export function ViewCategoryById (props: ViewCategoryByIdProps) {
   const { match: { params: { id } } } = props;
+  const { api } = useApi();
+
   try {
-    return <ViewCategory id={new CategoryId(id)} />;
+    return <ViewCategory id={api.createType('CategoryId', id)} />;
   } catch (err) {
     return <em>Invalid category ID: {id}</em>;
   }
@@ -340,16 +340,17 @@ function InnerCategoryList (props: CategoryListProps) {
     const loadCategories = async () => {
       if (!nextCategoryId) return;
 
-      const newId = (id: number | BN) => new CategoryId(id);
+      const newId = (id: number | BN) => api.createType('CategoryId', id);
       const apiCalls: Promise<Category>[] = [];
       let id = newId(1);
+
       while (nextCategoryId.gt(id)) {
         apiCalls.push(api.query.forum.categoryById(id) as Promise<Category>);
         id = newId(id.add(newId(1)));
       }
 
       const allCats = await Promise.all<Category>(apiCalls);
-      const filteredCats = allCats.filter(cat =>
+      const filteredCats = allCats.filter((cat) =>
         !cat.isEmpty &&
         !cat.deleted && // TODO show deleted categories if current user is forum sudo
         (parentId ? parentId.eq(cat.parent_id) : cat.isRoot)
@@ -359,7 +360,7 @@ function InnerCategoryList (props: CategoryListProps) {
       setLoaded(true);
     };
 
-    loadCategories();
+    void loadCategories();
   }, [bnToStr(parentId), bnToStr(nextCategoryId)]);
 
   // console.log({ nextCategoryId: bnToStr(nextCategoryId), loaded, categories });

+ 35 - 22
pioneer/packages/joy-forum/src/Context.tsx

@@ -2,9 +2,8 @@
 // NOTE: The purpose of this context is to immitate a Substrate storage for the forum until it's implemented as a substrate runtime module.
 
 import React, { useReducer, createContext, useContext } from 'react';
-import { Category, Thread, Reply, ModerationAction } from '@joystream/types/forum';
-import { BlockAndTime } from '@joystream/types/common';
-import { Option, Text, GenericAccountId } from '@polkadot/types';
+import { Category, Thread, Reply } from '@joystream/types/forum';
+import { createType } from '@joystream/types';
 
 type CategoryId = number;
 type ThreadId = number;
@@ -31,17 +30,17 @@ const initialState: ForumState = {
   sudo: undefined,
 
   nextCategoryId: 1,
-  categoryById: new Map(),
+  categoryById: new Map<CategoryId, Category>(),
   rootCategoryIds: [],
-  categoryIdsByParentId: new Map(),
+  categoryIdsByParentId: new Map<CategoryId, CategoryId[]>(),
 
   nextThreadId: 1,
-  threadById: new Map(),
-  threadIdsByCategoryId: new Map(),
+  threadById: new Map<ThreadId, Thread>(),
+  threadIdsByCategoryId: new Map<CategoryId, ThreadId[]>(),
 
   nextReplyId: 1,
-  replyById: new Map(),
-  replyIdsByThreadId: new Map()
+  replyById: new Map<ReplyId, Reply>(),
+  replyIdsByThreadId: new Map<ThreadId, ReplyId[]>()
 };
 
 type SetForumSudo = {
@@ -114,6 +113,7 @@ function reducer (state: ForumState, action: ForumAction): ForumState {
   switch (action.type) {
     case 'SetForumSudo': {
       const { sudo } = action;
+
       return {
         ...state,
         sudo
@@ -133,19 +133,23 @@ function reducer (state: ForumState, action: ForumAction): ForumState {
 
       if (parent_id) {
         let childrenIds = categoryIdsByParentId.get(parent_id.toNumber());
+
         if (!childrenIds) {
           childrenIds = [];
         }
+
         childrenIds.push(nextCategoryId);
         categoryIdsByParentId.set(parent_id.toNumber(), childrenIds);
       } else {
         if (!rootCategoryIds) {
           rootCategoryIds = [];
         }
+
         rootCategoryIds.push(nextCategoryId);
       }
 
       const newId = nextCategoryId;
+
       categoryById.set(newId, category);
       nextCategoryId = nextCategoryId + 1;
 
@@ -183,13 +187,16 @@ function reducer (state: ForumState, action: ForumAction): ForumState {
       } = state;
 
       let threadIds = threadIdsByCategoryId.get(category_id.toNumber());
+
       if (!threadIds) {
         threadIds = [];
         threadIdsByCategoryId.set(category_id.toNumber(), threadIds);
       }
+
       threadIds.push(nextThreadId);
 
       const newId = nextThreadId;
+
       threadById.set(newId, thread);
       nextThreadId = nextThreadId + 1;
 
@@ -220,15 +227,16 @@ function reducer (state: ForumState, action: ForumAction): ForumState {
       const { threadById } = state;
 
       const thread = threadById.get(id) as Thread;
-      const moderation = new ModerationAction({
-        moderated_at: BlockAndTime.newEmpty(),
-        moderator_id: new GenericAccountId(moderator),
-        rationale: new Text(rationale)
+      const moderation = createType('ModerationAction', {
+        moderated_at: createType('BlockAndTime', {}),
+        moderator_id: createType('AccountId', moderator),
+        rationale: createType('Text', rationale)
       });
-      const threadUpd = new Thread(Object.assign(
+      const threadUpd = createType('Thread', Object.assign(
         thread.cloneValues(),
-        { moderation: new Option(ModerationAction, moderation) }
+        { moderation: createType('Option<ModerationAction>', moderation) }
       ));
+
       threadById.set(id, threadUpd);
 
       return {
@@ -248,13 +256,16 @@ function reducer (state: ForumState, action: ForumAction): ForumState {
       } = state;
 
       let replyIds = replyIdsByThreadId.get(thread_id.toNumber());
+
       if (!replyIds) {
         replyIds = [];
         replyIdsByThreadId.set(thread_id.toNumber(), replyIds);
       }
+
       replyIds.push(nextReplyId);
 
       const newId = nextReplyId;
+
       replyById.set(newId, reply);
       nextReplyId = nextReplyId + 1;
 
@@ -285,15 +296,16 @@ function reducer (state: ForumState, action: ForumAction): ForumState {
       const { replyById } = state;
 
       const reply = replyById.get(id) as Reply;
-      const moderation = new ModerationAction({
-        moderated_at: BlockAndTime.newEmpty(),
-        moderator_id: new GenericAccountId(moderator),
-        rationale: new Text(rationale)
+      const moderation = createType('ModerationAction', {
+        moderated_at: createType('BlockAndTime', {}),
+        moderator_id: createType('AccountId', moderator),
+        rationale: createType('Text', rationale)
       });
-      const replyUpd = new Reply(Object.assign(
+      const replyUpd = createType('Reply', Object.assign(
         reply.cloneValues(),
-        { moderation: new Option(ModerationAction, moderation) }
+        { moderation: createType('Option<ModerationAction>', moderation) }
       ));
+
       replyById.set(id, replyUpd);
 
       return {
@@ -323,8 +335,9 @@ const contextStub: ForumContextProps = {
 
 export const ForumContext = createContext<ForumContextProps>(contextStub);
 
-export function ForumProvider (props: React.PropsWithChildren<{}>) {
+export function ForumProvider (props: React.PropsWithChildren<Record<any, unknown>>) {
   const [state, dispatch] = useReducer(reducer, initialState);
+
   return (
     <ForumContext.Provider value={{ state, dispatch }}>
       {props.children}

+ 24 - 17
pioneer/packages/joy-forum/src/EditCategory.tsx

@@ -4,21 +4,20 @@ import { Form, Field, withFormik, FormikProps } from 'formik';
 import * as Yup from 'yup';
 import { History } from 'history';
 
-import TxButton from '@polkadot/joy-utils/TxButton';
+import { TxButton, Section } from '@polkadot/joy-utils/react/components';
 import { SubmittableResult } from '@polkadot/api';
-import { withMulti } from '@polkadot/react-api/with';
+import { withMulti } from '@polkadot/react-api/hoc';
 
-import * as JoyForms from '@polkadot/joy-utils/forms';
-import { Text } from '@polkadot/types';
-import { Option } from '@polkadot/types/codec';
+import * as JoyForms from '@polkadot/joy-utils/react/components/forms';
 import { CategoryId, Category } from '@joystream/types/forum';
-import Section from '@polkadot/joy-utils/Section';
-import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext';
+import { useMyAccount } from '@polkadot/joy-utils/react/hooks';
 import { UrlHasIdProps, CategoryCrumbs } from './utils';
 import { withOnlyForumSudo } from './ForumSudo';
 import { withForumCalls } from './calls';
 import { ValidationProps, withCategoryValidation } from './validation';
 import { TxFailedCallback, TxCallback } from '@polkadot/react-components/Status/types';
+import { createType } from '@joystream/types';
+import { useApi } from '@polkadot/react-hooks';
 
 const buildSchema = (props: ValidationProps) => {
   const {
@@ -92,6 +91,7 @@ const InnerForm = (props: FormProps) => {
 
   const onTxFailed: TxFailedCallback = (txResult: SubmittableResult | null) => {
     setSubmitting(false);
+
     if (txResult == null) {
       // Tx cancelled.
 
@@ -104,12 +104,15 @@ const InnerForm = (props: FormProps) => {
 
     // Get id of newly created category:
     let _id = id;
+
     if (!_id) {
-      _txResult.events.find(event => {
+      _txResult.events.find((event) => {
         const { event: { data, method } } = event;
+
         if (method === 'CategoryCreated') {
           _id = data.toArray()[0] as CategoryId;
         }
+
         return true;
       });
     }
@@ -128,9 +131,9 @@ const InnerForm = (props: FormProps) => {
 
     if (isNew) {
       return [
-        new Option(CategoryId, parentId),
-        new Text(title),
-        new Text(description)
+        createType('Option<CategoryId>', parentId),
+        title,
+        description
       ];
     } else {
       // NOTE: currently update_category doesn't support title and description updates.
@@ -149,10 +152,9 @@ const InnerForm = (props: FormProps) => {
         <Field component='textarea' id='description' name='description' disabled={isSubmitting} rows={3} placeholder={`Describe your ${categoryWord}. You can use Markdown.`} />
       </LabelledField>
 
-      <LabelledField {...props}>
+      <LabelledField {...props} flex>
         <TxButton
           type='submit'
-          size='large'
           label={isNew
             ? `Create a ${categoryWord}`
             : 'Update a category'
@@ -192,7 +194,7 @@ const InnerForm = (props: FormProps) => {
 const EditForm = withFormik<OuterProps, FormValues>({
 
   // Transform outer props into form values
-  mapPropsToValues: props => {
+  mapPropsToValues: (props) => {
     const { parentId, struct } = props;
 
     return {
@@ -204,7 +206,7 @@ const EditForm = withFormik<OuterProps, FormValues>({
 
   validationSchema: buildSchema,
 
-  handleSubmit: values => {
+  handleSubmit: (values) => {
     // do submitting things
   }
 })(InnerForm);
@@ -222,6 +224,7 @@ function FormOrLoading (props: OuterProps) {
   }
 
   const isMyStruct = address === struct.moderator_id.toString();
+
   if (isMyStruct) {
     return <EditForm {...props} />;
   }
@@ -232,8 +235,10 @@ function FormOrLoading (props: OuterProps) {
 function withIdFromUrl (Component: React.ComponentType<OuterProps>) {
   return function (props: UrlHasIdProps) {
     const { match: { params: { id } } } = props;
+    const { api } = useApi();
+
     try {
-      return <Component id={new CategoryId(id)} />;
+      return <Component id={api.createType('CategoryId', id)} />;
     } catch (err) {
       return <em>Invalid category ID: {id}</em>;
     }
@@ -242,8 +247,10 @@ function withIdFromUrl (Component: React.ComponentType<OuterProps>) {
 
 function NewSubcategoryForm (props: UrlHasIdProps & OuterProps) {
   const { match: { params: { id } } } = props;
+  const { api } = useApi();
+
   try {
-    return <EditForm {...props} parentId={new CategoryId(id)} />;
+    return <EditForm {...props} parentId={api.createType('CategoryId', id)} />;
   } catch (err) {
     return <em>Invalid parent category id: {id}</em>;
   }

+ 17 - 15
pioneer/packages/joy-forum/src/EditReply.tsx

@@ -4,17 +4,16 @@ import styled from 'styled-components';
 import { Form, Field, withFormik, FormikProps } from 'formik';
 import * as Yup from 'yup';
 
-import TxButton from '@polkadot/joy-utils/TxButton';
+import { TxButton, Section } from '@polkadot/joy-utils/react/components';
 import { SubmittableResult } from '@polkadot/api';
-import { withMulti } from '@polkadot/react-api/with';
+import { withMulti } from '@polkadot/react-api/hoc';
 
-import * as JoyForms from '@polkadot/joy-utils/forms';
-import { Text } from '@polkadot/types';
+import * as JoyForms from '@polkadot/joy-utils/react/components/forms';
 import { PostId, ThreadId } from '@joystream/types/common';
 import { Post } from '@joystream/types/forum';
-import { withOnlyMembers } from '@polkadot/joy-utils/MyAccount';
-import Section from '@polkadot/joy-utils/Section';
-import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext';
+import { withOnlyMembers } from '@polkadot/joy-utils/react/hocs/guards';
+
+import { useMyAccount } from '@polkadot/joy-utils/react/hooks';
 import { withForumCalls } from './calls';
 import { ValidationProps, withReplyValidation } from './validation';
 import { TxFailedCallback, TxCallback } from '@polkadot/react-components/Status/types';
@@ -88,6 +87,7 @@ const InnerForm = (props: FormProps) => {
 
   const onTxFailed: TxFailedCallback = (txResult: SubmittableResult | null) => {
     setSubmitting(false);
+
     if (txResult == null) {
       // Tx cancelled.
 
@@ -97,6 +97,7 @@ const InnerForm = (props: FormProps) => {
   const onTxSuccess: TxCallback = (_txResult: SubmittableResult) => {
     setSubmitting(false);
     resetForm();
+
     if (!isNew && onEditSuccess) {
       onEditSuccess();
     }
@@ -105,11 +106,10 @@ const InnerForm = (props: FormProps) => {
   const buildTxParams = () => {
     if (!isValid) return [];
 
-    const textParam = new Text(text);
     if (!id) {
-      return [threadId, textParam];
+      return [threadId, text];
     } else {
-      return [id, textParam];
+      return [id, text];
     }
   };
 
@@ -122,10 +122,9 @@ const InnerForm = (props: FormProps) => {
 
       <LabelledField {...props}>
         <FormActionsContainer>
-          <div>
+          <div style={{ display: 'flex' }}>
             <TxButton
               type='submit'
-              size='large'
               label={isNew
                 ? 'Post a reply'
                 : 'Update a reply'
@@ -165,7 +164,7 @@ const InnerForm = (props: FormProps) => {
 
   const sectionTitle = isNew
     ? 'New reply'
-    : `Edit my reply #${struct?.nr_in_thread}`;
+    : `Edit my reply #${struct?.nr_in_thread.toString() || ''}`;
 
   return (
     <Section className='EditEntityBox' title={sectionTitle}>
@@ -176,6 +175,7 @@ const InnerForm = (props: FormProps) => {
 
 const getQuotedPostString = (post: Post) => {
   const lines = post.current_text.split('\n');
+
   return lines.reduce((acc, line) => {
     return `${acc}> ${line}\n`;
   }, '');
@@ -184,8 +184,9 @@ const getQuotedPostString = (post: Post) => {
 const EditForm = withFormik<OuterProps, FormValues>({
 
   // Transform outer props into form values
-  mapPropsToValues: props => {
+  mapPropsToValues: (props) => {
     const { struct, quotedPost } = props;
+
     return {
       text: struct
         ? struct.current_text
@@ -197,7 +198,7 @@ const EditForm = withFormik<OuterProps, FormValues>({
 
   validationSchema: buildSchema,
 
-  handleSubmit: values => {
+  handleSubmit: (values) => {
     // do submitting things
   }
 })(InnerForm);
@@ -215,6 +216,7 @@ function FormOrLoading (props: OuterProps) {
   }
 
   const isMyStruct = address === struct.author_id.toString();
+
   if (isMyStruct) {
     return <EditForm {...props} threadId={struct.thread_id} />;
   }

+ 24 - 16
pioneer/packages/joy-forum/src/EditThread.tsx

@@ -6,21 +6,21 @@ import { Form, Field, withFormik, FormikProps } from 'formik';
 import * as Yup from 'yup';
 import { History } from 'history';
 
-import TxButton from '@polkadot/joy-utils/TxButton';
+import { TxButton, Section } from '@polkadot/joy-utils/react/components';
 import { SubmittableResult } from '@polkadot/api';
-import { withMulti } from '@polkadot/react-api/with';
+import { withMulti } from '@polkadot/react-api/hoc';
 
-import * as JoyForms from '@polkadot/joy-utils/forms';
-import { Text } from '@polkadot/types';
+import * as JoyForms from '@polkadot/joy-utils/react/components/forms';
 import { ThreadId } from '@joystream/types/common';
 import { Thread, CategoryId } from '@joystream/types/forum';
-import { withOnlyMembers } from '@polkadot/joy-utils/MyAccount';
-import Section from '@polkadot/joy-utils/Section';
-import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext';
+import { withOnlyMembers } from '@polkadot/joy-utils/react/hocs/guards';
+
+import { useMyAccount } from '@polkadot/joy-utils/react/hooks';
 import { UrlHasIdProps, CategoryCrumbs } from './utils';
 import { withForumCalls } from './calls';
 import { ValidationProps, withThreadValidation } from './validation';
 import { TxFailedCallback, TxCallback } from '@polkadot/react-components/Status/types';
+import { useApi } from '@polkadot/react-hooks';
 
 const buildSchema = (props: ValidationProps) => {
   const {
@@ -96,6 +96,7 @@ const InnerForm = (props: FormProps) => {
 
   const onTxFailed: TxFailedCallback = (txResult: SubmittableResult | null) => {
     setSubmitting(false);
+
     if (txResult == null) {
       // Tx cancelled.
 
@@ -108,12 +109,15 @@ const InnerForm = (props: FormProps) => {
 
     // Get id of newly created thread:
     let _id = id;
+
     if (!_id) {
-      _txResult.events.find(event => {
+      _txResult.events.find((event) => {
         const { event: { data, method } } = event;
+
         if (method === 'ThreadCreated') {
           _id = data.toArray()[0] as ThreadId;
         }
+
         return true;
       });
     }
@@ -141,8 +145,8 @@ const InnerForm = (props: FormProps) => {
     if (isNew) {
       return [
         resolvedCategoryId,
-        new Text(title),
-        new Text(text)
+        title,
+        text
       ];
     } else {
       // NOTE: currently forum SRML doesn't support thread update.
@@ -179,10 +183,9 @@ const InnerForm = (props: FormProps) => {
         <Field component='textarea' id='text' name='text' disabled={isSubmitting} rows={5} placeholder='Type here. You can use Markdown.' />
       </LabelledField>
 
-      <LabelledField {...props}>
+      <LabelledField {...props} flex>
         <TxButton
           type='submit'
-          size='large'
           label={isNew
             ? 'Create a thread'
             : 'Update a thread'
@@ -222,7 +225,7 @@ const InnerForm = (props: FormProps) => {
 const EditForm = withFormik<OuterProps, FormValues>({
 
   // Transform outer props into form values
-  mapPropsToValues: props => {
+  mapPropsToValues: (props) => {
     return {
       // pinned: struct && struct.pinned || false,
       title: '',
@@ -232,7 +235,7 @@ const EditForm = withFormik<OuterProps, FormValues>({
 
   validationSchema: buildSchema,
 
-  handleSubmit: values => {
+  handleSubmit: (values) => {
     // do submitting things
   }
 })(InnerForm);
@@ -250,6 +253,7 @@ function FormOrLoading (props: OuterProps) {
   }
 
   const isMyStruct = address === struct.author_id.toString();
+
   if (isMyStruct) {
     return <EditForm {...props} />;
   }
@@ -260,8 +264,10 @@ function FormOrLoading (props: OuterProps) {
 function withCategoryIdFromUrl (Component: React.ComponentType<OuterProps>) {
   return function (props: UrlHasIdProps) {
     const { match: { params: { id } } } = props;
+    const { api } = useApi();
+
     try {
-      return <Component {...props} categoryId={new CategoryId(id)} />;
+      return <Component {...props} categoryId={api.createType('CategoryId', id)} />;
     } catch (err) {
       return <em>Invalid category ID: {id}</em>;
     }
@@ -271,8 +277,10 @@ function withCategoryIdFromUrl (Component: React.ComponentType<OuterProps>) {
 function withIdFromUrl (Component: React.ComponentType<OuterProps>) {
   return function (props: UrlHasIdProps) {
     const { match: { params: { id } } } = props;
+    const { api } = useApi();
+
     try {
-      return <Component {...props} id={new ThreadId(id)} />;
+      return <Component {...props} id={api.createType('ThreadId', id)} />;
     } catch (err) {
       return <em>Invalid thread ID: {id}</em>;
     }

+ 20 - 13
pioneer/packages/joy-forum/src/ForumRoot.tsx

@@ -4,13 +4,13 @@ import styled from 'styled-components';
 import { orderBy } from 'lodash';
 import BN from 'bn.js';
 
-import Section from '@polkadot/joy-utils/Section';
+import { Section } from '@polkadot/joy-utils/react/components';
 import { withMulti, withApi } from '@polkadot/react-api';
 import { PostId } from '@joystream/types/common';
 import { Post, Thread } from '@joystream/types/forum';
-import { bnToStr } from '@polkadot/joy-utils/';
+import { bnToStr } from '@polkadot/joy-utils/functions/misc';
 import { ApiProps } from '@polkadot/react-api/types';
-import { MemberPreview } from '@polkadot/joy-members/MemberPreview';
+import MemberPreview from '@polkadot/joy-utils/react/components/MemberByAccountPreview';
 
 import { CategoryCrumbs, RecentActivityPostsCount, ReplyIdxQueryParam, TimeAgoDate } from './utils';
 import { withForumCalls } from './calls';
@@ -21,7 +21,7 @@ const ForumRoot: React.FC = () => {
     <>
       <CategoryCrumbs root />
       <RecentActivity />
-      <Section title="Top categories">
+      <Section title='Top categories'>
         <CategoryList />
       </Section>
     </>
@@ -62,9 +62,10 @@ const InnerRecentActivity: React.FC<RecentActivityProps> = ({ nextPostId, api })
     const loadPosts = async () => {
       if (!nextPostId) return;
 
-      const newId = (id: number | BN) => new PostId(id);
+      const newId = (id: number | BN) => api.createType('PostId', id);
       const apiCalls: Promise<Post>[] = [];
       let id = newId(1);
+
       while (nextPostId.gt(id)) {
         apiCalls.push(api.query.forum.postById(id) as Promise<Post>);
         id = newId(id.add(newId(1)));
@@ -73,16 +74,18 @@ const InnerRecentActivity: React.FC<RecentActivityProps> = ({ nextPostId, api })
       const allPosts = await Promise.all(apiCalls);
       const sortedPosts = orderBy(
         allPosts,
-        [x => x.id.toNumber()],
+        [(x) => x.id.toNumber()],
         ['desc']
       );
 
       const threadsIdsLookup = {} as Record<number, boolean>;
       const postsWithUniqueThreads = sortedPosts.reduce((acc, post) => {
         const threadId = post.thread_id.toNumber();
+
         if (threadsIdsLookup[threadId]) return acc;
 
         threadsIdsLookup[threadId] = true;
+
         return [
           ...acc,
           post
@@ -90,22 +93,24 @@ const InnerRecentActivity: React.FC<RecentActivityProps> = ({ nextPostId, api })
       }, [] as Post[]);
 
       const recentUniquePosts = postsWithUniqueThreads.slice(0, RecentActivityPostsCount);
+
       setRecentPosts(recentUniquePosts);
       setLoaded(true);
     };
 
-    loadPosts();
+    void loadPosts();
   }, [bnToStr(nextPostId)]);
 
   useEffect(() => {
     const loadThreads = async () => {
       const apiCalls: Promise<Thread>[] = recentPosts
-        .filter(p => !threadsLookup[p.thread_id.toNumber()])
-        .map(p => api.query.forum.threadById(p.thread_id) as Promise<Thread>);
+        .filter((p) => !threadsLookup[p.thread_id.toNumber()])
+        .map((p) => api.query.forum.threadById(p.thread_id) as Promise<Thread>);
 
       const threads = await Promise.all(apiCalls);
       const newThreadsLookup = threads.reduce((acc, thread) => {
         acc[thread.id.toNumber()] = thread;
+
         return acc;
       }, {} as Record<number, Thread>);
       const newLookup = {
@@ -116,21 +121,23 @@ const InnerRecentActivity: React.FC<RecentActivityProps> = ({ nextPostId, api })
       setThreadsLookup(newLookup);
     };
 
-    loadThreads();
+    void loadThreads();
   }, [recentPosts]);
 
   const renderSectionContent = () => {
     if (!loaded) {
       return <i>Loading recent activity...</i>;
     }
+
     if (loaded && !recentPosts.length) {
       return <span>No recent activity</span>;
     }
 
-    return recentPosts.map(p => {
+    return recentPosts.map((p) => {
       const threadId = p.thread_id.toNumber();
 
       const postLinkSearch = new URLSearchParams();
+
       postLinkSearch.set(ReplyIdxQueryParam, p.nr_in_thread.toString());
       const postLinkPathname = `/forum/threads/${threadId}`;
 
@@ -138,7 +145,7 @@ const InnerRecentActivity: React.FC<RecentActivityProps> = ({ nextPostId, api })
 
       return (
         <RecentActivityEntry key={p.id.toNumber()}>
-          <StyledMemberPreview accountId={p.author_id} inline />
+          <StyledMemberPreview accountId={p.author_id} size='small' showId={false}/>
           posted in
           {thread && (
             <StyledPostLink to={{ pathname: postLinkPathname, search: postLinkSearch.toString() }}>{thread.title}</StyledPostLink>
@@ -150,7 +157,7 @@ const InnerRecentActivity: React.FC<RecentActivityProps> = ({ nextPostId, api })
   };
 
   return (
-    <Section title="Recent activity">
+    <Section title='Recent activity'>
       {renderSectionContent()}
     </Section>
   );

+ 13 - 180
pioneer/packages/joy-forum/src/ForumSudo.tsx

@@ -1,169 +1,15 @@
-import React, { useState, useContext, createContext } from 'react';
-import { Button } from 'semantic-ui-react';
-import { Form, Field, withFormik, FormikProps, FieldProps } from 'formik';
-import * as Yup from 'yup';
+import React, { useContext, createContext } from 'react';
 
-import TxButton from '@polkadot/joy-utils/TxButton';
-import { SubmittableResult } from '@polkadot/api';
-import { InputAddress } from '@polkadot/react-components/index';
-import { withMulti } from '@polkadot/react-api/with';
+import { JoyError } from '@polkadot/joy-utils/react/components';
+import { withMulti } from '@polkadot/react-api/hoc';
 
-import * as JoyForms from '@polkadot/joy-utils/forms';
 import { Option } from '@polkadot/types/codec';
-import Section from '@polkadot/joy-utils/Section';
-import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext';
-import { withOnlySudo } from '@polkadot/joy-utils/Sudo';
-import { AccountId } from '@polkadot/types/interfaces';
-import { JoyError } from '@polkadot/joy-utils/JoyStatus';
-import AddressMini from '@polkadot/react-components/AddressMiniJoy';
-import { withForumCalls } from './calls';
-import { TxFailedCallback, TxCallback } from '@polkadot/react-components/Status/types';
-
-const buildSchema = () => Yup.object().shape({});
-
-type OuterProps = {
-  currentSudo?: string;
-};
-
-type FormValues = {
-  sudo?: string;
-};
-
-type FormProps = OuterProps & FormikProps<FormValues>;
-
-const LabelledField = JoyForms.LabelledField<FormValues>();
-
-const InnerForm = (props: FormProps) => {
-  const {
-    currentSudo,
-    values,
-    dirty,
-    isValid,
-    isSubmitting,
-    setSubmitting
-  } = props;
-
-  const {
-    sudo
-  } = values;
-
-  const [showSelector, setShowSelector] = useState(false);
-
-  const resetForm = () => {
-    setShowSelector(false);
-    props.resetForm();
-  };
-
-  const onSubmit = (sendTx: () => void) => {
-    if (isValid) sendTx();
-  };
-
-  const onTxFailed: TxFailedCallback = (txResult: SubmittableResult | null) => {
-    setSubmitting(false);
-    if (txResult == null) {
-      // Tx cancelled.
-
-    }
-  };
-
-  const onTxSuccess: TxCallback = (_txResult: SubmittableResult) => {
-    setSubmitting(false);
-    resetForm();
-  };
-
-  const isNotSet = currentSudo === undefined;
-
-  const buildTxParams = () => {
-    if (!isValid) return [];
-    return [new Option('AccountId', sudo)];
-  };
-
-  type SudoInputAddressProps = FieldProps<FormValues>; /* & InputAddressProps */
-
-  const SudoInputAddress = ({ field, form, ...props }: SudoInputAddressProps) => {
-    const { name, value } = field;
-
-    const onChange = (address: string | null) => {
-      address !== value && form.setFieldValue(name, address);
-    };
-
-    return (
-      <InputAddress
-        {...props}
-        // name={name}
-        value={value}
-        onChange={onChange}
-        withLabel={false}
-      />
-    );
-  };
-
-  const form = () => (
-    <Form className='ui form JoyForm EditEntityForm'>
-
-      <LabelledField name='sudo' {...props}>
-        <Field component={SudoInputAddress} id='sudo' name='sudo' disabled={isSubmitting} />
-      </LabelledField>
-
-      <LabelledField {...props}>
-        <TxButton
-          type='submit'
-          size='large'
-          label={isNotSet
-            ? 'Set forum sudo'
-            : 'Update forum sudo'
-          }
-          isDisabled={!dirty || isSubmitting}
-          params={buildTxParams()}
-          tx={'forum.setForumSudo'}
-          onClick={onSubmit}
-          txFailedCb={onTxFailed}
-          txSuccessCb={onTxSuccess}
-        />
-        <Button
-          type='button'
-          size='large'
-          disabled={!dirty || isSubmitting}
-          onClick={resetForm}
-          content='Reset form'
-        />
-      </LabelledField>
-    </Form>
-  );
-
-  return showSelector
-    ? (
-      <Section className='EditEntityBox'>
-        {form()}
-      </Section>
-    )
-    : (<>
-      {currentSudo && <p><AddressMini value={currentSudo} /></p>}
-      <Button
-        type='button'
-        size='large'
-        onClick={() => setShowSelector(true)}
-        content={`${currentSudo ? 'Edit' : 'Set'} forum sudo`}
-      />
-    </>);
-};
-
-const EditForm = withFormik<OuterProps, FormValues>({
-
-  // Transform outer props into form values
-  mapPropsToValues: props => {
-    const { currentSudo } = props;
-    return {
-      sudo: currentSudo
-    };
-  },
 
-  validationSchema: buildSchema,
+import { useMyAccount } from '@polkadot/joy-utils/react/hooks';
+import { AccountId } from '@polkadot/types/interfaces';
 
-  handleSubmit: values => {
-    // do submitting things
-  }
-})(InnerForm);
+import AddressMini from '@polkadot/react-components/AddressMini';
+import { withForumCalls } from './calls';
 
 type LoadStructProps = {
   structOpt: Option<AccountId>;
@@ -173,25 +19,10 @@ const withLoadForumSudo = withForumCalls<LoadStructProps>(
   ['forumSudo', { propName: 'structOpt' }]
 );
 
-function InjectCurrentSudo (props: LoadStructProps) {
-  const { structOpt } = props;
-  if (!structOpt) {
-    return <em>Loading forum sudo...</em>;
-  }
-
-  const sudo = structOpt.isSome ? structOpt.unwrap().toString() : undefined;
-  return <EditForm currentSudo={sudo} />;
-}
-
-export const EditForumSudo = withMulti(
-  InjectCurrentSudo,
-  withOnlySudo,
-  withLoadForumSudo
-);
-
 function innerWithOnlyForumSudo<P extends LoadStructProps> (Component: React.ComponentType<P>) {
   return function (props: P) {
     const { structOpt } = props;
+
     if (!structOpt) {
       return <em>Loading forum sudo...</em>;
     }
@@ -213,7 +44,7 @@ function innerWithOnlyForumSudo<P extends LoadStructProps> (Component: React.Com
   };
 }
 
-export function withOnlyForumSudo<P extends {}> (Component: React.ComponentType<P>) {
+export function withOnlyForumSudo<P extends Record<string, unknown>> (Component: React.ComponentType<P>) {
   return withMulti(
     Component,
     withLoadForumSudo,
@@ -230,6 +61,7 @@ export const ForumSudoContext = createContext<ForumSudoContextProps>({});
 export function InnerForumSudoProvider (props: React.PropsWithChildren<LoadStructProps>) {
   const { structOpt } = props;
   const forumSudo = structOpt ? structOpt.unwrapOr(undefined) : undefined;
+
   return (
     <ForumSudoContext.Provider value={{ forumSudo }}>
       {props.children}
@@ -246,9 +78,10 @@ export function useForumSudo () {
   return useContext(ForumSudoContext);
 }
 
-export const IfIAmForumSudo = (props: React.PropsWithChildren<any>) => {
+export const IfIAmForumSudo = (props: React.PropsWithChildren<Record<any, unknown>>) => {
   const { forumSudo } = useForumSudo();
   const { state: { address: myAddress } } = useMyAccount();
   const iAmForumSudo: boolean = forumSudo !== undefined && forumSudo.eq(myAddress);
-  return iAmForumSudo ? props.children : null;
+
+  return iAmForumSudo ? <>{props.children}</> : null;
 };

+ 3 - 0
pioneer/packages/joy-forum/src/LegacyPagingRedirect.tsx

@@ -5,6 +5,7 @@ export const LegacyPagingRedirect: React.FC = () => {
   const { pathname } = useLocation();
   const parsingRegexp = /(.+)\/page\/(\d+)/;
   const groups = parsingRegexp.exec(pathname);
+
   if (!groups) {
     return <em>Failed to parse the URL</em>;
   }
@@ -12,6 +13,8 @@ export const LegacyPagingRedirect: React.FC = () => {
   const basePath = groups[1];
   const page = groups[2];
   const search = new URLSearchParams();
+
   search.set('page', page);
+
   return <Redirect to={{ pathname: basePath, search: search.toString() }} />;
 };

+ 10 - 12
pioneer/packages/joy-forum/src/Moderate.tsx

@@ -3,15 +3,14 @@ import { Button } from 'semantic-ui-react';
 import { Form, Field, withFormik, FormikProps } from 'formik';
 import * as Yup from 'yup';
 
-import TxButton from '@polkadot/joy-utils/TxButton';
+import { TxButton, Section } from '@polkadot/joy-utils/react/components';
 import { SubmittableResult } from '@polkadot/api';
-import { withMulti } from '@polkadot/react-api/with';
+import { withMulti } from '@polkadot/react-api/hoc';
 
-import * as JoyForms from '@polkadot/joy-utils/forms';
-import { Text } from '@polkadot/types';
+import * as JoyForms from '@polkadot/joy-utils/react/components/forms';
 import { ThreadId } from '@joystream/types/common';
 import { ReplyId } from '@joystream/types/forum';
-import Section from '@polkadot/joy-utils/Section';
+
 import { withOnlyForumSudo } from './ForumSudo';
 import { ValidationProps, withPostModerationValidation } from './validation';
 import { TxFailedCallback, TxCallback } from '@polkadot/react-components/Status/types';
@@ -74,6 +73,7 @@ const InnerForm = (props: FormProps) => {
 
   const onTxFailed: TxFailedCallback = (txResult: SubmittableResult | null) => {
     setSubmitting(false);
+
     if (txResult == null) {
       // Tx cancelled.
 
@@ -90,11 +90,10 @@ const InnerForm = (props: FormProps) => {
   const buildTxParams = () => {
     if (!isValid) return [];
 
-    const rationaleParam = new Text(rationale);
     if (isThread) {
-      return [id, rationaleParam];
+      return [id, rationale];
     } else {
-      return [id, rationaleParam];
+      return [id, rationale];
     }
   };
 
@@ -105,10 +104,9 @@ const InnerForm = (props: FormProps) => {
         <Field component='textarea' id='rationale' name='rationale' disabled={isSubmitting} rows={5} placeholder='Type a retionale here. You can use Markdown.' />
       </LabelledField>
 
-      <LabelledField {...props}>
+      <LabelledField {...props} flex>
         <TxButton
           type='submit'
-          size='large'
           label={'Moderate'}
           isDisabled={!dirty || isSubmitting}
           params={buildTxParams()}
@@ -144,7 +142,7 @@ const InnerForm = (props: FormProps) => {
 const EditForm = withFormik<OuterProps, FormValues>({
 
   // Transform outer props into form values
-  mapPropsToValues: _props => {
+  mapPropsToValues: (_props) => {
     return {
       rationale: ''
     };
@@ -152,7 +150,7 @@ const EditForm = withFormik<OuterProps, FormValues>({
 
   validationSchema: buildSchema,
 
-  handleSubmit: values => {
+  handleSubmit: (values) => {
     // do submitting things
   }
 })(InnerForm);

+ 14 - 11
pioneer/packages/joy-forum/src/ViewReply.tsx

@@ -6,10 +6,10 @@ import { Button, Icon } from 'semantic-ui-react';
 
 import { Post, Category, Thread } from '@joystream/types/forum';
 import { Moderate } from './Moderate';
-import { JoyWarn } from '@polkadot/joy-utils/JoyStatus';
-import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext';
+import { JoyWarn } from '@polkadot/joy-utils/react/components';
+import { useMyAccount } from '@polkadot/joy-utils/react/hooks';
 import { IfIAmForumSudo } from './ForumSudo';
-import { MemberPreview } from '@polkadot/joy-members/MemberPreview';
+import MemberPreview from '@polkadot/joy-utils/react/components/MemberByAccountPreview';
 import { TimeAgoDate, ReplyIdxQueryParam } from './utils';
 
 const HORIZONTAL_PADDING = '1em';
@@ -96,43 +96,46 @@ export const ViewReply = React.forwardRef((props: ViewReplyProps, ref: React.Ref
     if (reply.moderated || thread.moderated || category.archived || category.deleted) {
       return null;
     }
+
     const isMyPost = reply.author_id.eq(myAddress);
+
     return <ReplyFooterActionsRow>
       <div>
         {isMyPost &&
-          <Button onClick={onEdit} size="mini">
-            <Icon name="pencil" />
+          <Button onClick={onEdit} size='mini'>
+            <Icon name='pencil' />
             Edit
           </Button>
         }
 
         <IfIAmForumSudo>
           <Button
-            size="mini"
+            size='mini'
             onClick={() => setShowModerateForm(!showModerateForm)}
           >
             Moderate
           </Button>
         </IfIAmForumSudo>
       </div>
-      <Button onClick={onQuote} size="mini">
-        <Icon name="quote left" />
+      <Button onClick={onQuote} size='mini'>
+        <Icon name='quote left' />
         Quote
       </Button>
     </ReplyFooterActionsRow>;
   };
 
   const replyLinkSearch = new URLSearchParams(search);
+
   replyLinkSearch.set(ReplyIdxQueryParam, reply.nr_in_thread.toString());
 
   return (
-    <ReplyContainer className="ui segment" ref={ref} selected={selected}>
+    <ReplyContainer className='ui segment' ref={ref} selected={selected}>
       <ReplyHeader>
         <ReplyHeaderAuthorRow>
-          <MemberPreview accountId={reply.author_id} />
+          <MemberPreview accountId={reply.author_id} showCouncilBadge showId={false}/>
         </ReplyHeaderAuthorRow>
         <ReplyHeaderDetailsRow>
-          <TimeAgoDate date={reply.created_at.momentDate} id={reply.id} />
+          <TimeAgoDate date={reply.created_at.momentDate} id={reply.id.toString()} />
           <Link to={{ pathname, search: replyLinkSearch.toString() }}>
             #{reply.nr_in_thread.toNumber()}
           </Link>

+ 69 - 74
pioneer/packages/joy-forum/src/ViewThread.tsx

@@ -1,26 +1,27 @@
 import React, { useState, useEffect, useRef } from 'react';
-import { Link } from 'react-router-dom';
+import { Link, RouteComponentProps } from 'react-router-dom';
 import ReactMarkdown from 'react-markdown';
 import styled from 'styled-components';
 import { Table, Button, Label, Icon } from 'semantic-ui-react';
 import BN from 'bn.js';
 
-import { ThreadId, PostId } from '@joystream/types/common';
+import { ThreadId } from '@joystream/types/common';
 import { Category, Thread, Post } from '@joystream/types/forum';
 import { Pagination, RepliesPerPage, CategoryCrumbs, TimeAgoDate, usePagination, useQueryParam, ReplyIdxQueryParam, ReplyEditIdQueryParam } from './utils';
 import { ViewReply } from './ViewReply';
 import { Moderate } from './Moderate';
-import { MutedSpan } from '@polkadot/joy-utils/MutedText';
-import { JoyWarn } from '@polkadot/joy-utils/JoyStatus';
+import { MutedSpan, JoyWarn } from '@polkadot/joy-utils/react/components';
+
 import { withForumCalls } from './calls';
 import { withApi, withMulti } from '@polkadot/react-api';
 import { ApiProps } from '@polkadot/react-api/types';
 import { orderBy } from 'lodash';
-import { bnToStr } from '@polkadot/joy-utils/index';
+import { bnToStr } from '@polkadot/joy-utils/functions/misc';
 import { IfIAmForumSudo } from './ForumSudo';
-import { MemberPreview } from '@polkadot/joy-members/MemberPreview';
+import MemberPreview from '@polkadot/joy-utils/react/components/MemberByAccountPreview';
 import { formatDate } from '@polkadot/joy-utils/functions/date';
 import { NewReply, EditReply } from './EditReply';
+import { useApi } from '@polkadot/react-hooks';
 
 type ThreadTitleProps = {
   thread: Thread;
@@ -29,6 +30,7 @@ type ThreadTitleProps = {
 
 function ThreadTitle (props: ThreadTitleProps) {
   const { thread, className } = props;
+
   return <span className={className}>
     {/* {thread.pinned && <i
       className='star icon'
@@ -68,14 +70,7 @@ const ThreadInfo = styled.span`
 `;
 
 const ThreadInfoMemberPreview = styled(MemberPreview)`
-  && {
-    margin: 0 .2rem;
-
-    .PrefixLabel {
-      color: inherit;
-      margin-right: .2rem;
-    }
-  }
+  margin: 0 .5rem;
 `;
 
 const ReplyEditContainer = styled.div`
@@ -110,7 +105,7 @@ const ThreadPreview: React.FC<ThreadPreviewProps> = ({ thread, repliesCount }) =
         {repliesCount}
       </Table.Cell>
       <Table.Cell>
-        <MemberPreview accountId={thread.author_id} />
+        <MemberPreview accountId={thread.author_id} showCouncilBadge showId={false}/>
       </Table.Cell>
       <Table.Cell>
         {formatDate(thread.created_at.momentDate)}
@@ -144,72 +139,47 @@ function InnerViewThread (props: ViewThreadProps) {
   const parsedSelectedPostIdx = rawSelectedPostIdx ? parseInt(rawSelectedPostIdx) : null;
   const selectedPostIdx = (parsedSelectedPostIdx && !Number.isNaN(parsedSelectedPostIdx)) ? parsedSelectedPostIdx : null;
 
-  const { category, thread, preview = false } = props;
+  const { category, thread, preview = false, api, nextPostId } = props;
 
-  const editedPostId = rawEditedPostId && new PostId(rawEditedPostId);
-
-  if (!thread) {
-    return <em>Loading thread details...</em>;
-  }
-
-  const renderThreadNotFound = () => (
-    preview ? null : <em>Thread not found</em>
-  );
-
-  if (thread.isEmpty) {
-    return renderThreadNotFound();
-  }
+  const editedPostId = rawEditedPostId && api.createType('PostId', rawEditedPostId);
 
   const { id } = thread;
   const totalPostsInThread = thread.num_posts_ever_created.toNumber();
 
-  const changePageAndClearSelectedPost = (page?: number | string) => {
-    setSelectedPostIdx(null);
-    setCurrentPage(page, [ReplyIdxQueryParam]);
-  };
-
-  if (!category) {
-    return <em>{'Thread\'s category was not found.'}</em>;
-  } else if (category.deleted) {
-    return renderThreadNotFound();
-  }
-
-  if (preview) {
-    return <ThreadPreview thread={thread} repliesCount={totalPostsInThread - 1} />;
-  }
-
-  const { api, nextPostId } = props;
   const [loaded, setLoaded] = useState(false);
   const [posts, setPosts] = useState(new Array<Post>());
 
   // fetch posts
   useEffect(() => {
     const loadPosts = async () => {
-      if (!nextPostId || totalPostsInThread === 0) return;
+      if (!nextPostId || totalPostsInThread === 0 || thread.isEmpty) return;
 
-      const newId = (id: number | BN) => new PostId(id);
+      const newId = (id: number | BN) => api.createType('PostId', id);
       const apiCalls: Promise<Post>[] = [];
       let id = newId(1);
+
       while (nextPostId.gt(id)) {
         apiCalls.push(api.query.forum.postById(id) as Promise<Post>);
         id = newId(id.add(newId(1)));
       }
 
       const allPosts = await Promise.all<Post>(apiCalls);
-      const postsInThisThread = allPosts.filter(item =>
+      const postsInThisThread = allPosts.filter((item) =>
         !item.isEmpty &&
         item.thread_id.eq(thread.id)
       );
       const sortedPosts = orderBy(
         postsInThisThread,
-        [x => x.nr_in_thread.toNumber()],
+        [(x) => x.nr_in_thread.toNumber()],
         ['asc']
       );
 
       // initialize refs for posts
       postsRefs.current = sortedPosts.reduce((acc, reply) => {
         const refKey = reply.nr_in_thread.toNumber();
+
         acc[refKey] = React.createRef();
+
         return acc;
       }, postsRefs.current);
 
@@ -217,7 +187,7 @@ function InnerViewThread (props: ViewThreadProps) {
       setLoaded(true);
     };
 
-    loadPosts();
+    void loadPosts();
   }, [bnToStr(thread.id), bnToStr(nextPostId)]);
 
   // handle selected post
@@ -225,14 +195,17 @@ function InnerViewThread (props: ViewThreadProps) {
     if (!selectedPostIdx) return;
 
     const selectedPostPage = Math.ceil(selectedPostIdx / RepliesPerPage);
+
     if (currentPage !== selectedPostPage) {
       setCurrentPage(selectedPostPage);
     }
 
     if (!loaded) return;
+
     if (selectedPostIdx > posts.length) {
       // eslint-disable-next-line no-console
       console.warn(`Tried to open nonexistent reply with idx: ${selectedPostIdx}`);
+
       return;
     }
 
@@ -255,9 +228,33 @@ function InnerViewThread (props: ViewThreadProps) {
     const minIdx = (currentPage - 1) * RepliesPerPage;
     const maxIdx = minIdx + RepliesPerPage - 1;
     const postsToDisplay = posts.filter((_id, i) => i >= minIdx && i <= maxIdx);
+
     setDisplayedPosts(postsToDisplay);
   }, [loaded, posts, currentPage]);
 
+  const renderThreadNotFound = () => (
+    preview ? null : <em>Thread not found</em>
+  );
+
+  if (thread.isEmpty) {
+    return renderThreadNotFound();
+  }
+
+  if (!category) {
+    return <em>{'Thread\'s category was not found.'}</em>;
+  } else if (category.deleted) {
+    return renderThreadNotFound();
+  }
+
+  if (preview) {
+    return <ThreadPreview thread={thread} repliesCount={totalPostsInThread - 1} />;
+  }
+
+  const changePageAndClearSelectedPost = (page?: number | string) => {
+    setSelectedPostIdx(null);
+    setCurrentPage(page, [ReplyIdxQueryParam]);
+  };
+
   const scrollToReplyForm = () => {
     if (!replyFormRef.current) return;
     replyFormRef.current.scrollIntoView();
@@ -277,11 +274,12 @@ function InnerViewThread (props: ViewThreadProps) {
     if (!editedPostId) {
       // eslint-disable-next-line no-console
       console.error('editedPostId not set!');
+
       return;
     }
 
     const updatedPost = await api.query.forum.postById(editedPostId) as Post;
-    const updatedPosts = posts.map(post => post.id.eq(editedPostId) ? updatedPost : post);
+    const updatedPosts = posts.map((post) => post.id.eq(editedPostId) ? updatedPost : post);
 
     setPosts(updatedPosts);
     clearEditedPost();
@@ -340,9 +338,10 @@ function InnerViewThread (props: ViewThreadProps) {
     if (thread.moderated || category.archived || category.deleted) {
       return null;
     }
+
     return <span className='JoyInlineActions'>
       <Button onClick={onThreadReplyClick}>
-        <Icon name="reply" />
+        <Icon name='reply' />
         Reply
       </Button>
 
@@ -384,9 +383,9 @@ function InnerViewThread (props: ViewThreadProps) {
       </h1>
       <ThreadInfoAndActions>
         <ThreadInfo>
-          Created
-          <ThreadInfoMemberPreview accountId={thread.author_id} inline prefixLabel="by" />
-          <TimeAgoDate date={thread.created_at.momentDate} id="thread" />
+          Created by
+          <ThreadInfoMemberPreview accountId={thread.author_id} size='small' showId={false}/>
+          <TimeAgoDate date={thread.created_at.momentDate} id='thread' />
         </ThreadInfo>
         {renderActions()}
       </ThreadInfoAndActions>
@@ -424,29 +423,23 @@ export const ViewThread = withMulti(
   )
 );
 
-type ViewThreadByIdProps = ApiProps & {
-  match: {
-    params: {
-      id: string;
-    };
-  };
-};
+type ViewThreadByIdProps = RouteComponentProps<{ id: string }>;
 
-function InnerViewThreadById (props: ViewThreadByIdProps) {
-  const { api, match: { params: { id } } } = props;
+export function ViewThreadById (props: ViewThreadByIdProps) {
+  const { api } = useApi();
+  const { match: { params: { id } } } = props;
+  const [loaded, setLoaded] = useState(false);
+  const [thread, setThread] = useState(api.createType('Thread', {}));
+  const [category, setCategory] = useState(api.createType('Category', {}));
+
+  let threadId: ThreadId | undefined;
 
-  let threadId: ThreadId;
   try {
-    threadId = new ThreadId(id);
+    threadId = api.createType('ThreadId', id);
   } catch (err) {
     console.log('Failed to parse thread id form URL');
-    return <em>Invalid thread ID: {id}</em>;
   }
 
-  const [loaded, setLoaded] = useState(false);
-  const [thread, setThread] = useState(Thread.newEmpty());
-  const [category, setCategory] = useState(Category.newEmpty());
-
   useEffect(() => {
     const loadThreadAndCategory = async () => {
       if (!threadId) return;
@@ -459,9 +452,13 @@ function InnerViewThreadById (props: ViewThreadByIdProps) {
       setLoaded(true);
     };
 
-    loadThreadAndCategory();
+    void loadThreadAndCategory();
   }, [id]);
 
+  if (threadId === undefined) {
+    return <em>Invalid thread ID: {id}</em>;
+  }
+
   // console.log({ threadId: id, page });
 
   if (!loaded) {
@@ -478,5 +475,3 @@ function InnerViewThreadById (props: ViewThreadByIdProps) {
 
   return <ViewThread id={threadId} category={category} thread={thread} />;
 }
-
-export const ViewThreadById = withApi(InnerViewThreadById);

+ 18 - 15
pioneer/packages/joy-forum/src/calls.tsx

@@ -1,14 +1,12 @@
 
 import React from 'react';
 import { ApiProps, SubtractProps } from '@polkadot/react-api/types';
-import { Options } from '@polkadot/react-api/with/types';
-import { withApi, withCall as withSubstrateCall } from '@polkadot/react-api';
-import { Option } from '@polkadot/types/codec';
-import { AccountId } from '@polkadot/types/interfaces';
+import { Options } from '@polkadot/react-api/hoc/types';
+import { withApi, withCall as withSubstrateCall } from '@polkadot/react-api/hoc';
 import { u64 } from '@polkadot/types';
-import { Constructor } from '@polkadot/types/types';
-import { Category, Thread, Reply } from '@joystream/types/forum';
+import { InterfaceTypes } from '@polkadot/types/types/registry';
 import { useForum, ForumState } from './Context';
+import { createType } from '@joystream/types';
 
 type Call = string | [string, Options];
 
@@ -18,18 +16,20 @@ 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 getReactValue = (state: ForumState, endpoint: string, paramValue: any) => {
+  function getEntityById<T extends keyof InterfaceTypes>
+  (mapName: EntityMapName, type: T): InterfaceTypes[T] {
     const id = (paramValue as u64).toNumber();
     const entity = state[mapName].get(id);
-    return new constructor(entity);
-  };
+
+    return createType(type, entity);
+  }
 
   switch (endpoint) {
-    case 'forumSudo': return new Option<AccountId>('AccountId', state.sudo);
-    case 'categoryById': return getEntityById(endpoint, Category);
-    case 'threadById': return getEntityById(endpoint, Thread);
-    case 'replyById': return getEntityById(endpoint, Reply);
+    case 'forumSudo': return createType('Option<AccountId>', state.sudo);
+    case 'categoryById': return getEntityById(endpoint, 'Category');
+    case 'threadById': return getEntityById(endpoint, 'Thread');
+    case 'replyById': return getEntityById(endpoint, 'Reply');
     default: throw new Error('Unknown endpoint for Forum storage');
   }
 };
@@ -38,13 +38,14 @@ function withReactCall<P extends ApiProps> (endpoint: string, { paramName, propN
   return (Inner: React.ComponentType<ApiProps>): React.ComponentType<SubtractProps<P, ApiProps>> => {
     const SetProp = (props: P) => {
       const { state } = useForum();
-      const paramValue = paramName ? (props as any)[paramName] : undefined;
+      const paramValue = paramName ? (props as Record<string, unknown>)[paramName] : undefined;
       const propValue = getReactValue(state, endpoint, paramValue);
       const _propName = propName || endpoint;
       const _props = {
         ...props,
         [_propName]: propValue
       };
+
       return <Inner {..._props} />;
     };
 
@@ -56,10 +57,12 @@ function withForumCall<P extends ApiProps> (endpoint: string, opts: Options = {}
   if (!opts.propName) {
     opts.propName = endpoint;
   }
+
   if (storage === 'react') {
     return withReactCall(endpoint, opts);
   } else {
     endpoint = 'query.forum.' + endpoint;
+
     return withSubstrateCall(endpoint, opts);
   }
 }

+ 10 - 13
pioneer/packages/joy-forum/src/index.tsx

@@ -3,42 +3,39 @@ import React from 'react';
 import { Route, Switch } from 'react-router';
 import styled from 'styled-components';
 
-import { AppProps, I18nProps } from '@polkadot/react-components/types';
-
-import './index.css';
+import { RouteProps as AppMainRouteProps } from '@polkadot/apps-routing/types';
+import { I18nProps } from '@polkadot/react-components/types';
 
+import style from './style';
 import translate from './translate';
 import { ForumProvider } from './Context';
 import { ForumSudoProvider } from './ForumSudo';
-import { NewSubcategory, EditCategory } from './EditCategory';
+import { NewSubcategory, NewCategory, EditCategory } from './EditCategory';
 import { NewThread, EditThread } from './EditThread';
 import { CategoryList, ViewCategoryById } from './CategoryList';
 import { ViewThreadById } from './ViewThread';
 import { LegacyPagingRedirect } from './LegacyPagingRedirect';
 import ForumRoot from './ForumRoot';
 
-const ForumContentWrapper = styled.main`
-  padding-top: 1.5rem;
-`;
+const ForumMain = styled.main`${style}`;
 
-type Props = AppProps & I18nProps & {};
+type Props = AppMainRouteProps & I18nProps;
 
 class App extends React.PureComponent<Props> {
   render () {
     const { basePath } = this.props;
+
     return (
       <ForumProvider>
         <ForumSudoProvider>
-          <ForumContentWrapper className='forum--App'>
+          <ForumMain className='forum--App'>
             <Switch>
+              <Route path={`${basePath}/categories/new`} component={NewCategory} />
               {/* routes for handling legacy format of forum paging within the routing path */}
               {/* translate page param to search query */}
               <Route path={`${basePath}/categories/:id/page/:page`} component={LegacyPagingRedirect} />
               <Route path={`${basePath}/threads/:id/page/:page`} component={LegacyPagingRedirect} />
 
-              {/* <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} />
@@ -50,7 +47,7 @@ class App extends React.PureComponent<Props> {
 
               <Route component={ForumRoot} />
             </Switch>
-          </ForumContentWrapper>
+          </ForumMain>
         </ForumSudoProvider>
       </ForumProvider>
     );

+ 12 - 8
pioneer/packages/joy-forum/src/index.css → pioneer/packages/joy-forum/src/style.ts

@@ -1,4 +1,8 @@
-.forum--App {
+import { css } from 'styled-components';
+
+export default css`
+  padding-top: 1.5rem;
+
   .ui.segment {
     background-color: #fff;
   }
@@ -11,13 +15,13 @@
       margin-right: 1rem;
     }
   }
-}
-
-.EditEntityBox {
-  width: 100%;
-  max-width: 600px;
 
-  .EditEntityForm {
+  .EditEntityBox {
     width: 100%;
+    max-width: 600px;
+
+    .EditEntityForm {
+      width: 100%;
+    }
   }
-}
+`;

+ 14 - 7
pioneer/packages/joy-forum/src/utils.tsx

@@ -54,9 +54,10 @@ function InnerCategoryCrumb (p: CategoryCrumbsProps) {
   if (category) {
     try {
       const url = `/forum/categories/${category.id.toString()}`;
+
       return <>
         {category.parent_id ? <CategoryCrumb categoryId={category.parent_id} /> : null}
-        <Breadcrumb.Divider icon="right angle" />
+        <Breadcrumb.Divider icon='right angle' />
         <Breadcrumb.Section as={Link} to={url}>{category.title}</Breadcrumb.Section>
       </>;
     } catch (err) {
@@ -80,9 +81,10 @@ function InnerThreadCrumb (p: CategoryCrumbsProps) {
   if (thread) {
     try {
       const url = `/forum/threads/${thread.id.toString()}`;
+
       return <>
         <CategoryCrumb categoryId={thread.category_id} />
-        <Breadcrumb.Divider icon="right angle" />
+        <Breadcrumb.Divider icon='right angle' />
         <Breadcrumb.Section as={Link} to={url}>{thread.title}</Breadcrumb.Section>
       </>;
     } catch (err) {
@@ -113,8 +115,8 @@ export const CategoryCrumbs = ({ categoryId, threadId, root }: CategoryCrumbsPro
       <Breadcrumb.Section>Forum</Breadcrumb.Section>
       {!root && (
         <>
-          <Breadcrumb.Divider icon="right angle" />
-          <Breadcrumb.Section as={Link} to="/forum">Top categories</Breadcrumb.Section>
+          <Breadcrumb.Divider icon='right angle' />
+          <Breadcrumb.Section as={Link} to='/forum'>Top categories</Breadcrumb.Section>
           <CategoryCrumb categoryId={categoryId} />
           <ThreadCrumb threadId={threadId} />
         </>
@@ -125,7 +127,7 @@ export const CategoryCrumbs = ({ categoryId, threadId, root }: CategoryCrumbsPro
 
 type TimeAgoDateProps = {
   date: moment.Moment;
-  id: any;
+  id: string | number;
 };
 
 export const TimeAgoDate: React.FC<TimeAgoDateProps> = ({ date, id }) => (
@@ -133,7 +135,7 @@ export const TimeAgoDate: React.FC<TimeAgoDateProps> = ({ date, id }) => (
     <span data-tip data-for={`${id}-date-tooltip`}>
       {date.fromNow()}
     </span>
-    <Tooltip id={`${id}-date-tooltip`} place="top" effect="solid">
+    <Tooltip id={`${id}-date-tooltip`} place='top' effect='solid'>
       {date.toLocaleString()}
     </Tooltip>
   </>
@@ -164,6 +166,7 @@ export const useQueryParam = (queryParam: string): QueryReturnType => {
   useEffect(() => {
     const params = new URLSearchParams(search);
     const paramValue = params.get(queryParam);
+
     if (paramValue !== value) {
       setValue(paramValue);
     }
@@ -171,6 +174,7 @@ export const useQueryParam = (queryParam: string): QueryReturnType => {
 
   const setParam: QuerySetValueType = (rawValue, paramsToReset = []) => {
     let parsedValue: string | null;
+
     if (!rawValue && rawValue !== 0) {
       parsedValue = null;
     } else {
@@ -178,13 +182,14 @@ export const useQueryParam = (queryParam: string): QueryReturnType => {
     }
 
     const params = new URLSearchParams(search);
+
     if (parsedValue) {
       params.set(queryParam, parsedValue);
     } else {
       params.delete(queryParam);
     }
 
-    paramsToReset.forEach(p => params.delete(p));
+    paramsToReset.forEach((p) => params.delete(p));
 
     setValue(parsedValue);
     history.push({ pathname, search: params.toString() });
@@ -197,8 +202,10 @@ export const usePagination = (): [number, QuerySetValueType] => {
   const [rawCurrentPage, setCurrentPage] = useQueryParam(PagingQueryParam);
 
   let currentPage = 1;
+
   if (rawCurrentPage) {
     const parsedPage = Number.parseInt(rawCurrentPage);
+
     if (!Number.isNaN(parsedPage)) {
       currentPage = parsedPage;
     } else {

+ 6 - 2
pioneer/packages/joy-forum/src/validation.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
-import { withMulti } from '@polkadot/react-api/with';
+import { withMulti } from '@polkadot/react-api/hoc';
 import { InputValidationLengthConstraint } from '@joystream/types/common';
 import { withForumCalls } from './calls';
 import { componentName } from '@polkadot/joy-utils/react/helpers';
@@ -28,14 +28,18 @@ function waitForRequiredConstraints (
   return function (Component: React.ComponentType<any>) {
     const ResultComponent: React.FunctionComponent<ValidationProps> = (props: ValidationProps) => {
       const nonEmptyProps = requiredConstraintNames
-        .filter(name => props[name] !== undefined)
+        .filter((name) => props[name] !== undefined)
         .length;
+
       if (nonEmptyProps !== requiredConstraintNames.length) {
         return <em>Loading validation constraints...</em>;
       }
+
       return <Component {...props} />;
     };
+
     ResultComponent.displayName = `waitForRequiredConstraints(${componentName(Component)})`;
+
     return ResultComponent;
   };
 }

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


+ 2 - 0
pioneer/packages/joy-media/aplayer.d.ts

@@ -0,0 +1,2 @@
+// No offical definitions available
+declare module 'react-aplayer';

+ 2 - 0
pioneer/packages/joy-media/dplayer.d.ts

@@ -0,0 +1,2 @@
+// No offical definitions available
+declare module 'react-dplayer';

+ 4 - 4
pioneer/packages/joy-media/package.json

@@ -7,14 +7,14 @@
   "author": "Joystream contributors",
   "maintainers": [],
   "dependencies": {
-    "@babel/runtime": "^7.7.1",
+    "@babel/runtime": "^7.10.5",
     "@polkadot/joy-utils": "^0.1.1",
-    "@polkadot/react-components": "0.37.0-beta.63",
-    "@polkadot/react-query": "0.37.0-beta.63",
+    "@polkadot/react-components": "0.51.1",
+    "@polkadot/react-query": "0.51.1",
     "@types/mime-types": "^2.1.0",
     "@types/react-beautiful-dnd": "^11.0.3",
     "aplayer": "^1.10.1",
-    "dplayer": "^1.25.0",
+    "dplayer": "1.25.0",
     "ipfs-only-hash": "^1.0.2",
     "iso-639-1": "^2.1.0",
     "lodash": "^4.17.11",

+ 33 - 7
pioneer/packages/joy-media/src/DiscoveryProvider.tsx

@@ -7,8 +7,9 @@ import { Vec } from '@polkadot/types';
 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 { JoyInfo } from '@polkadot/joy-utils/react/components';
 import { componentName } from '@polkadot/joy-utils/react/helpers';
+import { isObjectWithProperties } from '@polkadot/joy-utils/functions/misc';
 
 export type BootstrapNodes = {
   bootstrapNodes?: Url[];
@@ -26,9 +27,11 @@ export type DiscoveryProviderProps = {
 // return string Url with last `/` removed
 function normalizeUrl (url: string | Url): string {
   const st: string = url.toString();
+
   if (st.endsWith('/')) {
     return st.substring(0, st.length - 1);
   }
+
   return st.toString();
 }
 
@@ -39,7 +42,7 @@ type ProviderStats = {
 }
 
 function newDiscoveryProvider ({ bootstrapNodes }: BootstrapNodes): DiscoveryProvider {
-  const stats: Map<string, ProviderStats> = new Map();
+  const stats = new Map<string, ProviderStats>();
 
   const resolveAssetEndpoint = async (storageProvider: StorageProviderId, contentId?: string, cancelToken?: CancelToken) => {
     const providerKey = storageProvider.toString();
@@ -65,23 +68,41 @@ function newDiscoveryProvider ({ bootstrapNodes }: BootstrapNodes): DiscoveryPro
         try {
           console.log(`Resolving ${providerKey} using ${discoveryUrl}`);
 
-          const serviceInfo = await axios.get(serviceInfoQuery, { cancelToken }) as any;
+          const serviceInfo = await axios.get<unknown>(serviceInfoQuery, { cancelToken });
 
           if (!serviceInfo) {
             continue;
           }
 
+          const { data } = serviceInfo;
+
+          if (!isObjectWithProperties(data, 'serialized') || typeof data.serialized !== 'string') {
+            continue;
+          }
+
+          const dataParsed = JSON.parse(data.serialized) as unknown;
+
+          if (
+            !isObjectWithProperties(dataParsed, 'asset') ||
+            !isObjectWithProperties(dataParsed.asset, 'endpoint') ||
+            typeof dataParsed.asset.endpoint !== 'string'
+          ) {
+            continue;
+          }
+
           stats.set(providerKey, {
-            assetApiEndpoint: normalizeUrl(JSON.parse(serviceInfo.data.serialized).asset.endpoint),
+            assetApiEndpoint: normalizeUrl(dataParsed.asset.endpoint),
             unreachableReports: 0,
             resolvedAt: Date.now()
           });
           break;
         } catch (err) {
           console.log(err);
+
           if (axios.isCancel(err)) {
             throw err;
           }
+
           continue;
         }
       }
@@ -101,6 +122,7 @@ function newDiscoveryProvider ({ bootstrapNodes }: BootstrapNodes): DiscoveryPro
   const reportUnreachable = (provider: StorageProviderId) => {
     const key = provider.toString();
     const stat = stats.get(key);
+
     if (stat) {
       stat.unreachableReports = stat.unreachableReports + 1;
     }
@@ -111,7 +133,7 @@ function newDiscoveryProvider ({ bootstrapNodes }: BootstrapNodes): DiscoveryPro
 
 const DiscoveryProviderContext = createContext<DiscoveryProvider>(undefined as unknown as DiscoveryProvider);
 
-export const DiscoveryProviderProvider = (props: React.PropsWithChildren<{}>) => {
+export const DiscoveryProviderProvider = (props: React.PropsWithChildren<Record<any, unknown>>) => {
   const api: ApiProps = useContext(ApiContext);
   const [provider, setProvider] = useState<DiscoveryProvider | undefined>();
   const [loaded, setLoaded] = useState<boolean | undefined>();
@@ -122,12 +144,13 @@ export const DiscoveryProviderProvider = (props: React.PropsWithChildren<{}>) =>
 
       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');
     };
 
-    load();
+    void load();
   }, [loaded]);
 
   if (!api || !api.isApiReady) {
@@ -157,8 +180,9 @@ export const useDiscoveryProvider = () =>
   useContext(DiscoveryProviderContext);
 
 export function withDiscoveryProvider (Component: React.ComponentType<DiscoveryProviderProps>) {
-  const ResultComponent: React.FunctionComponent<{}> = (props: React.PropsWithChildren<{}>) => {
+  const ResultComponent: React.FunctionComponent<Record<any, unknown>> = (props: React.PropsWithChildren<Record<any, unknown>>) => {
     const discoveryProvider = useDiscoveryProvider();
+
     if (!discoveryProvider) {
       return <JoyInfo title={'Please wait...'}>Loading discovery provider.</JoyInfo>;
     }
@@ -169,6 +193,8 @@ export function withDiscoveryProvider (Component: React.ComponentType<DiscoveryP
       </Component>
     );
   };
+
   ResultComponent.displayName = `withDiscoveryProvider(${componentName(Component)})`;
+
   return ResultComponent;
 }

+ 4 - 3
pioneer/packages/joy-media/src/IterableFile.ts

@@ -32,10 +32,11 @@ export class IterableFile implements AsyncIterable<Buffer> {
 
   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.onload = (e) => {
+        e.target?.result && resolve(typeof e.target.result === 'string' ? Buffer.from(e.target.result) : Buffer.from(e.target.result));
+        e.target?.error && reject(e.target.error);
       };
+
       this.reader.readAsArrayBuffer(blob);
     });
   }

+ 15 - 10
pioneer/packages/joy-media/src/MediaView.tsx

@@ -1,9 +1,12 @@
 import React, { useState, useEffect } from 'react';
 import { MediaTransport } from './transport';
 import { MemberId } from '@joystream/types/members';
-import { useMyMembership } from '@polkadot/joy-utils/MyMembershipContext';
+import { useMyMembership } from '@polkadot/joy-utils/react/hooks';
 import { useTransportContext } from './TransportContext';
-import { withMembershipRequired } from '@polkadot/joy-utils/MyAccount';
+import { withMembershipRequired } from '@polkadot/joy-utils/react/hocs/guards';
+import { useApi } from '@polkadot/react-hooks';
+import { ApiPromise } from '@polkadot/api';
+import { isObjectWithProperties } from '@polkadot/joy-utils/functions/misc';
 
 type InitialPropsWithMembership<A> = A & {
   myAddress?: string;
@@ -12,6 +15,7 @@ type InitialPropsWithMembership<A> = A & {
 
 type ResolverProps<A> = InitialPropsWithMembership<A> & {
   transport: MediaTransport;
+  api: ApiPromise;
 }
 
 type BaseProps<A, B> = {
@@ -29,28 +33,29 @@ type BaseProps<A, B> = {
   membersOnly?: boolean;
 }
 
-function serializeTrigger (val: any): any {
+function serializeTrigger (val: unknown): number | boolean | string | undefined {
   if (['number', 'boolean', 'string'].includes(typeof val)) {
-    return val;
-  } else if (typeof val === 'object' && typeof val.toString === 'function') {
-    return val.toString();
+    return val as number | boolean | string;
+  } else if (isObjectWithProperties(val, 'toString') && typeof val.toString === 'function') {
+    return val.toString() as string;
   } else {
     return undefined;
   }
 }
 
-export function MediaView<A = {}, B = {}> (baseProps: BaseProps<A, B>) {
+export function MediaView<A extends Record<string, unknown> = Record<string, unknown>, B extends Record<string, unknown> = Record<string, unknown>> (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 { api } = useApi();
+    const resolverProps = { ...initialProps, transport, api, myAddress, myMemberId };
 
     const [resolvedProps, setResolvedProps] = useState({} as B);
     const [propsResolved, setPropsResolved] = useState(false);
 
-    const initialDeps = triggers.map(propName => serializeTrigger(initialProps[propName]));
+    const initialDeps = triggers.map((propName) => serializeTrigger(initialProps[propName]));
     const rerenderDeps = [...initialDeps, myAddress];
 
     useEffect(() => {
@@ -70,7 +75,7 @@ export function MediaView<A = {}, B = {}> (baseProps: BaseProps<A, B>) {
       if (!transport) {
         console.error('Transport is not defined');
       } else {
-        doResolveProps();
+        void doResolveProps();
       }
     }, rerenderDeps);
 

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

@@ -10,12 +10,12 @@ export const TransportContext = createContext<MediaTransport>(undefined as unkno
 export const useTransportContext = () =>
   useContext(TransportContext);
 
-export const MockTransportProvider = (props: React.PropsWithChildren<{}>) =>
+export const MockTransportProvider = (props: React.PropsWithChildren<Record<any, unknown>>) =>
   <TransportContext.Provider value={new MockTransport()}>
     {props.children}
   </TransportContext.Provider>;
 
-export const SubstrateTransportProvider = (props: React.PropsWithChildren<{}>) => {
+export const SubstrateTransportProvider = (props: React.PropsWithChildren<Record<any, unknown>>) => {
   const api: ApiProps = useContext(ApiContext);
   const [transport, setTransport] = useState<SubstrateTransport>();
   const [loaded, setLoaded] = useState<boolean>();

+ 73 - 30
pioneer/packages/joy-media/src/Upload.tsx

@@ -1,10 +1,11 @@
 import React from 'react';
 import BN from 'bn.js';
-import axios, { CancelTokenSource } from 'axios';
+import axios, { CancelTokenSource, AxiosError, AxiosRequestConfig } from 'axios';
 import { History } from 'history';
 import { Progress, Message } from 'semantic-ui-react';
 
-import { InputFileAsync } from '@polkadot/react-components/index';
+import { registry } from '@joystream/types';
+import { InputFileAsync, TxButton, JoyInfo, Loading } from '@polkadot/joy-utils/react/components';
 import { ApiProps } from '@polkadot/react-api/types';
 import { I18nProps } from '@polkadot/react-components/types';
 import { SubmittableResult } from '@polkadot/api';
@@ -15,15 +16,17 @@ import { formatNumber } from '@polkadot/util';
 import translate from './translate';
 import { fileNameWoExt } from './utils';
 import { ContentId, DataObject } from '@joystream/types/media';
-import { withOnlyMembers, MyAccountProps } from '@polkadot/joy-utils/MyAccount';
+import { MyAccountProps } from '@polkadot/joy-utils/react/hocs/accounts';
+import { withOnlyMembers } from '@polkadot/joy-utils/react/hocs/guards';
 import { DiscoveryProviderProps, withDiscoveryProvider } from './DiscoveryProvider';
-import TxButton from '@polkadot/joy-utils/TxButton';
+
 import IpfsHash from 'ipfs-only-hash';
 import { ChannelId } from '@joystream/types/content-working-group';
 import { EditVideoView } from './upload/EditVideo.view';
-import { JoyInfo } from '@polkadot/joy-utils/JoyStatus';
+
 import { IterableFile } from './IterableFile';
 import { StorageProviderId } from '@joystream/types/working-group';
+import { normalizeError, isObjectWithProperties } from '@polkadot/joy-utils/functions/misc';
 
 const MAX_FILE_SIZE_MB = 500;
 const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
@@ -39,13 +42,14 @@ type Props = ApiProps & I18nProps & DiscoveryProviderProps & MyAccountProps & {
 };
 
 type State = {
-  error?: any;
+  error?: string;
   file?: File;
   computingHash: boolean;
   ipfs_cid?: string;
   newContentId: ContentId;
   discovering: boolean;
   uploading: boolean;
+  sendingTx: boolean;
   progress: number;
   cancelSource: CancelTokenSource;
 };
@@ -55,9 +59,10 @@ const defaultState = (): State => ({
   file: undefined,
   computingHash: false,
   ipfs_cid: undefined,
-  newContentId: ContentId.generate(),
+  newContentId: ContentId.generate(registry),
   discovering: false,
   uploading: false,
+  sendingTx: false,
   progress: 0,
   cancelSource: axios.CancelToken.source()
 });
@@ -72,6 +77,7 @@ class Upload extends React.PureComponent<Props, State> {
     });
 
     const { cancelSource } = this.state;
+
     cancelSource.cancel('unmounting');
   }
 
@@ -84,21 +90,21 @@ class Upload extends React.PureComponent<Props, State> {
   }
 
   private renderContent () {
-    const { error, uploading, discovering, computingHash } = this.state;
+    const { error, uploading, discovering, computingHash, sendingTx } = this.state;
 
-    if (error) return this.renderError();
+    if (error) return this.renderError(error);
     else if (discovering) return this.renderDiscovering();
     else if (uploading) return this.renderUploading();
     else if (computingHash) return this.renderComputingHash();
+    else if (sendingTx) return this.renderSendingTx();
     else return this.renderFileInput();
   }
 
-  private renderError () {
-    const { error } = this.state;
+  private renderError (error: string) {
     return (
       <Message error className='JoyMainStatus'>
         <Message.Header>Failed to upload your file</Message.Header>
-        <p>{error.toString()}</p>
+        <p>{error}</p>
         <button className='ui button' onClick={this.resetForm}>Start over</button>
       </Message>
     );
@@ -106,6 +112,7 @@ class Upload extends React.PureComponent<Props, State> {
 
   private resetForm = () => {
     const { cancelSource } = this.state;
+
     this.setState({
       ...defaultState(),
       cancelSource
@@ -114,16 +121,17 @@ class Upload extends React.PureComponent<Props, State> {
 
   private renderUploading () {
     const { file, newContentId, progress, error } = this.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 } }, api } = this.props;
 
     return <div style={{ width: '100%' }}>
       {this.renderProgress()}
       {success &&
         <EditVideoView
-          channelId={new ChannelId(channelId)}
+          channelId={api.createType('ChannelId', channelId)}
           contentId={newContentId}
           fileName={fileNameWoExt(file.name)}
           history={history}
@@ -132,6 +140,10 @@ class Upload extends React.PureComponent<Props, State> {
     </div>;
   }
 
+  private renderSendingTx () {
+    return <JoyInfo title='Please wait...'><Loading text='Waiting for the transaction confirmation...' /></JoyInfo>;
+  }
+
   private renderDiscovering () {
     return <JoyInfo title={'Please wait...'}>Contacting storage provider.</JoyInfo>;
   }
@@ -142,6 +154,7 @@ class Upload extends React.PureComponent<Props, State> {
     const success = !error && progress >= 100;
 
     let label = '';
+
     if (active) {
       label = 'Your file is uploading. Please keep this page open until it\'s done.';
     } else if (success) {
@@ -165,7 +178,7 @@ class Upload extends React.PureComponent<Props, State> {
 
     return <div className='UploadSelectForm'>
       <InputFileAsync
-        label=""
+        label=''
         withLabel={false}
         className={`UploadInputFile ${file_name ? 'FileSelected' : ''}`}
         placeholder={
@@ -184,12 +197,16 @@ class Upload extends React.PureComponent<Props, State> {
       />
       {file_name && <div className='UploadButtonBox'>
         <TxButton
-          size='large'
           label={'Upload'}
           isDisabled={!file_name}
           tx={'dataDirectory.addContent'}
           params={this.buildTxParams()}
-          txSuccessCb={this.onDataObjectCreated}
+          onClick={(sendTx) => {
+            this.setState({ sendingTx: true });
+            sendTx();
+          }}
+          txSuccessCb={ this.onDataObjectCreated }
+          txFailedCb={() => { this.setState({ sendingTx: false }); }}
         />
       </div>}
     </div>;
@@ -205,7 +222,7 @@ class Upload extends React.PureComponent<Props, State> {
       });
     } else {
       this.setState({ file, computingHash: true });
-      this.startComputingHash();
+      void this.startComputingHash();
     }
   }
 
@@ -218,7 +235,8 @@ class Upload extends React.PureComponent<Props, State> {
 
     try {
       const iterableFile = new IterableFile(file, { chunkSize: 65535 });
-      const ipfs_cid = await IpfsHash.of(iterableFile);
+      // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call
+      const ipfs_cid = (await IpfsHash.of(iterableFile)) as string;
 
       this.hashComputationComplete(ipfs_cid);
     } catch (err) {
@@ -244,27 +262,31 @@ class Upload extends React.PureComponent<Props, State> {
 
   private buildTxParams = () => {
     const { file, newContentId, ipfs_cid } = this.state;
+
     if (!file || !ipfs_cid) return [];
 
     // TODO get corresponding data type id based on file content
     const dataObjectTypeId = new BN(1);
     const { myMemberId } = this.props;
+
     return [myMemberId, newContentId, dataObjectTypeId, new BN(file.size), ipfs_cid];
   }
 
   private onDataObjectCreated = async (_txResult: SubmittableResult) => {
-    this.setState({ discovering: true });
+    this.setState({ sendingTx: false, discovering: true });
 
     const { api } = this.props;
     const { newContentId } = this.state;
     let dataObject: Option<DataObject>;
+
     try {
       dataObject = await api.query.dataDirectory.dataObjectByContentId(newContentId) as Option<DataObject>;
     } catch (err) {
       this.setState({
-        error: err,
+        error: normalizeError(err),
         discovering: false
       });
+
       return;
     }
 
@@ -276,10 +298,11 @@ class Upload extends React.PureComponent<Props, State> {
 
     if (dataObject.isSome) {
       const storageProvider = dataObject.unwrap().liaison;
-      this.uploadFileTo(storageProvider);
+
+      void this.uploadFileTo(storageProvider);
     } else {
       this.setState({
-        error: new Error('No Storage Provider assigned to process upload'),
+        error: 'No Storage Provider assigned to process upload',
         discovering: false
       });
     }
@@ -287,16 +310,18 @@ class Upload extends React.PureComponent<Props, State> {
 
   private uploadFileTo = async (storageProvider: StorageProviderId) => {
     const { file, newContentId, cancelSource } = this.state;
+
     if (!file || !file.size) {
       this.setState({
-        error: new Error('No file to upload!'),
+        error: 'No file to upload!',
         discovering: false
       });
+
       return;
     }
 
     const contentId = newContentId.encode();
-    const config = {
+    const config: AxiosRequestConfig = {
       headers: {
         // TODO uncomment this once the issue fixed:
         // https://github.com/Joystream/storage-node-joystream/issues/16
@@ -304,8 +329,17 @@ class Upload extends React.PureComponent<Props, State> {
         'Content-Type': '' // <-- this is a temporary hack
       },
       cancelToken: cancelSource.token,
-      onUploadProgress: (progressEvent: any) => {
+      onUploadProgress: (progressEvent: unknown) => {
+        if (
+          !isObjectWithProperties(progressEvent, 'loaded', 'total') ||
+          typeof progressEvent.loaded !== 'number' ||
+          typeof progressEvent.total !== 'number'
+        ) {
+          return;
+        }
+
         const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
+
         this.setState({
           progress: percentCompleted
         });
@@ -314,11 +348,12 @@ class Upload extends React.PureComponent<Props, State> {
 
     const { discoveryProvider } = this.props;
     let url: string;
+
     try {
       url = await discoveryProvider.resolveAssetEndpoint(storageProvider, contentId, cancelSource.token);
     } catch (err) {
       return this.setState({
-        error: new Error(`Failed to contact storage provider: ${err.message}`),
+        error: `Failed to contact storage provider: ${normalizeError(err)}`,
         discovering: false
       });
     }
@@ -335,12 +370,20 @@ class Upload extends React.PureComponent<Props, State> {
 
     try {
       await axios.put<{ message: string }>(url, file, config);
-    } catch (err) {
-      this.setState({ progress: 0, error: err, uploading: false });
+    } catch (e) {
+      const err = e as unknown;
+
+      this.setState({ progress: 0, error: normalizeError(err), uploading: false });
+
       if (axios.isCancel(err)) {
         return;
       }
-      if (!err.response || (err.response.status >= 500 && err.response.status <= 504)) {
+
+      const response = isObjectWithProperties(err, 'response')
+        ? (err as AxiosError).response
+        : undefined;
+
+      if (!response || (response.status >= 500 && response.status <= 504)) {
         // network connection error
         discoveryProvider.reportUnreachable(storageProvider);
       }

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

@@ -2,7 +2,7 @@ import React from 'react';
 import { Link } from 'react-router-dom';
 import { ChannelEntity } from '../entities/ChannelEntity';
 import { BgImg } from '../common/BgImg';
-import { DEFAULT_THUMBNAIL_URL } from '@polkadot/joy-utils/images';
+import { DEFAULT_THUMBNAIL_URL } from '../common/images';
 
 const defaultSizePx = 75;
 

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

@@ -9,6 +9,7 @@ type Props = {
 
 export const ChannelAvatarAndName = (props: Props) => {
   const { channel } = props;
+
   return (
     <div className={'ChannelPreview small'}>
       <ChannelAvatar channel={channel} size='small' />

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

@@ -4,7 +4,7 @@ import { ChannelPublicationStatusAllValues } from '@joystream/types/content-work
 
 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';

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

@@ -10,6 +10,7 @@ type Props = {
 
 export const ChannelNameAsLink = (props: Props) => {
   const { channel, className, style } = props;
+
   return (
     <Link to={`/media/channels/${channel.id}`} className={className} style={style}>
       {channel.title || channel.handle}

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

@@ -6,8 +6,8 @@ import { ChannelEntity } from '../entities/ChannelEntity';
 import { ChannelAvatar, ChannelAvatarSize } from './ChannelAvatar';
 import { isPublicChannel, isMusicChannel, isVideoChannel, isAccountAChannelOwner, isVerifiedChannel } from './ChannelHelpers';
 
-import { useMyMembership } from '@polkadot/joy-utils/MyMembershipContext';
-import { nonEmptyStr } from '@polkadot/joy-utils/index';
+import { useMyMembership } from '@polkadot/joy-utils/react/hooks';
+import { nonEmptyStr } from '@polkadot/joy-utils/functions/misc';
 import { CurationPanel } from './CurationPanel';
 import { ChannelNameAsLink } from './ChannelNameAsLink';
 
@@ -52,8 +52,6 @@ export const ChannelPreview = (props: ChannelPreviewProps) => {
         <h3 className='ChannelTitle' style={{ display: 'block' }}>
           <ChannelNameAsLink channel={channel} style={{ marginRight: '1rem' }} />
 
-          <CurationPanel channel={channel} />
-
           {isAccountAChannelOwner(channel, myAccountId) &&
             <div style={{ float: 'right' }}>
 
@@ -101,6 +99,8 @@ export const ChannelPreview = (props: ChannelPreviewProps) => {
           }
         </div>
 
+        <CurationPanel channel={channel} />
+
         {withDescription && nonEmptyStr(channel.description) &&
           <ReactMarkdown className='JoyMemo--full ChannelDesc' source={channel.description} linkTarget='_blank' />
         }

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

@@ -13,6 +13,7 @@ export const ChannelPreviewStats = (props: Props) => {
   const statSize = 'tiny';
 
   let itemsPublishedLabel = '';
+
   if (channel.content === 'Video') {
     itemsPublishedLabel = 'Videos';
   } else if (channel.content === 'Music') {

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

@@ -19,7 +19,8 @@ const TabsAndChannels = (props: ChannelsByOwnerProps) => {
 
   let videoChannelsCount = 0;
   let musicChannelsCount = 0;
-  allChannels.forEach(x => {
+
+  allChannels.forEach((x) => {
     if (x.content === 'Video') {
       videoChannelsCount++;
     } else if (x.content === 'Music') {
@@ -38,6 +39,7 @@ const TabsAndChannels = (props: ChannelsByOwnerProps) => {
 
   const switchTab = (activeIndex: number) => {
     const activeContentType = contentTypeByTabIndex[activeIndex];
+
     if (activeContentType === undefined) {
       setChannels(allChannels);
     } else {

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

@@ -1,10 +1,10 @@
 import React from 'react';
 import { RouteComponentProps } from 'react-router';
 
-import { GenericAccountId } from '@polkadot/types';
 import { MediaView } from '../MediaView';
 import { ChannelsByOwnerProps, ChannelsByOwner } from './ChannelsByOwner';
-import { JoyError } from '@polkadot/joy-utils/JoyStatus';
+import { JoyError } from '@polkadot/joy-utils/react/components';
+import { useApi } from '@polkadot/react-hooks';
 
 type Props = ChannelsByOwnerProps;
 
@@ -13,16 +13,18 @@ export const ChannelsByOwnerView = MediaView<Props>({
   resolveProps: async (props) => {
     const { transport, accountId } = props;
     const channels = await transport.channelsByAccount(accountId);
+
     return { channels };
   }
 });
 
-export const ChannelsByOwnerWithRouter = (props: Props & RouteComponentProps<any>) => {
+export const ChannelsByOwnerWithRouter = (props: Props & RouteComponentProps<Record<string, string | undefined>>) => {
   const { match: { params: { account } } } = props;
+  const { api } = useApi();
 
   if (account) {
     try {
-      return <ChannelsByOwnerView {...props} accountId={new GenericAccountId(account)} />;
+      return <ChannelsByOwnerView {...props} accountId={api.createType('AccountId', account)} />;
     } catch (err) {
       console.log('ChannelsByOwnerWithRouter failed:', err);
     }

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

@@ -1,16 +1,18 @@
 import React from 'react';
 import { ChannelEntity } from '../entities/ChannelEntity';
 import { isVerifiedChannel, isCensoredChannel } from './ChannelHelpers';
-import { useMyMembership } from '@polkadot/joy-utils/MyMembershipContext';
-import TxButton from '@polkadot/joy-utils/TxButton';
-import { ChannelCurationStatus } from '@joystream/types/content-working-group';
+import { useMyMembership } from '@polkadot/joy-utils/react/hooks';
+import { SemanticTxButton } from '@polkadot/joy-utils/react/components/TxButton';
 import { AccountId } from '@polkadot/types/interfaces';
+import { useApi } from '@polkadot/react-hooks';
+import { Icon } from 'semantic-ui-react';
 
 type ChannelCurationPanelProps = {
   channel: ChannelEntity;
 };
 
 export const CurationPanel = (props: ChannelCurationPanelProps) => {
+  const { api } = useApi();
   const { curationActor, allAccounts } = useMyMembership();
   const { channel } = props;
 
@@ -34,17 +36,16 @@ export const CurationPanel = (props: ChannelCurationPanelProps) => {
 
     const isCensored = isCensoredChannel(channel);
 
-    const new_curation_status = new ChannelCurationStatus(
+    const new_curation_status = api.createType('ChannelCurationStatus',
       isCensored ? 'Normal' : 'Censored'
     );
 
-    return <TxButton
+    return <SemanticTxButton
       accountId={role_account.toString()}
       type='submit'
-      size='medium'
-      icon={isCensored ? 'x' : 'warning'}
-      isDisabled={!accountAvailable}
-      label={isCensored ? 'Un-Censor' : 'Censor'}
+      size='small'
+      color={isCensored ? undefined : 'red'}
+      disabled={!accountAvailable}
       params={[
         curation_actor,
         channel.id,
@@ -52,7 +53,10 @@ export const CurationPanel = (props: ChannelCurationPanelProps) => {
         new_curation_status // toggled curation status
       ]}
       tx={'contentWorkingGroup.updateChannelAsCurationActor'}
-    />;
+    >
+      <Icon name={isCensored ? 'x' : 'warning'}/>
+      { isCensored ? 'Un-Censor' : 'Censor' }
+    </SemanticTxButton>;
   };
 
   const renderToggleVerifiedButton = () => {
@@ -62,13 +66,12 @@ export const CurationPanel = (props: ChannelCurationPanelProps) => {
     const accountAvailable = canUseAccount(role_account);
     const isVerified = isVerifiedChannel(channel);
 
-    return <TxButton
+    return <SemanticTxButton
       accountId={role_account.toString()}
       type='submit'
-      size='medium'
-      icon={isVerified ? 'x' : 'checkmark'}
-      isDisabled={!accountAvailable}
-      label={isVerified ? 'Remove Verification' : 'Verify'}
+      size='small'
+      color={isVerified ? undefined : 'green'}
+      disabled={!accountAvailable}
       params={[
         curation_actor,
         channel.id,
@@ -76,11 +79,14 @@ export const CurationPanel = (props: ChannelCurationPanelProps) => {
         null // not changing curation status
       ]}
       tx={'contentWorkingGroup.updateChannelAsCurationActor'}
-    />;
+    >
+      <Icon name={isVerified ? 'x' : 'checkmark'}/>
+      { isVerified ? 'Remove Verification' : 'Verify' }
+    </SemanticTxButton>;
   };
 
   return <>
-    <div style={{ float: 'right' }}>
+    <div style={{ display: 'flex', float: 'right', margin: '0.5em', marginRight: 0 }}>
       {renderToggleCensorshipButton()}
       {renderToggleVerifiedButton()}
     </div>

+ 25 - 26
pioneer/packages/joy-media/src/channels/EditChannel.tsx

@@ -3,21 +3,21 @@ import { Button } from 'semantic-ui-react';
 import { Form, withFormik } from 'formik';
 import { History } from 'history';
 
-import { Text, Option } from '@polkadot/types';
-import TxButton from '@polkadot/joy-utils/TxButton';
-import { onImageError } from '@polkadot/joy-utils/images';
+import { Option } from '@polkadot/types';
+import { TxButton, JoyError, Section } from '@polkadot/joy-utils/react/components';
+import { onImageError } from '../common/images';
 import { withMediaForm, MediaFormProps } from '../common/MediaForms';
 import { ChannelType, ChannelClass as Fields, buildChannelValidationSchema, ChannelFormValues, ChannelToFormValues, ChannelGenericProp } from '../schemas/channel/Channel';
 import { MediaDropdownOptions } from '../common/MediaDropdownOptions';
-import { ChannelId, ChannelContentType, ChannelPublicationStatus, OptionalText } from '@joystream/types/content-working-group';
-import { newOptionalText, findFirstParamOfSubstrateEvent } from '@polkadot/joy-utils/index';
-import { useMyMembership } from '@polkadot/joy-utils/MyMembershipContext';
+import { ChannelId, OptionalText } from '@joystream/types/content-working-group';
+import { findFirstParamOfSubstrateEvent } from '@polkadot/joy-utils/functions/misc';
+import { useMyMembership } from '@polkadot/joy-utils/react/hooks';
 import { ChannelPublicationStatusDropdownOptions, isAccountAChannelOwner } from './ChannelHelpers';
 import { TxCallback } from '@polkadot/react-components/Status/types';
 import { SubmittableResult } from '@polkadot/api';
 import { ChannelValidationConstraints } from '../transport';
-import { JoyError } from '@polkadot/joy-utils/JoyStatus';
-import Section from '@polkadot/joy-utils/Section';
+
+import { useApi } from '@polkadot/react-hooks';
 
 export type OuterProps = {
   history?: History;
@@ -56,6 +56,7 @@ const InnerForm = (props: MediaFormProps<OuterProps, FormValues>) => {
   } = props;
 
   const { myAccountId, myMemberId } = useMyMembership();
+  const { api } = useApi();
 
   if (entity && !isAccountAChannelOwner(entity, myAccountId)) {
     return <JoyError title={'Only owner can edit channel'} />;
@@ -83,52 +84,49 @@ const InnerForm = (props: MediaFormProps<OuterProps, FormValues>) => {
   const buildTxParams = () => {
     if (!isValid) return [];
 
-    // TODO get value from the form:
-    const publicationStatus = new ChannelPublicationStatus('Public');
-
     if (!entity) {
       // Create a new channel
 
       const channelOwner = myMemberId;
       const roleAccount = myAccountId;
-      const contentType = new ChannelContentType(values.content);
+      const contentType = api.createType('ChannelContentType', values.content);
 
       return [
         channelOwner,
         roleAccount,
         contentType,
-        new Text(values.handle),
-        newOptionalText(values.title),
-        newOptionalText(values.description),
-        newOptionalText(values.avatar),
-        newOptionalText(values.banner),
-        publicationStatus
+        values.handle,
+        values.title || null,
+        values.description || null,
+        values.avatar || null,
+        values.banner || null,
+        values.publicationStatus
       ];
     } else {
       // Update an existing channel
 
       const updOptText = (field: ChannelGenericProp): Option<OptionalText> => {
-        return new Option(OptionalText,
+        return api.createType('Option<OptionalText>',
           isFieldChanged(field)
-            ? newOptionalText(values[field.id])
+            ? api.createType('Option<Text>', values[field.id])
             : null
         );
       };
 
-      const updHandle = new Option(Text,
+      const updHandle = api.createType('Option<Text>',
         isFieldChanged(Fields.handle)
           ? values[Fields.handle.id]
           : null
       );
 
-      const updPublicationStatus = new Option(ChannelPublicationStatus,
+      const updPublicationStatus = api.createType('Option<ChannelPublicationStatus>',
         isFieldChanged(Fields.publicationStatus)
-          ? new ChannelPublicationStatus(values[Fields.publicationStatus.id] as any)
+          ? api.createType('ChannelPublicationStatus', values[Fields.publicationStatus.id])
           : null
       );
 
       return [
-        new ChannelId(entity.id),
+        entity.id,
         updHandle,
         updOptText(Fields.title),
         updOptText(Fields.description),
@@ -156,7 +154,6 @@ const InnerForm = (props: MediaFormProps<OuterProps, FormValues>) => {
   const renderMainButton = () =>
     <TxButton
       type='submit'
-      size='large'
       isDisabled={!dirty || isSubmitting}
       label={isNew
         ? 'Create channel'
@@ -182,7 +179,7 @@ const InnerForm = (props: MediaFormProps<OuterProps, FormValues>) => {
 
         {formFields()}
 
-        <LabelledField style={{ marginTop: '1rem' }} {...props}>
+        <LabelledField style={{ marginTop: '1rem' }} {...props} flex>
           {renderMainButton()}
           <Button
             type='button'
@@ -202,11 +199,13 @@ export const EditForm = withFormik<OuterProps, FormValues>({
   // Transform outer props into form values
   mapPropsToValues: (props): FormValues => {
     const { entity } = props;
+
     return ChannelToFormValues(entity);
   },
 
   validationSchema: (props: OuterProps): any => {
     const { constraints } = props;
+
     if (!constraints) return null;
 
     return buildChannelValidationSchema(constraints);

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

@@ -2,8 +2,8 @@ import React from 'react';
 import { RouteComponentProps } from 'react-router';
 import { MediaView } from '../MediaView';
 import { OuterProps, EditForm } from './EditChannel';
-import { ChannelId } from '@joystream/types/content-working-group';
-import { JoyError } from '@polkadot/joy-utils/JoyStatus';
+import { JoyError } from '@polkadot/joy-utils/react/components';
+import { useApi } from '@polkadot/react-hooks';
 
 type Props = OuterProps;
 
@@ -15,18 +15,20 @@ export const EditChannelView = MediaView<Props>({
     const { transport, id } = props;
     const entity = id && await transport.channelById(id);
     const constraints = await transport.channelValidationConstraints();
+
     return { entity, constraints };
   }
 });
 
-type WithRouterProps = Props & RouteComponentProps<any>
+type WithRouterProps = Props & RouteComponentProps<Record<string, string | undefined>>
 
 export const EditChannelWithRouter = (props: WithRouterProps) => {
   const { match: { params: { id } } } = props;
+  const { api } = useApi();
 
   if (id) {
     try {
-      return <EditChannelView {...props} id={new ChannelId(id)} />;
+      return <EditChannelView {...props} id={api.createType('ChannelId', id)} />;
     } catch (err) {
       console.log('EditChannelWithRouter failed:', err);
     }

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

@@ -8,7 +8,7 @@ import { ViewVideoChannel } from './ViewVideoChannel';
 import { ViewMusicChannel } from './ViewMusicChannel';
 import { toVideoPreviews } from '../video/VideoPreview';
 import { isVideoChannel, isMusicChannel } from './ChannelHelpers';
-import { JoyError } from '@polkadot/joy-utils/JoyStatus';
+import { JoyError } from '@polkadot/joy-utils/react/components';
 
 export type ViewChannelProps = {
   id: ChannelId;
@@ -31,6 +31,7 @@ export function ViewChannel (props: ViewChannelProps) {
 
   if (isVideoChannel(channel)) {
     const previews = toVideoPreviews(videos);
+
     return <ViewVideoChannel channel={channel} videos={previews} />;
   } else if (isMusicChannel(channel)) {
     return <ViewMusicChannel channel={channel} albums={albums} tracks={tracks} />;

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

@@ -2,8 +2,8 @@ import React from 'react';
 import { RouteComponentProps } from 'react-router';
 import { MediaView } from '../MediaView';
 import { ViewChannelProps, ViewChannel } from './ViewChannel';
-import { ChannelId } from '@joystream/types/content-working-group';
-import { JoyError } from '@polkadot/joy-utils/JoyStatus';
+import { JoyError } from '@polkadot/joy-utils/react/components';
+import { useApi } from '@polkadot/react-hooks';
 
 type Props = ViewChannelProps;
 
@@ -14,16 +14,18 @@ export const ViewChannelView = MediaView<Props>({
     const { transport, id } = props;
     const channel = await transport.channelById(id);
     const videos = await transport.videosByChannelId(id);
+
     return { channel, videos };
   }
 });
 
-export const ViewChannelWithRouter = (props: Props & RouteComponentProps<any>) => {
+export const ViewChannelWithRouter = (props: Props & RouteComponentProps<Record<string, string | undefined>>) => {
   const { match: { params: { id } } } = props;
+  const { api } = useApi();
 
   if (id) {
     try {
-      return <ViewChannelView {...props} id={new ChannelId(id)} />;
+      return <ViewChannelView {...props} id={api.createType('ChannelId', id)} />;
     } catch (err) {
       console.log('ViewChannelWithRouter failed:', err);
     }

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

@@ -1,6 +1,6 @@
 import React from 'react';
 import { ChannelEntity } from '../entities/ChannelEntity';
-import Section from '@polkadot/joy-utils/Section';
+import { Section } from '@polkadot/joy-utils/react/components';
 import { ChannelHeader } from './ChannelHeader';
 import { MusicAlbumPreviewProps, MusicAlbumPreview } from '../music/MusicAlbumPreview';
 import { MusicTrackReaderPreview, MusicTrackReaderPreviewProps } from '../music/MusicTrackReaderPreview';
@@ -27,7 +27,7 @@ export function ViewMusicChannel (props: Props) {
     !albums.length
       ? <NoAlbums />
       : <Section title={'Music albums'}>
-        {albums.map(x => <MusicAlbumPreview key={x.id} {...x} />)}
+        {albums.map((x) => <MusicAlbumPreview key={x.id} {...x} />)}
       </Section>
   );
 
@@ -35,7 +35,7 @@ export function ViewMusicChannel (props: Props) {
     !tracks.length
       ? <NoTracks />
       : <Section title={'Music tracks'}>
-        {tracks.map(x => <MusicTrackReaderPreview key={x.id} {...x} />)}
+        {tracks.map((x) => <MusicTrackReaderPreview key={x.id} {...x} />)}
       </Section>
   );
 

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

@@ -1,5 +1,5 @@
 import React from 'react';
-import Section from '@polkadot/joy-utils/Section';
+import { Section } from '@polkadot/joy-utils/react/components';
 import { ChannelEntity } from '../entities/ChannelEntity';
 import { ChannelHeader } from './ChannelHeader';
 import { VideoPreview, VideoPreviewProps } from '../video/VideoPreview';

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio