Joystream Stats 3 роки тому
батько
коміт
53de1fa1d4

+ 1 - 1
package.json

@@ -49,11 +49,11 @@
   },
   "devDependencies": {
     "@joystream/types": "^0.14.0",
+    "@types/bootstrap": "^5.0.1",
     "@types/jest": "^26.0.15",
     "@types/node": "^12.0.0",
     "@types/node-fetch": "^2.5.7",
     "@types/react": "^16.9.53",
-    "@types/bootstrap": "^5.0.1",
     "@types/react-bootstrap": "^0.32.25",
     "@types/react-calendar-timeline": "^0.26.3",
     "@types/react-dom": "^16.9.8",

+ 244 - 275
src/App.tsx

@@ -7,10 +7,10 @@ import { domain, wsLocation } from "./config";
 import proposalPosts from "./proposalPosts";
 import axios from "axios";
 import { ProposalDetail } from "./types";
+import socket from "./socket";
 
 import {
   Api,
-  Block,
   Handles,
   IState,
   Member,
@@ -19,6 +19,7 @@ import {
   Post,
   Seat,
   Thread,
+  Status,
 } from "./types";
 import { types } from "@joystream/types";
 import { ApiPromise, WsProvider } from "@polkadot/api";
@@ -27,17 +28,11 @@ import { VoteKind } from "@joystream/types/proposals";
 
 interface IProps {}
 
-const version = 0.3;
+const version = 0.4;
 
 const initialState = {
-  connecting: true,
-  loading: true,
+  queue: [],
   blocks: [],
-  now: 0,
-  block: 0,
-  era: 0,
-  issued: 0,
-  price: 0,
   nominators: [],
   validators: [],
   channels: [],
@@ -46,151 +41,171 @@ const initialState = {
   categories: [],
   threads: [],
   proposals: [],
-  proposalCount: 0,
   domain,
   handles: {},
   members: [],
   proposalPosts,
   reports: {},
-  termEndsAt: 0,
-  stage: {},
   stakes: {},
   stashes: [],
   stars: {},
-  lastReward: 0,
   hideFooter: false,
+  status: { connecting: true, loading: "" },
 };
 
 class App extends React.Component<IProps, IState> {
-  async initializeSocket() {
+  initializeSocket() {
+    socket.on("connect", () => {
+      if (!socket.id) return console.log("no websocket connection");
+      console.log("my socketId:", socket.id);
+      socket.emit("get posts", this.state.posts.length);
+    });
+    socket.on("posts", (posts: Post[]) => {
+      console.log(`received ${posts.length} posts`);
+      this.setState({ posts });
+    });
+
     console.debug(`Connecting to ${wsLocation}`);
     const provider = new WsProvider(wsLocation);
-    const api = await ApiPromise.create({ provider, types });
-    await api.isReady;
-    this.setState({ connecting: false });
-    console.log(`Connected to ${wsLocation}`);
-
-    let blocks: Block[] = [];
-    let lastBlock: Block = { id: 0, timestamp: 0, duration: 6 };
-    let era = 0;
+    ApiPromise.create({ provider, types }).then((api) =>
+      api.isReady.then(() => {
+        console.log(`Connected to ${wsLocation}`);
+        this.setState({ connecting: false });
+        this.handleApi(api);
+      })
+    );
+  }
 
+  async handleApi(api: Api) {
+    let blockHash = await api.rpc.chain.getBlockHash(1);
+    let startTime = (await api.query.timestamp.now.at(blockHash)).toNumber();
+    let stage: any = await api.query.councilElection.stage();
     let termEndsAt = Number((await api.query.council.termEndsAt()).toJSON());
-    this.save("termEndsAt", termEndsAt);
     let round: number = Number(
       (await api.query.councilElection.round()).toJSON()
     );
-    this.save("round", round);
-    let stage: any = await api.query.councilElection.stage();
-    this.save("stage", stage);
-    let councilElection = { termEndsAt, stage: stage.toJSON(), round };
-    this.setState({ councilElection });
-    let stageEndsAt: number = termEndsAt;
-
-    let lastCategory = await get.currentCategoryId(api);
-    this.fetchCategories(api, lastCategory);
-
-    let lastChannel = await get.currentChannelId(api);
-    this.fetchChannels(api, lastChannel);
-
-    let lastPost = await get.currentPostId(api);
-    this.fetchPosts(api, lastPost);
-
-    let lastThread = await get.currentThreadId(api);
-    this.fetchThreads(api, lastThread);
-
-    let lastMember = await api.query.members.nextMemberId();
-    this.fetchMembers(api, Number(lastMember));
-
-    api.rpc.chain.subscribeNewHeads(
-      async (header: Header): Promise<void> => {
-        // current block
-        const id = header.number.toNumber();
-        if (blocks.find((b) => b.id === id)) return;
-        const timestamp = (await api.query.timestamp.now()).toNumber();
-        const duration = timestamp - lastBlock.timestamp;
-        const block: Block = { id, timestamp, duration };
-        blocks = blocks.concat(block);
-        this.setState({ blocks, loading: false });
-        this.save("block", id);
-        this.save("now", timestamp);
-
-        const proposalCount = await get.proposalCount(api);
-        if (proposalCount > this.state.proposalCount) {
-          this.fetchProposal(api, proposalCount);
-          this.setState({ proposalCount });
-        }
-
-        const currentChannel = await get.currentChannelId(api);
-        if (currentChannel > lastChannel)
-          lastChannel = await this.fetchChannels(api, currentChannel);
-
-        const currentCategory = await get.currentCategoryId(api);
-        if (currentCategory > lastCategory)
-          lastCategory = await this.fetchCategories(api, currentCategory);
-
-        const currentPost = await get.currentPostId(api);
-        if (currentPost > lastPost)
-          lastPost = await this.fetchPosts(api, currentPost);
-
-        const currentThread = await get.currentThreadId(api);
-        if (currentThread > lastThread)
-          lastThread = await this.fetchThreads(api, currentThread);
-
-        const postCount = await api.query.proposalsDiscussion.postCount();
-        this.setState({ proposalComments: Number(postCount) });
-
-        lastBlock = block;
-
-        // validators
-        const currentEra = Number(await api.query.staking.currentEra());
-        if (currentEra > era) {
-          era = currentEra;
-          this.fetchStakes(api, era, this.state.validators);
-          this.save("era", era);
-          this.fetchLastReward(api, era - 1);
-        } else if (this.state.lastReward === 0)
-          this.fetchLastReward(api, currentEra);
-
-        this.fetchEraRewardPoints(api, Number(era));
-
-        // check election stage
-        if (id < termEndsAt || id < stageEndsAt) return;
-        const json = stage.toJSON();
-        const key = Object.keys(json)[0];
-        stageEndsAt = json[key];
-        //console.log(id, stageEndsAt, json, key);
-
-        termEndsAt = Number((await api.query.council.termEndsAt()).toJSON());
-        round = Number((await api.query.councilElection.round()).toJSON());
-        stage = await api.query.councilElection.stage();
-        councilElection = { termEndsAt, stage: stage.toJSON(), round };
-        this.setState({ councilElection });
-      }
+
+    let status: Status = {
+      block: { id: 0, era: 0, timestamp: 0, duration: 6 },
+      council: { termEndsAt, stage: stage.toJSON(), round },
+      category: await get.currentCategoryId(api),
+      channel: await get.currentChannelId(api),
+      post: await get.currentPostId(api),
+      thread: await get.currentThreadId(api),
+      member: await api.query.members.nextMemberId(),
+      proposals: 0,
+      startTime,
+    };
+    this.save("status", status);
+
+    api.rpc.chain.subscribeNewHeads((header: Header) =>
+      this.handleBlock(api, header)
+    );
+    this.enqueue("members", () =>
+      this.fetchMembers(api, Number(status.member))
     );
+    this.enqueue("councils", () => this.fetchCouncils(api, round));
+    this.enqueue("proposals", () => this.fetchProposals(api));
+    this.enqueue("validators", () => this.fetchValidators(api));
+    this.enqueue("nominators", () => this.fetchNominators(api));
+    //this.enqueue("categories", () => this.fetchCategories(api));
+    //this.enqueue("threads", () => this.fetchThreads(api, status.thread));
+    this.enqueue("posts", () => this.fetchPosts(api, status.post));
+    //this.enqueue("channels", () => this.fetchChannels(api, status.channel));
+  }
+
+  async handleBlock(api, header: Header) {
+    let { blocks, status } = this.state;
+
+    // current block
+    const id = header.number.toNumber();
+    if (blocks.find((b) => b.id === id)) return;
+    const timestamp = (await api.query.timestamp.now()).toNumber();
+    const duration = status.block ? timestamp - status.block.timestamp : 6000;
+    status.block = { id, timestamp, duration };
+    blocks = blocks.concat(status.block);
+    this.setState({ blocks });
+
+    // validators
+    const era = Number(await api.query.staking.currentEra());
+    this.fetchEraRewardPoints(api, era);
+    if (era > status.era) {
+      status.era = era;
+      this.fetchStakes(api, era, this.state.validators);
+      this.fetchLastReward(api, era - 1);
+    } else if (!status.lastReward) this.fetchLastReward(api, era);
+
+    // every minute
+    if (id / 10 === (id / 10).toFixed()) {
+      this.updateCouncil(api, id);
+      status.proposals = await this.fetchProposals(api, status.proposals);
+      status.posts = await this.fetchPosts(api, status.posts);
+      status.channels = await this.fetchChannels(api, status.currentChannel);
+      status.categories = await this.fetchCategories(api);
+      status.threads = await this.fetchThreads(api, status.threads);
+      status.proposalPosts = await api.query.proposalsDiscussion.postCount();
+    }
 
-    this.fetchCouncils(api, round);
-    this.fetchProposals(api);
-    this.fetchValidators(api);
-    this.fetchNominators(api);
+    this.save("status", status);
+    this.nextTask();
+  }
+
+  async updateCouncil(api, id) {
+    let { status } = this.state;
+    if (id < status.council.termEndsAt || id < status.council.stageEndsAt)
+      return;
+    round = Number((await api.query.councilElection.round()).toJSON());
+    stage = await api.query.councilElection.stage();
+    const json = stage.toJSON();
+    const key = Object.keys(json)[0];
+    const stageEndsAt = json[key];
+    const termEndsAt = Number((await api.query.council.termEndsAt()).toJSON());
+    status.council = { round, stageEndsAt, termEndsAt, stage: stage.toJSON() };
+    this.save("status", status);
+  }
+
+  enqueue(key: string, action: () => void) {
+    this.setState({ queue: this.state.queue.concat({ key, action }) });
+  }
+  async nextTask() {
+    let { queue, status } = this.state;
+    if (status.loading === "") return;
+    const task = queue.shift();
+    if (!task) return;
+    status.loading = task.key;
+    this.setState({ status, queue });
+    console.debug(`processing: ${status.loading}`);
+
+    await task.action();
+    status.loading = "";
+    this.setState({ status });
+    setTimeout(() => this.nextTask(), 0);
   }
 
   async fetchLastReward(api: Api, era: number) {
     const lastReward = Number(await api.query.staking.erasValidatorReward(era));
     console.debug(`last reward`, era, lastReward);
-    if (lastReward > 0) this.save("lastReward", lastReward);
-    else this.fetchLastReward(api, era - 1);
+    if (lastReward) {
+      let { status } = this.state;
+      status.lastReward = lastReward;
+      this.save("status", status);
+    } else this.fetchLastReward(api, era - 1);
   }
 
   async fetchTokenomics() {
     console.debug(`Updating tokenomics`);
-    const { data } = await axios.get("https://status.joystream.org/status");
-    if (!data) return;
-    this.save("tokenomics", data);
+    try {
+      const { data } = await axios.get("https://status.joystream.org/status");
+      if (!data) return;
+      this.save("tokenomics", data);
+    } catch (e) {}
   }
 
-  async fetchChannels(api: Api, lastId: number) {
+  async fetchChannels(api: Api) {
+    const lastId = await get.currentChannelId(api);
     for (let id = lastId; id > 0; id--) {
-      if (this.state.channels.find((c) => c.id === id)) continue;
+      if (this.state.channels.find((c) => c.id === id)) return lastId;
+
       console.debug(`Fetching channel ${id}`);
       const data = await api.query.contentWorkingGroup.channelById(id);
 
@@ -231,67 +246,81 @@ class App extends React.Component<IProps, IState> {
     }
     return lastId;
   }
-  async fetchCategories(api: Api, lastId: number) {
-    for (let id = lastId; id > 0; id--) {
-      if (this.state.categories.find((c) => c.id === id)) continue;
-      console.debug(`fetching category ${id}`);
-      const data = await api.query.forum.categoryById(id);
 
-      const threadId = Number(data.thread_id);
-      const title = String(data.title);
-      const description = String(data.description);
-      const createdAt = Number(data.created_at.block);
-      const deleted = data.deleted;
-      const archived = data.archived;
-      const subcategories = Number(data.num_direct_subcategories);
-      const moderatedThreads = Number(data.num_direct_moderated_threads);
-      const unmoderatedThreads = Number(data.num_direct_unmoderated_threads);
-      const position = Number(data.position_in_parent_category);
-      const moderatorId = String(data.moderator_id);
-
-      const category: Category = {
-        id,
-        threadId,
-        title,
-        description,
-        createdAt,
-        deleted,
-        archived,
-        subcategories,
-        moderatedThreads,
-        unmoderatedThreads,
-        position,
-        moderatorId,
-      };
-
-      const categories = this.state.categories.concat(category);
-      this.save("categories", categories);
+  async fetchCategories(api: Api) {
+    const lastId = await get.currentCategoryId(api);
+    for (let id = lastId; id > 0; id--) {
+      if (this.state.categories.find((c) => c.id === id)) return lastId;
+      this.enqueue(`category ${id}`, () => this.fetchCategory(api, id));
     }
     return lastId;
   }
-  async fetchPosts(api: Api, lastId: number) {
-    for (let id = lastId; id > 0; id--) {
-      if (this.state.posts.find((p) => p.id === id)) continue;
-      console.debug(`fetching post ${id}`);
-      const data = await api.query.forum.postById(id);
-
-      const threadId = Number(data.thread_id);
-      const text = data.current_text;
-      //const moderation = data.moderation;
-      //const history = data.text_change_history;
-      //const createdAt = moment(data.created_at);
-      const createdAt = data.created_at;
-      const authorId = String(data.author_id);
+  async fetchCategory(api: Api, id: number) {
+    const data = await api.query.forum.categoryById(id);
+    const threadId = Number(data.thread_id);
+    const title = String(data.title);
+    const description = String(data.description);
+    const createdAt = Number(data.created_at.block);
+    const deleted = data.deleted;
+    const archived = data.archived;
+    const subcategories = Number(data.num_direct_subcategories);
+    const moderatedThreads = Number(data.num_direct_moderated_threads);
+    const unmoderatedThreads = Number(data.num_direct_unmoderated_threads);
+    const position = Number(data.position_in_parent_category);
+    const moderatorId = String(data.moderator_id);
+
+    const category: Category = {
+      id,
+      threadId,
+      title,
+      description,
+      createdAt,
+      deleted,
+      archived,
+      subcategories,
+      moderatedThreads,
+      unmoderatedThreads,
+      position,
+      moderatorId,
+    };
+
+    this.save("categories", this.state.categories.concat(category));
+  }
+  async fetchPosts(api: Api) {
+    const lastId = get.currentPostId(api);
+    //const { data } = await axios.get(`${apiLocation}/posts`);
+    //console.log(`received posts`, data);
+    //this.save("posts", data);
+    //return lastId;
 
-      const post: Post = { id, threadId, text, authorId, createdAt };
-      const posts = this.state.posts.concat(post);
-      this.save("posts", posts);
+    let { posts } = this.state;
+    for (let id = lastId; id > 0; id--) {
+      if (posts.find((p) => p.id === id)) return lastId;
+      this.enqueue(`post ${id}`, () => this.fetchPost(api, id));
     }
     return lastId;
   }
-  async fetchThreads(api: Api, lastId: number) {
+  async fetchPost(api: Api, id: number) {
+    if (this.state.posts.find((p) => p.id === id)) return;
+    console.debug(`fetching post ${id}`);
+    const data = await api.query.forum.postById(id);
+
+    const threadId = Number(data.thread_id);
+    const text = data.current_text.slice(0, 1000);
+    //const moderation = data.moderation;
+    //const history = data.text_change_history;
+    const createdAt = data.created_at;
+    const authorId = String(data.author_id);
+
+    const post: Post = { id, threadId, text, authorId, createdAt };
+    const posts = this.state.posts.concat(post);
+    this.save("posts", posts);
+  }
+
+  async fetchThreads(api: Api) {
+    const lastId = await get.currentThreadId(api);
     for (let id = lastId; id > 0; id--) {
-      if (this.state.threads.find((t) => t.id === id)) continue;
+      if (this.state.threads.find((t) => t.id === id)) return lastId;
       console.debug(`fetching thread ${id}`);
       const data = await api.query.forum.threadById(id);
 
@@ -311,7 +340,6 @@ class App extends React.Component<IProps, IState> {
         createdAt,
         authorId,
       };
-
       const threads = this.state.threads.concat(thread);
       this.save("threads", threads);
     }
@@ -337,18 +365,24 @@ class App extends React.Component<IProps, IState> {
 
   // proposals
   async fetchProposals(api: Api) {
-    const proposalCount = await get.proposalCount(api);
-    for (let i = proposalCount; i > 0; i--) this.fetchProposal(api, i);
+    const lastId = await get.proposalCount(api);
+    for (let id = lastId; id > 0; id--)
+      this.enqueue(`proposal ${id}`, () => this.fetchProposal(api, id));
+    return lastId;
   }
   async fetchProposal(api: Api, id: number) {
     const { proposals } = this.state;
     const exists = this.state.proposals.find((p) => p && p.id === id);
 
-    if (exists && exists.detail && exists.stage === "Finalized")
+    if (
+      exists &&
+      exists.detail &&
+      exists.stage === "Finalized" &&
+      exists.executed
+    )
       if (exists.votesByAccount && exists.votesByAccount.length) return;
       else return this.fetchVotesPerProposal(api, exists);
 
-    console.debug(`Fetching proposal ${id}`);
     const proposal = await get.proposalDetail(api, id);
     if (proposal.type !== "Text") {
       const details = await api.query.proposalsCodex.proposalDetailsByProposalId(
@@ -569,8 +603,8 @@ class App extends React.Component<IProps, IState> {
   loadMembers() {
     const members = this.load("members");
     if (!members) return;
-    this.updateHandles(members);
     this.setState({ members });
+    this.updateHandles(members);
   }
   loadCouncils() {
     const councils = this.load("councils");
@@ -578,120 +612,58 @@ class App extends React.Component<IProps, IState> {
       return;
     this.setState({ councils });
   }
-  loadProposals() {
-    const proposals = this.load("proposals");
-    if (proposals) this.setState({ proposals });
-  }
-  loadChannels() {
-    const channels = this.load("channels");
-    if (channels) this.setState({ channels });
-  }
-  loadCategories() {
-    const categories = this.load("categories");
-    if (categories) this.setState({ categories });
-  }
   loadPosts() {
-    const posts = this.load("posts");
+    const posts: Post[] = this.load("posts");
+    posts.forEach(({ id, text }) => {
+      if (text && text.length > 500)
+        console.debug(`post ${id}: ${(text.length / 1000).toFixed(1)} KB`);
+    });
     if (posts) this.setState({ posts });
   }
-  loadThreads() {
-    const threads = this.load("threads");
-    if (threads) this.setState({ threads });
-  }
-
-  loadValidators() {
-    const validators = this.load("validators");
-    if (validators) this.setState({ validators });
-
-    const stashes = this.load("stashes") || [];
-    if (stashes) this.setState({ stashes });
-  }
-  loadNominators() {
-    const nominators = this.load("nominators");
-    if (nominators) this.setState({ nominators });
-  }
-  loadHandles() {
-    const handles = this.load("handles");
-    if (handles) this.setState({ handles });
-  }
-  loadReports() {
-    const reports = this.load("reports");
-    if (!reports) return this.fetchReports();
-    this.setState({ reports });
-  }
-  loadTokenomics() {
-    const tokenomics = this.load("tokenomics");
-    if (tokenomics) this.setState({ tokenomics });
-  }
-  loadMint() {
-    const mint = this.load("mint");
-    if (mint) this.setState({ mint });
-  }
-  loadStakes() {
-    const stakes = this.load("stakes");
-    if (stakes) this.setState({ stakes });
-  }
 
   clearData() {
-    this.save("version", version);
+    let { status } = this.state;
+    status.version = version;
+    this.save("status", status);
     this.save("proposals", []);
+    this.save("posts", []);
   }
   async loadData() {
-    const lastVersion = this.load("version");
-    if (lastVersion !== version) return this.clearData();
-    console.log(`Loading data`);
-    const termEndsAt = this.load("termEndsAt");
-    await this.loadMembers();
-    await this.loadCouncils();
-    await this.loadCategories();
-    await this.loadChannels();
-    await this.loadProposals();
-    await this.loadPosts();
-    await this.loadThreads();
-    await this.loadValidators();
-    await this.loadNominators();
-    await this.loadHandles();
-    await this.loadTokenomics();
-    await this.loadReports();
-    await this.loadStakes();
-    const block = this.load("block");
-    const now = this.load("now");
-    const era = this.load("era") || `..`;
-    const round = this.load("round");
-    const stage = this.load("stage");
-    const stars = this.load("stars") || {};
-    const lastReward = this.load("lastReward") || 0;
-    const loading = false;
-    this.setState({
-      block,
-      era,
-      now,
-      round,
-      stage,
-      stars,
-      termEndsAt,
-      loading,
-      lastReward,
-    });
+    const status = this.load("status");
+    if (status && status.version !== version) return this.clearData();
+    this.setState({ status });
+    console.debug(`Loading data`);
+    this.loadMembers();
+    this.loadCouncils();
+    "categories channels proposals posts threads validators nominators handles tokenomics reports stakes stars"
+      .split(" ")
+      .map((key) => this.load(key));
     console.debug(`Finished loading.`);
   }
 
   load(key: string) {
+    console.debug(`loading ${key}`);
     try {
       const data = localStorage.getItem(key);
-      if (data) return JSON.parse(data);
+      if (!data) return;
+      const size = data.length;
+      if (size > 10240) console.debug(`${key}: ${(size / 1024).toFixed(1)} KB`);
+      this.setState({ [key]: JSON.parse(data) });
+      return JSON.parse(data);
     } catch (e) {
       console.warn(`Failed to load ${key}`, e);
     }
   }
   save(key: string, data: any) {
+    this.setState({ [key]: data });
     try {
       localStorage.setItem(key, JSON.stringify(data));
     } catch (e) {
-      console.warn(`Failed to save ${key}`, e);
-    } finally {
-      //console.debug(`saving ${key}`, data);
-      this.setState({ [key]: data });
+      console.warn(`Failed to save ${key} (${data.length}KB)`, e);
+      if (key !== `posts`) {
+        localStorage.setItem(`posts`, `[]`);
+        localStorage.setItem(`channels`, `[]`);
+      }
     }
   }
 
@@ -717,14 +689,11 @@ class App extends React.Component<IProps, IState> {
     this.fetchTokenomics();
     setInterval(this.fetchTokenomics, 900000);
   }
-  componentWillUnmount() {
-    console.debug("unmounting...");
-  }
+  componentWillUnmount() {}
   constructor(props: IProps) {
     super(props);
     this.state = initialState;
     this.fetchTokenomics = this.fetchTokenomics.bind(this);
-    this.fetchProposal = this.fetchProposal.bind(this);
     this.load = this.load.bind(this);
     this.toggleStar = this.toggleStar.bind(this);
     this.toggleFooter = this.toggleFooter.bind(this);

+ 2 - 1
src/components/Back.tsx

@@ -1,7 +1,8 @@
 import React from "react";
 import { Button } from "react-bootstrap";
 
-const Back = (props: { target?: string; history: any }) => {
+const Back = (props: { hide?: boolean; target?: string; history: any }) => {
+  if (props.hide) return <span />;
   const goBack = () => props.history.goBack();
   return (
     <Button variant="secondary" className="p-1 m-1" onClick={goBack}>

+ 11 - 13
src/components/Calendar/index.tsx

@@ -12,8 +12,7 @@ import { CalendarItem, CalendarGroup, ProposalDetail } from "../../types";
 
 interface IProps {
   proposals: ProposalDetail[];
-  now: number;
-  block: number;
+  status: { startTime: number };
   history: any;
 }
 interface IState {
@@ -30,14 +29,12 @@ class Calendar extends Component<IProps, IState> {
     this.openProposal = this.openProposal.bind(this);
   }
 
-  componentDidMount() {
-    this.filterItems();
-  }
+  componentDidMount() {}
 
   filterItems() {
-    const { block, now, proposals } = this.props;
+    const { status, proposals } = this.props;
     const { hide } = this.state;
-    const startTime = now - block * 6000;
+    const { startTime, block } = status;
     let groups: CalendarGroup[] = [
       { id: 1, title: "RuntimeUpgrade" },
       { id: 2, title: "Council Round" },
@@ -75,7 +72,7 @@ class Calendar extends Component<IProps, IState> {
     const cycle = termDuration + announcing + voting + revealing;
     this.setState({ groups, items });
 
-    for (let round = 1; round * cycle < block + cycle; round++) {
+    for (let round = 1; round * cycle < block.id + cycle; round++) {
       items.push({
         id: items.length + 1,
         group: 2,
@@ -98,6 +95,7 @@ class Calendar extends Component<IProps, IState> {
       });
     }
     this.setState({ items });
+    return items;
   }
   toggleShowProposalType(id: number) {
     const { hide } = this.state;
@@ -111,11 +109,11 @@ class Calendar extends Component<IProps, IState> {
   }
 
   render() {
-    const { hide, items, groups } = this.state;
-
-    const first = items.sort((a, b) => a.end_time - b.end_time)[0];
+    const { hide, groups } = this.state;
+    const { status } = this.props;
 
-    if (!items.length) return <Loading />;
+    if (!status.block) return <Loading />;
+    const items = this.state.items || this.filterItems();
 
     const filters = (
       <div className="d-flex flew-row">
@@ -145,7 +143,7 @@ class Calendar extends Component<IProps, IState> {
           sidebarWidth={220}
           sidebarContent={filters}
           stackItems={true}
-          defaultTimeStart={moment(first.start_time).add(-1, "day")}
+          defaultTimeStart={moment(status.startTime).add(-1, "day")}
           defaultTimeEnd={moment().add(15, "day")}
           onItemSelect={this.openProposal}
         />

+ 11 - 17
src/components/Council/ElectionStatus.tsx

@@ -1,5 +1,5 @@
 import React from "react";
-import { domain } from "../../config";
+import { Status } from "../../types";
 
 const timeLeft = (blocks: number) => {
   const seconds = blocks * 6;
@@ -10,11 +10,12 @@ const timeLeft = (blocks: number) => {
 };
 
 const ElectionStage = (props: {
-  termEndsAt: number;
   block: number;
-  stage: any;
+  council: { stage; termEndsAt: number };
 }) => {
-  const { block, stage, termEndsAt } = props;
+  const { block, council } = props;
+  if (!council) return <div>Loading..</div>;
+  const { stage, termEndsAt } = council;
 
   if (!stage) {
     if (!block || !termEndsAt) return <div />;
@@ -38,22 +39,15 @@ const ElectionStage = (props: {
   return <div>{JSON.stringify(stage)}</div>;
 };
 
-const ElectionStatus = (props: {
-  councilElection?: { termEndsAt: number; round: number; stage: any };
-  block: number;
-  show: boolean;
-  stage: any;
-  termEndsAt: number;
-}) => {
-  const { councilElection, block, termEndsAt, show, stage } = props;
-  if (!show) return <div />;
+const ElectionStatus = (props: { domain: string; status: Status }) => {
+  const { domain, status } = props;
+  //if (!status.council) return <div />;
   return (
     <div className="position-absolute text-left text-light">
       <ElectionStage
-        stage={stage}
-        termEndsAt={termEndsAt}
-        block={block}
-        {...councilElection}
+        domain={domain}
+        council={status.council}
+        block={status.block.id}
       />
     </div>
   );

+ 71 - 63
src/components/Council/index.tsx

@@ -1,84 +1,92 @@
 import React from "react";
-import { Link } from "react-router-dom";
 import ElectionStatus from "./ElectionStatus";
 import MemberBox from "../Members/MemberBox";
-import Loading from "../Loading";
-import { Handles, Member, Post, ProposalDetail, Seat } from "../../types";
+import {
+  Handles,
+  Member,
+  Post,
+  ProposalDetail,
+  Seat,
+  Status,
+} from "../../types";
 
 const Council = (props: {
   councils: Seat[][];
   councilElection?: any;
-  block: number;
   members: Member[];
-  stage: any;
-  termEndsAt: number;
   proposals: ProposalDetail[];
   posts: Post[];
-  now: number;
   validators: string[];
   handles: Handles;
-  round: number;
+  status: Status;
 }) => {
-  const { councilElection, members, handles, round, termEndsAt } = props;
-
-  const council = props.councils[props.councils.length - 1];
-  const show = round && council;
-  const half = show ? Math.floor(council.length / 2) : 0;
-
-  const startTime = props.now - props.block * 6000;
+  const { councils, status, members, handles, posts, proposals } = props;
+  const council = councils[councils.length - 1];
+  if (!status || !council) return <div />;
+  const third = Math.floor(council.length / 3);
 
   return (
-    <div className="box">
-      <ElectionStatus
-        show={show}
-        stage={props.stage}
-        termEndsAt={termEndsAt}
-        block={props.block}
-        {...councilElection}
-      />
-      <h3>Council</h3>
+    <div className="box w-50 p-3 m-3">
+      <ElectionStatus domain={props.domain} status={status} />
 
-      {(show && (
+      <h3>Council</h3>
+      <div className="d-flex flex-row  justify-content-between">
+        <div className="d-flex flex-column">
+          {council.slice(0, third).map((m) => (
+            <div key={m.member} className="box">
+              <MemberBox
+                id={m.id || 0}
+                account={m.member}
+                handle={m.handle || handles[m.member]}
+                members={members}
+                councils={councils}
+                proposals={proposals}
+                placement={"bottom"}
+                posts={posts}
+                startTime={status.startTime}
+                validators={props.validators}
+              />
+            </div>
+          ))}
+        </div>
+        <div className="d-flex flex-column">
+          {council.slice(third, 2 * third).map((m) => (
+            <div key={m.member} className="box">
+              <MemberBox
+                id={m.id || 0}
+                account={m.member}
+                handle={m.handle || handles[m.member]}
+                members={members}
+                councils={councils}
+                proposals={proposals}
+                placement={"bottom"}
+                posts={posts}
+                startTime={status.startTime}
+                validators={props.validators}
+              />
+            </div>
+          ))}
+        </div>
         <div className="d-flex flex-column">
-          <div className="d-flex flex-row justify-content-between">
-            {council.slice(0, half).map((m) => (
-              <div key={m.member} className="box">
-                <MemberBox
-                  id={m.id || 0}
-                  account={m.member}
-                  handle={m.handle || handles[m.member]}
-                  members={members}
-                  councils={props.councils}
-                  proposals={props.proposals}
-                  placement={"top"}
-                  posts={props.posts}
-                  startTime={startTime}
-                  validators={props.validators}
-                />
-              </div>
-            ))}
-          </div>
-          <div className="d-flex flex-row justify-content-between">
-            {council.slice(half).map((m) => (
-              <div key={m.member} className="box">
-                <MemberBox
-                  key={m.member}
-                  id={m.id || 0}
-                  account={m.member}
-                  handle={m.handle || handles[m.member]}
-                  members={members}
-                  councils={props.councils}
-                  proposals={props.proposals}
-                  placement={"top"}
-                  posts={props.posts}
-                  startTime={startTime}
-                  validators={props.validators}
-                />
-              </div>
-            ))}
-          </div>
+          {council.slice(2 * third).map((m) => (
+            <div key={m.member} className="box">
+              <MemberBox
+                key={m.member}
+                id={m.id || 0}
+                account={m.member}
+                handle={m.handle || handles[m.member]}
+                members={members}
+                councils={councils}
+                proposals={proposals}
+                placement={"bottom"}
+                posts={posts}
+                startTime={status.startTime}
+                validators={props.validators}
+              />
+            </div>
+          ))}
         </div>
-      )) || <Loading />}
+      </div>
     </div>
   );
 };

+ 1 - 1
src/components/Councils/CouncilVotes.tsx

@@ -56,7 +56,7 @@ class CouncilVotes extends Component<IProps, IState> {
       <Table className="text-light text-center">
         <thead onClick={this.toggleExpand}>
           <tr>
-            <th colSpan={councilMembers.length + 1}>Round {round}</th>
+            <th colSpan={council.length + 1}>Round {round}</th>
           </tr>
         </thead>
         <tbody>

+ 49 - 80
src/components/Dashboard/index.tsx

@@ -1,9 +1,10 @@
 import React from "react";
 import { Link } from "react-router-dom";
 import { Council } from "..";
-import Proposals from "../Proposals/ProposalTable";
-import Post from "../Forum/LatestPost";
+import Forum from "./Forum";
+import Proposals from "./Proposals";
 import Footer from "./Footer";
+import Status from "./Status";
 import Validators from "../Validators";
 import { IState } from "../../types";
 
@@ -14,18 +15,29 @@ interface IProps extends IState {
 
 const Dashboard = (props: IProps) => {
   const {
-    connecting,
-    block,
-    now,
+    toggleStar,
+    toggleFooter,
+    hideFooter,
+    councils,
     domain,
     handles,
     members,
+    nominators,
     posts,
     proposals,
+    rewardPoints,
     threads,
     tokenomics,
+    status,
+    stars,
+    stashes,
+    stakes,
+    validators,
   } = props;
   const userLink = `${domain}/#/members/joystreamstats`;
+
+  //console.log(`status`, status);
+
   return (
     <div className="w-100 flex-grow-1 d-flex align-items-center justify-content-center d-flex flex-column">
       <div className="back bg-warning d-flex flex-column p-2">
@@ -41,97 +53,54 @@ const Dashboard = (props: IProps) => {
       </h1>
 
       <Council
-        councils={props.councils}
+        councils={councils}
         members={members}
-        councilElection={props.councilElection}
-        block={block}
-        now={now}
-        round={props.round}
-        handles={props.handles}
-        termEndsAt={props.termEndsAt}
-        stage={props.stage}
-        posts={props.posts}
-        proposals={props.proposals}
-        validators={props.validators}
+        handles={handles}
+        posts={posts}
+        proposals={proposals}
+        stars={stars}
+        status={status}
+        validators={validators}
       />
 
-      <div className="w-100 p-3 m-3">
-        <div className="d-flex flex-row">
-          <h3 className="ml-1 text-light">Active Proposals</h3>
-          <Link className="m-3 text-light" to={"/proposals"}>
-            All
-          </Link>
-          <Link className="m-3 text-light" to={`/spending`}>
-            Spending
-          </Link>
-          <Link className="m-3 text-light" to={"/councils"}>
-            Votes
-          </Link>
-        </div>
-        <Proposals
-          hideNav={true}
-          startTime={now - block * 6000}
-          block={block}
-          proposals={proposals.filter((p) => p && p.result === "Pending")}
-          proposalPosts={props.proposalPosts}
-          members={members}
-          councils={props.councils}
-          posts={posts}
-          validators={props.validators}
-        />
-      </div>
+      <Proposals
+        members={members}
+        councils={councils}
+        posts={posts}
+        proposals={proposals}
+        proposalPosts={props.proposalPosts}
+        validators={validators}
+      />
 
-      <div className="w-100 p-3 m-3 d-flex flex-column">
-        <h3>
-          <Link className="text-light" to={"/forum"}>
-            Forum
-          </Link>
-        </h3>
-        {posts
-          .sort((a, b) => b.id - a.id)
-          .slice(0, 10)
-          .map((post) => (
-            <Post
-              key={post.id}
-              selectThread={() => {}}
-              handles={handles}
-              post={post}
-              thread={threads.find((t) => t.id === post.threadId)}
-              startTime={now - block * 6000}
-            />
-          ))}
-      </div>
+      <Forum posts={posts} threads={threads} startTime={status.startTime} />
 
       <div className="w-100 p-3 m-3">
         <Validators
-          block={block}
-          era={props.era}
-          now={now}
-          lastReward={props.lastReward}
-          councils={props.councils}
+          hideBackButton={true}
+          toggleStar={toggleStar}
+          councils={councils}
           handles={handles}
           members={members}
-          posts={props.posts}
-          proposals={props.proposals}
-          nominators={props.nominators}
-          validators={props.validators}
-          stashes={props.stashes}
-          stars={props.stars}
-          stakes={props.stakes}
-          toggleStar={props.toggleStar}
-          rewardPoints={props.rewardPoints}
+          posts={posts}
+          proposals={proposals}
+          nominators={nominators}
+          validators={validators}
+          stashes={stashes}
+          stars={stars}
+          stakes={stakes}
+          rewardPoints={rewardPoints}
           tokenomics={tokenomics}
-          hideBackButton={true}
+          status={status}
         />
       </div>
 
       <Footer
-        toggleHide={props.toggleFooter}
-        show={!props.hideFooter}
-        connecting={connecting}
+        show={!hideFooter}
+        toggleHide={toggleFooter}
+        connecting={status.connecting}
         link={userLink}
       />
-      {connecting ? <div className="connecting">Connecting ..</div> : ""}
+      <Status connecting={status.connecting} loading={status.loading} />
     </div>
   );
 };

+ 4 - 10
src/components/Forum/index.tsx

@@ -97,17 +97,11 @@ class Forum extends React.Component<IProps, IState> {
   }
 
   render() {
-    const { block, now, handles, categories, posts, threads } = this.props;
+    const { handles, categories, posts, threads, status } = this.props;
     const { categoryId, threadId, searchTerm } = this.state;
 
-    const startTime: number = now - block * 6000;
-
-    const category = categoryId
-      ? categories.find((c) => c.id === categoryId)
-      : undefined;
-    const thread = threadId
-      ? threads.find((t) => t.id === threadId)
-      : undefined;
+    const category = categories.find((c) => c.id === categoryId);
+    const thread = threads.find((t) => t.id === threadId);
 
     return (
       <div className="h-100 overflow-hidden bg-dark">
@@ -133,7 +127,7 @@ class Forum extends React.Component<IProps, IState> {
           category={category}
           thread={thread}
           handles={handles}
-          startTime={startTime}
+          startTime={status.startTime}
           searchTerm={searchTerm}
           filterPosts={this.filterPosts}
           filterThreads={this.filterThreads}

+ 9 - 96
src/components/Members/Member.tsx

@@ -1,15 +1,12 @@
 import React from "react";
 import { Member, Post, ProposalDetail, Seat, Thread } from "../../types";
-import { Link } from "react-router-dom";
-import { Badge, ListGroup } from "react-bootstrap";
 import { domain } from "../../config";
 import Summary from "./Summary";
+import Posts from "./MemberPosts";
+import Proposals from "./MemberProposals";
 import Loading from "../Loading";
 import NotFound from "./NotFound";
 import Back from "../Back";
-import moment from "moment";
-import Markdown from "react-markdown";
-import gfm from "remark-gfm";
 
 const MemberBox = (props: {
   match: { params: { handle: string } };
@@ -18,12 +15,11 @@ const MemberBox = (props: {
   proposals: ProposalDetail[];
   posts: Post[];
   threads: Thread[];
-  block: number;
-  now: number;
   validators: string[];
-  history:any
+  history: any;
+  status: { startTime: number };
 }) => {
-  const { block, now, councils, members, posts, proposals } = props;
+  const { councils, members, posts, proposals, status } = props;
   const h = props.match.params.handle;
   const member = members.find(
     (m) => m.handle === h || String(m.account) === h || m.id === Number(h)
@@ -35,7 +31,6 @@ const MemberBox = (props: {
   const isCouncilMember = council.find(
     (seat) => seat.member === member.account
   );
-  const startTime = now - block * 6000;
 
   const threadTitle = (id: number) => {
     const thread = props.threads.find((t) => t.id === id);
@@ -44,7 +39,7 @@ const MemberBox = (props: {
 
   return (
     <div>
-        <Back history={props.history} />
+      <Back history={props.history} />
       <div className="box">
         {isCouncilMember && <div>council member</div>}
         <a href={`${domain}/#/members/${member.handle}`}>
@@ -58,105 +53,23 @@ const MemberBox = (props: {
           member={member}
           posts={posts}
           proposals={proposals}
-          startTime={now - block * 6000}
+          startTime={status.startTime}
           validators={props.validators}
         />
       </div>
 
       <Proposals
         proposals={proposals.filter((p) => p && p.authorId === member.id)}
-        startTime={startTime}
+        startTime={status.startTime}
       />
 
       <Posts
         posts={posts.filter((p) => p.authorId === member.account)}
         threadTitle={threadTitle}
-        startTime={startTime}
+        startTime={status.startTime}
       />
     </div>
   );
 };
 
-const Posts = (props: {
-  posts: Post[];
-  threadTitle: (id: number) => string;
-  startTime: number;
-}) => {
-  const { posts, startTime, threadTitle } = props;
-
-  if (!posts.length) return <div />;
-
-  return (
-    <div className="mt-3">
-      <h3 className="text-center text-light">Latest Posts</h3>
-      {posts
-        .sort((a, b) => b.id - a.id)
-        .slice(0, 5)
-        .map((p) => (
-          <div key={p.id} className="box d-flex flex-row">
-            <div className="col-2 mr-3">
-              <div>
-                {moment(startTime + p.createdAt.block * 6000).fromNow()}
-              </div>
-              <a href={`${domain}/#/forum/threads/${p.threadId}`}>reply</a>
-            </div>
-
-            <div>
-              <Link to={`/forum/threads/${p.threadId}`}>
-                <div className="text-left font-weight-bold mb-3">
-                  {threadTitle(p.threadId)}
-                </div>
-              </Link>
-
-              <Markdown
-                plugins={[gfm]}
-                className="overflow-auto text-left"
-                children={p.text}
-              />
-            </div>
-          </div>
-        ))}
-    </div>
-  );
-};
-
-const Proposals = (props: {
-  proposals: ProposalDetail[];
-  startTime: number;
-}) => {
-  const { proposals, startTime } = props;
-
-  if (!proposals.length) return <div />;
-
-  return (
-    <div className="mt-3">
-      <h3 className="text-center text-light">Latest Proposals</h3>
-      <ListGroup className="box">
-        {proposals
-          .sort((a, b) => b.id - a.id)
-          .slice(0, 5)
-          .map((p) => (
-            <ListGroup.Item key={p.id} className="box bg-secondary">
-              <Link to={`/proposals/${p.id}`}>
-                <Badge
-                  className="mr-2"
-                  variant={
-                    p.result === "Approved"
-                      ? "success"
-                      : p.result === "Pending"
-                      ? "warning"
-                      : "danger"
-                  }
-                >
-                  {p.result}
-                </Badge>
-                {p.title}, {moment(startTime + p.createdAt * 6000).fromNow()}
-              </Link>
-            </ListGroup.Item>
-          ))}
-      </ListGroup>
-    </div>
-  );
-};
-
 export default MemberBox;

+ 3 - 5
src/components/Members/index.tsx

@@ -10,10 +10,9 @@ interface IProps {
   handles: Handles;
   proposals: ProposalDetail[];
   posts: Post[];
-  now: number;
-  block: number;
   validators: string[];
   history:any
+  status:{startTime:number}
 }
 
 interface IState {
@@ -31,7 +30,7 @@ class Members extends React.Component<IProps, IState> {
   }
 
   render() {
-    const { councils, handles, members, posts, proposals } = this.props;
+    const { councils, handles, members, posts, proposals, status } = this.props;
     let unique: Member[] = [];
     members.forEach(
       (m) => unique.find((member) => member.id === m.id) || unique.push(m)
@@ -39,7 +38,6 @@ class Members extends React.Component<IProps, IState> {
     unique = unique.sort((a, b) => +a.id - +b.id);
     if (!unique.length) return <Loading />;
 
-    const startTime = this.props.now - this.props.block * 6000;
     const quart = Math.floor(unique.length / 4) + 1;
     const cols = [
       unique.slice(0, quart),
@@ -66,7 +64,7 @@ class Members extends React.Component<IProps, IState> {
                     proposals={proposals}
                     placement={index === 3 ? "left" : "bottom"}
                     posts={posts}
-                    startTime={startTime}
+                    startTime={status.startTime}
                     validators={this.props.validators}
                   />
                 </div>

+ 2 - 2
src/components/Proposals/Detail.tsx

@@ -11,10 +11,10 @@ const Detail = (props: { detail?: any; type: string }) => {
 
   if (type === "Spending")
     return (
-      <p>
+      <>
         <b>Spending</b>
         <p>{amount(detail.Spending[0])} M tJOY</p>
-      </p>
+      </>
     );
 
   if (type === "SetWorkingGroupMintCapacity")

+ 2 - 1
src/components/Proposals/NavBar.tsx

@@ -5,7 +5,7 @@ import { Sliders } from "react-feather";
 
 const NavBar = (props: any) => {
   const { authors, show } = props;
-if (!show) return <div/>
+  if (!show) return <div />;
   return (
     <Navbar bg="dark" variant="dark">
       <Link to="/">
@@ -22,6 +22,7 @@ if (!show) return <div/>
           <NavDropdown.Item
             className={"All" === props.author ? "bg-dark text-light" : ""}
             onClick={props.selectAuthor}
+            value=""
           >
             All
           </NavDropdown.Item>

+ 30 - 22
src/components/Proposals/ProposalTable.tsx

@@ -25,7 +25,7 @@ interface IState {
   author: string;
   key: any;
   asc: boolean;
-  hidden: string[];
+  selectedTypes: string[];
   showTypes: boolean;
   page: number;
 }
@@ -36,13 +36,13 @@ class ProposalTable extends React.Component<IProps, IState> {
     this.state = {
       key: "id",
       asc: false,
-      hidden: [],
-      author: "All",
+      selectedTypes: [],
+      author: "",
       showTypes: false,
       page: 1,
     };
     this.selectAuthor = this.selectAuthor.bind(this);
-    this.toggleHide = this.toggleHide.bind(this);
+    this.toggleShowType = this.toggleShowType.bind(this);
     this.toggleShowTypes = this.toggleShowTypes.bind(this);
     this.setPage = this.setPage.bind(this);
     this.setKey = this.setKey.bind(this);
@@ -59,25 +59,27 @@ class ProposalTable extends React.Component<IProps, IState> {
   toggleShowTypes() {
     this.setState({ showTypes: !this.state.showTypes });
   }
-  toggleHide(type: string) {
-    const isHidden = this.state.hidden.includes(type);
-    const hidden = isHidden
-      ? this.state.hidden.filter((h) => h !== type)
-      : this.state.hidden.concat(type);
-    this.setState({ hidden });
+  toggleShowType(type: string) {
+    const selected = this.state.selectedTypes.includes(type);
+    const selectedTypes = selected
+      ? this.state.selectedTypes.filter((t) => t !== type)
+      : this.state.selectedTypes.concat(type);
+    this.setState({ selectedTypes });
   }
   selectAuthor(event: any) {
     this.setState({ author: event.target.text });
   }
 
-  filterProposals() {
-    const proposals = this.props.proposals.filter(
-      (p) => !this.state.hidden.find((h) => h === p.type)
-    );
-    const { author } = this.state;
-    if (author === "All") return proposals;
+  filterProposals(proposals = this.props.proposals) {
+    return this.filterByAuthor(this.filterByType(proposals));
+  }
+  filterByAuthor(proposals, author = this.state.author) {
+    if (!author.length) return proposals;
     return proposals.filter((p) => p.author === author);
   }
+  filterByType(proposals, types = this.state.selectedTypes) {
+    return types.length ? filter((p) => types.includes(p.type)) : proposals;
+  }
   sortProposals(list: ProposalDetail[]) {
     const { asc, key } = this.state;
     if (key === "id" || key === "createdAt" || key === "finalizedAt")
@@ -98,7 +100,7 @@ class ProposalTable extends React.Component<IProps, IState> {
       posts,
       proposalPosts,
     } = this.props;
-    const { page, author, hidden } = this.state;
+    const { page, author, selectedTypes } = this.state;
 
     // proposal types
     let types: { [key: string]: number } = {};
@@ -108,7 +110,9 @@ class ProposalTable extends React.Component<IProps, IState> {
     let authors: { [key: string]: number } = {};
     this.props.proposals.forEach((p) => authors[p.author]++);
 
-    const proposals = this.sortProposals(this.filterProposals());
+    const proposals = this.sortProposals(
+      this.filterProposals(this.props.proposals)
+    );
     const approved = proposals.filter((p) => p.result === "Approved").length;
 
     // calculate finalization times
@@ -119,11 +123,14 @@ class ProposalTable extends React.Component<IProps, IState> {
     );
 
     // calculate mean voting duration
-    const avgBlocks =
-      durations.reduce((a: number, b: number) => a + b) / durations.length;
+    let avgBlocks = 0;
+    if (durations.length)
+      avgBlocks =
+        durations.reduce((a: number, b: number) => a + b) / durations.length;
     const avgDays = Math.floor(avgBlocks / 14400);
     const avgHours = Math.floor((avgBlocks - avgDays * 14400) / 600);
 
+    if (!proposals.length) return <div />;
     return (
       <div className="h-100 overflow-hidden bg-light">
         <NavBar
@@ -135,13 +142,14 @@ class ProposalTable extends React.Component<IProps, IState> {
         />
 
         <Types
-          hidden={hidden}
+          selected={selectedTypes}
           show={this.state.showTypes}
-          toggleHide={this.toggleHide}
+          toggleShow={this.toggleShowType}
           types={types}
         />
 
         <Head
+          show={!hideNav}
           setKey={this.setKey}
           approved={approved}
           proposals={proposals.length}

+ 4 - 3
src/components/Proposals/Row.tsx

@@ -40,7 +40,7 @@ const ProposalRow = (props: {
   author: string;
   id: number;
   parameters: ProposalParameters;
-  exec: boolean;
+  executed?: any;
   result: string;
   stage: string;
   title: string;
@@ -60,6 +60,7 @@ const ProposalRow = (props: {
     block,
     createdAt,
     description,
+    executed,
     finalizedAt,
     author,
     id,
@@ -72,7 +73,7 @@ const ProposalRow = (props: {
 
   const url = `https://pioneer.joystreamstats.live/#/proposals/${id}`;
   let result: string = props.result ? props.result : props.stage;
-  if (props.exec) result = "Executing";
+  if (executed) result = Object.keys(props.executed)[0]
   const color = colors[result];
 
   const created = formatTime(props.startTime + createdAt * 6000);
@@ -89,7 +90,7 @@ const ProposalRow = (props: {
   const duration = blocks ? `${daysStr} ${hoursStr} / ${blocks} blocks` : "";
 
   return (
-    <div className="d-flex flex-row justify-content-between text-left p-2">
+    <div className="d-flex flex-row justify-content-between text-left px-2">
       <div className="col-3">
         <OverlayTrigger
           key={id}

+ 2 - 2
src/components/Proposals/TableHead.tsx

@@ -1,8 +1,8 @@
 import React from "react";
-import { OverlayTrigger, Tooltip } from "react-bootstrap";
 
 const TableHead = (props: any) => {
-  const { setKey, approved, proposals, avgDays, avgHours } = props;
+  const { showNav,setKey, approved, proposals, avgDays, avgHours } = props;
+  if (!showNav) return <div/>
   return (
     <div className="d-flex flex-row justify-content-between p-2 bg-dark text-light text-left font-weight-bold">
       <div className="col-3">

+ 9 - 4
src/components/Proposals/Types.tsx

@@ -1,17 +1,22 @@
 import React from "react";
 import { Button } from "react-bootstrap";
 
-const Types = (props: any) => {
-  const { toggleHide, hidden, show, types } = props;
+const Types = (props: {
+  toggleShow: (type: string) => void;
+  selected: string[];
+  show: boolean;
+  types: { [key: string]: number };
+}) => {
+  const { toggleShow, selected, show, types } = props;
   if (!show) return <div />;
   return (
     <div className="bg-dark p-2">
       {Object.keys(types).map((type) => (
         <Button
           key={type}
-          variant={hidden.includes(type) ? "secondary" : "outline-light"}
+          variant={selected.includes(type) ? "secondary" : "outline-light"}
           className="btn-sm m-1"
-          onClick={() => toggleHide(type)}
+          onClick={() => toggleShow(type)}
         >
           {type}
         </Button>

+ 3 - 6
src/components/Proposals/index.tsx

@@ -4,8 +4,7 @@ import Loading from "..//Loading";
 import ProposalTable from "./ProposalTable";
 
 const Proposals = (props: {
-  now: number;
-  block: number;
+  status: { startTime: number };
   proposals: ProposalDetail[];
   proposalPosts: ProposalPost[];
   members: Member[];
@@ -15,11 +14,9 @@ const Proposals = (props: {
   posts: Post[];
   validators: string[];
 }) => {
-  const { proposalPosts, block, now, members } = props;
-  const startTime: number = now - block * 6000;
+  const { proposalPosts, members, status } = props;
 
   // prepare proposals
-
   //  - remove empty
   const proposals = props.proposals
     .filter((p) => p)
@@ -41,7 +38,7 @@ const Proposals = (props: {
       members={members}
       proposals={proposals}
       proposalPosts={proposalPosts}
-      startTime={startTime}
+      startTime={status.startTime}
       councils={props.councils}
       posts={props.posts}
       validators={props.validators}

+ 1 - 7
src/components/Routes/index.tsx

@@ -41,13 +41,7 @@ const Routes = (props: IProps) => {
       />
       <Route
         path="/proposals/:id"
-        render={(routeprops) => (
-          <Proposal
-            fetchProposal={props.fetchProposal}
-            {...routeprops}
-            {...props}
-          />
-        )}
+        render={(routeprops) => <Proposal {...routeprops} {...props} />}
       />
       <Route path="/proposals" render={() => <Proposals {...props} />} />
       <Route

+ 8 - 10
src/components/Timeline/index.tsx

@@ -1,17 +1,15 @@
 import React from "react";
-import { Link } from "react-router-dom";
-import { Button } from "react-bootstrap";
 import TimelineItem from "./Item";
+import { Back } from "..";
+
 import { Event, Post, ProposalDetail } from "../../types";
 
 const Timeline = (props: {
   posts: Post[];
   proposals: ProposalDetail[];
-  block: number;
-  now: number;
+  status: { startTime: number };
 }) => {
-  const { block, now, posts, proposals } = props;
-  const startTime: number = now - block * 6000;
+  const { posts, proposals, status } = props;
   let events: Event[] = [];
 
   proposals.forEach(
@@ -52,14 +50,14 @@ const Timeline = (props: {
 
   return (
     <div className="timeline-container">
-      <Link className="back left" to={"/"}>
-        <Button variant="secondary">Back</Button>
-      </Link>
+      <div className="back left">
+        <Back history={props.history} />
+      </div>
 
       {events
         .sort((a, b) => b.date - a.date)
         .map((event: Event, idx) => (
-          <TimelineItem event={event} key={idx} startTime={startTime} />
+          <TimelineItem event={event} key={idx} startTime={status.startTime} />
         ))}
     </div>
   );

+ 20 - 7
src/components/Validators/MinMax.tsx

@@ -34,6 +34,7 @@ const MinMax = (props: {
 
   const name = { className: "text-right" };
   const value = { className: "text-left" };
+  const validatorReward = reward ? reward / validators.length : 0;
 
   return (
     <Table className="bg-secondary w-50">
@@ -73,22 +74,34 @@ const MinMax = (props: {
         <tr>
           <td {...name}>Total payed per hour</td>
           <td {...value}>
-            {reward} JOY ({dollar(price * reward)}){" "}
+            {reward ? `${reward} JOY (${dollar(price * reward)})` : "Loading.."}
           </td>
         </tr>
         <tr>
           <td {...name}>Reward per validator per hour</td>
           <td className="text-left text-warning">
-            {(reward / validators.length).toFixed(0)} JOY (
-            {dollar((price * reward) / validators.length)})
-            <Link className="ml-1" to={"/mint"}>
-              Details
-            </Link>
+            <ValidatorReward
+              show={reward}
+              price={price}
+              reward={validatorReward}
+            />
           </td>
         </tr>
       </tbody>
     </Table>
   );
 };
-
 export default MinMax;
+
+const ValidatorReward = (props) => {
+  const { show, price, reward } = props;
+  if (!show) return `Loading..`;
+  return (
+    <>
+      {reward.toFixed(0)} JOY ({dollar(price * reward)})
+      <Link className="ml-1" to={"/mint"}>
+        Details
+      </Link>
+    </>
+  );
+};

+ 1 - 1
src/components/Validators/Validator.tsx

@@ -1,5 +1,5 @@
 import React, { Component } from "react";
-import { Activity, Minus, Star } from "react-feather";
+import { Activity, Star } from "react-feather";
 import Nominators from "./Nominators";
 import MemberBox from "../Members/MemberBox";
 import {

+ 27 - 55
src/components/Validators/index.tsx

@@ -1,9 +1,10 @@
 import React, { Component } from "react";
-import { Link } from "react-router-dom";
-import { Button, ListGroup } from "react-bootstrap";
+import { Button } from "react-bootstrap";
 import Stats from "./MinMax";
 import Validator from "./Validator";
-import MemberBox from "../Members/MemberBox";
+import Waiting from "./Waiting";
+import { Back } from "..";
+
 import {
   Handles,
   Member,
@@ -12,18 +13,16 @@ import {
   Seat,
   Stakes,
   RewardPoints,
+  Status,
   Tokenomics,
 } from "../../types";
 
 interface IProps {
-  era: number;
   councils: Seat[][];
   handles: Handles;
   members: Member[];
   posts: Post[];
   proposals: ProposalDetail[];
-  now: number;
-  block: number;
   validators: string[];
   stashes: string[];
   nominators: string[];
@@ -31,9 +30,9 @@ interface IProps {
   toggleStar: (account: string) => void;
   stakes?: { [key: string]: Stakes };
   rewardPoints?: RewardPoints;
-  lastReward: number;
   tokenomics?: Tokenomics;
   hideBackButton?: boolean;
+  status: Status;
 }
 
 interface IState {
@@ -112,9 +111,7 @@ class Validators extends Component<IProps, IState> {
   render() {
     const {
       hideBackButton,
-      block,
-      era,
-      now,
+      history,
       councils,
       handles,
       members,
@@ -124,30 +121,29 @@ class Validators extends Component<IProps, IState> {
       nominators,
       stashes,
       stars,
-      lastReward,
       rewardPoints,
+      status,
       stakes,
       tokenomics,
     } = this.props;
-    const issued = tokenomics ? Number(tokenomics.totalIssuance) : 0;
-    const price = tokenomics ? Number(tokenomics.price) : 0;
 
+    if (!status || !status.block) return <div />;
+
+    const { lastReward, block, era, startTime } = status;
     const { sortBy, showWaiting, showValidators } = this.state;
-    const startTime = now - block * 6000;
+
+    const issued = tokenomics ? Number(tokenomics.totalIssuance) : 0;
+    const price = tokenomics ? Number(tokenomics.price) : 0;
 
     const starred = stashes.filter((v) => stars[v]);
     const unstarred = validators.filter((v) => !stars[v]);
     const waiting = stashes.filter((s) => !stars[s] && !validators.includes(s));
+
     if (!unstarred.length) return <div />;
+
     return (
-      <div className="box w-100 !mx-5">
-        {hideBackButton ? (
-          ""
-        ) : (
-          <Link className="back" to={"/"}>
-            <Button variant="secondary">Back</Button>
-          </Link>
-        )}
+      <div className="box w-100 mx-5">
+        <Back hide={hideBackButton} history={history} />
         <h3 onClick={() => this.toggleValidators()}>Validator Stats</h3>
 
         <Stats
@@ -159,7 +155,7 @@ class Validators extends Component<IProps, IState> {
           validators={validators}
           nominators={nominators.length}
           waiting={waiting.length}
-          reward={lastReward}
+          reward={status.lastReward}
         />
 
         <div className="d-flex flex-column">
@@ -208,38 +204,14 @@ class Validators extends Component<IProps, IState> {
               />
             ))}
 
-          <Button
-            variant="secondary"
-            className="mb-5"
-            onClick={() => this.toggleWaiting()}
-          >
-            Toggle {waiting.length} waiting nodes
-          </Button>
-
-          {waiting.length && showWaiting ? (
-            <ListGroup className="waiting-validators">
-              <hr />
-              <h4 onClick={() => this.toggleWaiting()}>Waiting</h4>
-              {waiting.map((v) => (
-                <ListGroup.Item key={v}>
-                  <MemberBox
-                    id={0}
-                    account={v}
-                    placement={"top"}
-                    councils={councils}
-                    handle={handles[v]}
-                    members={members}
-                    posts={posts}
-                    proposals={proposals}
-                    startTime={startTime}
-                    validators={validators}
-                  />
-                </ListGroup.Item>
-              ))}
-            </ListGroup>
-          ) : (
-            ""
-          )}
+          <Waiting
+            show={showWaiting}
+            waiting={waiting}
+            posts={posts}
+            proposals={proposals}
+            members={members}
+            handles={handles}
+          />
         </div>
       </div>
     );

+ 5 - 5
src/components/Votes.tsx

@@ -78,10 +78,10 @@ export const VotesBubbles = (props: {
 
 // Tooltip
 
-interface IProps {
-  votes: VotingResults;
-  votesByAccount?: Vote[];
-}
+//interface IProps {
+//  votes: VotingResults;
+//  votesByAccount?: Vote[];
+//}
 // TODO Property 'votes' does not exist on type 'IntrinsicAttributes & IProps'
 // https://stackoverflow.com/questions/59969756/not-assignable-to-type-intrinsicattributes-intrinsicclassattributes-react-js
 
@@ -103,7 +103,7 @@ export const VotesTooltip = (props: any) => {
   return (
     <div className="text-left text-light">
       {votes.map((vote: Vote) => (
-        <VoteButton {...vote} />
+        <VoteButton key={vote.handle} {...vote} />
       ))}
     </div>
   );

+ 2 - 0
src/config.ts

@@ -1,2 +1,4 @@
 export const domain = "https://testnet.joystream.org";
 export const wsLocation = "wss://joystreamstats.live:9945";
+export const apiLocation = "https://api.joystreamstats.live/api/v1"
+export const socketLocation = "/socket.io"

+ 6 - 5
src/index.css

@@ -67,7 +67,7 @@ table td {
 }
 
 .title {
-  position: fixed;
+  position: absolute;
   top: 0px;
   left: 0px;
 }
@@ -263,7 +263,7 @@ table td {
   padding: 5px;
 }
 .back {
-  position: fixed;
+  position: absolute;
   right: 0px;
   top: 0px;
 }
@@ -271,12 +271,13 @@ table td {
   left: 0px;
 }
 .footer {
-  background: teal;
+  background: black;
+  text: white;
+  text-align: center;
   position: fixed;
   bottom: 0px;
-  padding: 10px;
+  padding: 5px;
   width: 100%;
-  text-align: center;
 }
 .footer-hidden {
   position: fixed;

+ 21 - 9
src/types.ts

@@ -15,28 +15,41 @@ export interface Api {
   derive: any;
 }
 
-export interface IState {
-  //gethandle: (account: AccountId | string)  => string;
-  connecting: boolean;
+export interface Status {
   now: number;
+  block: Block;
   era: number;
   block: number;
+  connecting: boolean;
+  loading: string;
+  council?: { stage: any; round: number; termEndsAt: number };
+  issued: number;
+  price: number;
+  proposals: number;
+  channels: number;
+  categories: number;
+  threads: number;
+  posts: number;
+  lastReward: number;
+  startTime: number;
+}
+
+export interface IState {
+  //gethandle: (account: AccountId | string)  => string;
+  status: Status;
   blocks: Block[];
   nominators: string[];
   validators: string[];
   stashes: string[];
-  loading: boolean;
+  queue: { key: string; action: any }[];
+  loading: string;
   councils: Seat[][];
-  councilElection?: { stage: any; round: number; termEndsAt: number };
   channels: Channel[];
   categories: Category[];
-  issued: number;
-  price: number;
   proposals: ProposalDetail[];
   posts: Post[];
   threads: Thread[];
   domain: string;
-  proposalCount: number;
   proposalPosts: any[];
   handles: Handles;
   members: Member[];
@@ -46,7 +59,6 @@ export interface IState {
   stars: { [key: string]: boolean };
   stakes?: { [key: string]: Stakes };
   rewardPoints?: RewardPoints;
-  lastReward: number;
   hideFooter: boolean;
 }