3
1
Эх сурвалжийг харах

fetch proposals posts members from jsstats API

Joystream Stats 3 жил өмнө
parent
commit
7500e4ea79

+ 71 - 362
src/App.tsx

@@ -5,27 +5,12 @@ import { Modals, Routes, Loading, Footer, Status } from "./components";
 
 import * as get from "./lib/getters";
 import { domain, wsLocation } from "./config";
-//import proposalPosts from "./proposalPosts";
 import axios from "axios";
-import { ProposalDetail } from "./types";
-//import socket from "./socket";
-
-import {
-  Api,
-  Handles,
-  IState,
-  Member,
-  Category,
-  Channel,
-  Post,
-  Seat,
-  Thread,
-  //  Status,
-} from "./types";
+
+import { Api, IState } from "./types";
 import { types } from "@joystream/types";
 import { ApiPromise, WsProvider } from "@polkadot/api";
 import { Header } from "@polkadot/types/interfaces";
-import { VoteKind } from "@joystream/types/proposals";
 
 interface IProps {}
 
@@ -37,7 +22,6 @@ const initialState = {
   connected: false,
   fetching: "",
   tasks: 0,
-  queue: [],
   blocks: [],
   nominators: [],
   validators: [],
@@ -49,7 +33,6 @@ const initialState = {
   threads: [],
   proposals: [],
   domain,
-  handles: {},
   members: [],
   proposalPosts: [],
   providers: [],
@@ -164,7 +147,7 @@ class App extends React.Component<IProps, IState> {
   }
 
   async handleBlock(api, header: Header) {
-    let { blocks, status, queue } = this.state;
+    let { blocks, status } = this.state;
     const id = header.number.toNumber();
     if (blocks.find((b) => b.id === id)) return;
     const timestamp = (await api.query.timestamp.now()).toNumber();
@@ -180,15 +163,18 @@ class App extends React.Component<IProps, IState> {
       this.updateStatus(api, id);
       this.fetchTokenomics();
     }
-    if (!queue.length) this.findJob(api);
+    if (!status.lastReward) this.fetchLastReward(api);
   }
 
   async updateStatus(api: Api, id = 0) {
     console.debug(`Updating status for block ${id}`);
 
-    let { status } = this.state;
+    let { status, councils } = this.state;
     status.era = await this.updateEra(api);
-    status.council = await this.updateCouncil(api);
+
+    councils.forEach((c) => {
+      if (c.round > status.council) status.council = c;
+    });
 
     const nextMemberId = await await api.query.members.nextMemberId();
     status.members = nextMemberId - 1;
@@ -199,8 +185,7 @@ class App extends React.Component<IProps, IState> {
     //status.channels = await get.currentChannelId(api);
     status.proposalPosts = await api.query.proposalsDiscussion.postCount();
     status.version = version;
-    await this.save("status", status);
-    this.findJob(api);
+    this.save("status", status);
   }
 
   async updateEra(api: Api) {
@@ -212,52 +197,51 @@ class App extends React.Component<IProps, IState> {
       console.debug(`Updating validators`);
       this.fetchLastReward(api, status.era);
       const validators = await this.fetchValidators(api);
-      this.enqueue("stakes", () => this.fetchStakes(api, era, validators));
+      this.fetchStakes(api, era, validators);
     } else if (!status.lastReward) this.fetchLastReward(api);
     return era;
   }
 
-  // queue management
-  enqueue(key: string, action: () => void) {
-    this.setState({ queue: this.state.queue.concat({ key, action }) });
-    this.processTask();
+  async fetchCouncils() {
+    const { data } = await axios.get(
+      `https://api.joystreamstats.live/api/v1/councils`
+    );
+    if (!data || data.error) return console.error(`failed to fetch from API`);
+    console.debug(`councils`, data);
+    this.save("councils", data);
+
+    // TODO OPTIMIZE find max round
+    let council = { round: 0 };
+    data.forEach((c) => {
+      if (c.round > council.round) council = c;
+    });
+    let { status } = this.state;
+    status.council = council;
+    this.save("status", status);
   }
-
-  findJob(api: Api) {
-    const { status, proposals, posts, members } = this.state;
-    if (!status.lastReward) this.fetchLastReward(api);
-    if (
-      status.council &&
-      status.council.stageEndsAt > 0 &&
-      status.council.stageEndsAt < status.block.id
-    )
-      this.updateCouncil(api);
-    if (
-      status.proposals > proposals.filter((p) => p && p.votesByAccount).length
-    )
-      this.fetchProposal(api, status.proposals);
-    if (status.posts > posts.length) this.fetchPost(api, status.posts);
-    if (status.members > members.length) this.fetchMember(api, status.members);
-  }
-
-  async processTask() {
-    // check status
-    let { tasks } = this.state;
-    if (tasks > 1) return;
-    if (tasks < 1) setTimeout(() => this.processTask(), 0);
-
-    // pull task
-    let { queue } = this.state;
-    const task = queue.shift();
-    if (!task) {
-      if (!tasks) this.setState({ fetching: "" });
-      return;
-    }
-
-    this.setState({ fetching: task.key, queue, tasks: tasks + 1 });
-    await task.action();
-    this.setState({ tasks: this.state.tasks - 1 });
-    setTimeout(() => this.processTask(), 100);
+  async fetchProposals() {
+    const { data } = await axios.get(
+      `https://api.joystreamstats.live/api/v1/proposals`
+    );
+    if (!data || data.error) return console.error(`failed to fetch from API`);
+    console.debug(`proposals`, data);
+    this.save("proposals", data);
+  }
+  async fetchPosts() {
+    const { data } = await axios.get(
+      `https://api.joystreamstats.live/api/v1/posts`
+    );
+    if (!data || data.error) return console.error(`failed to fetch from API`);
+    console.debug(`posts`, data);
+    this.save("posts", data);
+  }
+  async fetchMembers() {
+    const { data } = await axios.get(
+      `https://api.joystreamstats.live/api/v1/members`
+    );
+    if (!data || data.error) return console.error(`failed to fetch from API`);
+    console.debug(`members`, data);
+    this.save("members", data);
   }
 
   addOrReplace(array, item) {
@@ -278,258 +262,14 @@ class App extends React.Component<IProps, IState> {
 
   async fetchTokenomics() {
     console.debug(`Updating tokenomics`);
-    const { data } = await axios.get("https://status.joystream.org/status");
+    const { data } = await axios.get(
+      "https://joystreamstats.live/static/status.json"
+    );
+    //const { data } = await axios.get("https://status.joystream.org/status");
     if (!data || data.error) return;
     this.save("tokenomics", data);
   }
 
-  async fetchChannel(api: Api, id: number) {
-    if (this.state.channels.find((c) => c.id === id)) return;
-    const data = await api.query.contentWorkingGroup.channelById(id);
-
-    const handle = String(data.handle);
-    const title = String(data.title);
-    const description = String(data.description);
-    const avatar = String(data.avatar);
-    const banner = String(data.banner);
-    const content = String(data.content);
-    const ownerId = Number(data.owner);
-    const accountId = String(data.role_account);
-    const publicationStatus =
-      data.publication_status === "Public" ? true : false;
-    const curation = String(data.curation_status);
-    const createdAt = data.created;
-    const principal = Number(data.principal_id);
-    this.fetchMemberByAccount(api, accountId);
-
-    const channel: Channel = {
-      id,
-      handle,
-      title,
-      description,
-      avatar,
-      banner,
-      content,
-      ownerId,
-      accountId,
-      publicationStatus,
-      curation,
-      createdAt,
-      principal,
-    };
-
-    const channels = this.addOrReplace(this.state.channels, channel);
-    this.save("channels", channels);
-    if (id > 1)
-      this.enqueue(`channel ${id - 1}`, () => this.fetchChannel(api, id - 1));
-  }
-
-  async fetchCategory(api: Api, id: number) {
-    if (!id) return;
-    const exists = this.state.categories.find((c) => c.id === id);
-    if (exists) return this.fetchCategory(api, id - 1);
-
-    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.addOrReplace(this.state.categories, category));
-    this.enqueue(`category ${id - 1}`, () => this.fetchCategory(api, id - 1));
-  }
-
-  async fetchPost(api: Api, id: number) {
-    if (!id) return;
-    const exists = this.state.posts.find((p) => p.id === id);
-    if (exists) return this.fetchPost(api, id - 1);
-
-    const data = await api.query.forum.postById(id);
-    const threadId = Number(data.thread_id);
-    this.fetchThread(api, threadId);
-    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);
-    this.fetchMemberByAccount(api, authorId);
-
-    const post: Post = { id, threadId, text, authorId, createdAt };
-    const posts = this.addOrReplace(this.state.posts, post);
-    this.save("posts", posts);
-    this.enqueue(`post ${id - 1}`, () => this.fetchPost(api, id - 1));
-  }
-
-  async fetchThread(api: Api, id: number) {
-    if (!id) return;
-    const exists = this.state.threads.find((t) => t.id === id);
-    if (exists) return this.fetchThread(api, id - 1);
-
-    const data = await api.query.forum.threadById(id);
-
-    const title = String(data.title);
-    const categoryId = Number(data.category_id);
-    const nrInCategory = Number(data.nr_in_category);
-    const moderation = data.moderation;
-    const createdAt = String(data.created_at.block);
-    const authorId = String(data.author_id);
-
-    const thread: Thread = {
-      id,
-      title,
-      categoryId,
-      nrInCategory,
-      moderation,
-      createdAt,
-      authorId,
-    };
-    const threads = this.addOrReplace(this.state.threads, thread);
-    this.save("threads", threads);
-    this.enqueue(`thread ${id - 1}`, () => this.fetchThread(api, id - 1));
-  }
-
-  // council
-  async fetchCouncils(api: Api, currentRound: number) {
-    for (let round = currentRound; round > 0; round--)
-      this.enqueue(`council ${round}`, () => this.fetchCouncil(api, round));
-  }
-
-  async fetchCouncil(api: Api, round: number) {
-    let { councils } = this.state;
-    councils[round] = (await api.query.council.activeCouncil()).toJSON();
-    councils[round].map((c) => this.fetchMemberByAccount(api, c.member));
-    this.save("councils", councils);
-  }
-
-  async updateCouncil(api: Api, block: number) {
-    console.debug(`Updating council`);
-    const round = Number((await api.query.councilElection.round()).toJSON());
-    const termEndsAt = Number((await api.query.council.termEndsAt()).toJSON());
-    const stage = (await api.query.councilElection.stage()).toJSON();
-    let stageEndsAt = 0;
-    if (stage) {
-      const key = Object.keys(stage)[0];
-      stageEndsAt = stage[key];
-    }
-
-    const stages = [
-      "announcingPeriod",
-      "votingPeriod",
-      "revealingPeriod",
-      "newTermDuration",
-    ];
-    let durations = await Promise.all(
-      stages.map((s) => api.query.councilElection[s]())
-    ).then((stages) => stages.map((stage) => stage.toJSON()));
-    durations.push(durations.reduce((a, b) => a + b, 0));
-    this.fetchCouncils(api, round);
-    return { round, stageEndsAt, termEndsAt, stage, durations };
-  }
-
-  // proposals
-  async fetchProposal(api: Api, id: number) {
-    if (id > 1)
-      this.enqueue(`proposal ${id - 1}`, () => this.fetchProposal(api, id - 1));
-    // find existing
-    const { proposals } = this.state;
-    const exists = this.state.proposals.find((p) => p && p.id === id);
-
-    // check if complete
-    if (
-      exists &&
-      exists.detail &&
-      exists.stage === "Finalized" &&
-      exists.executed
-    )
-      if (exists.votesByAccount && exists.votesByAccount.length) return;
-      else
-        return this.enqueue(`votes for proposal ${id}`, () =>
-          this.fetchProposalVotes(api, exists)
-        );
-
-    // fetch
-    const proposal = await get.proposalDetail(api, id);
-    if (proposal.type !== "text")
-      proposal.detail = (
-        await api.query.proposalsCodex.proposalDetailsByProposalId(id)
-      ).toJSON();
-    proposals[id] = proposal;
-    this.save("proposals", proposals);
-    this.enqueue(`votes for proposal ${id}`, () =>
-      this.fetchProposalVotes(api, proposal)
-    );
-  }
-
-  async fetchProposalVotes(api: Api, proposal: ProposalDetail) {
-    const { votesByAccount } = proposal;
-    if (votesByAccount && votesByAccount.length) return;
-
-    const { councils, proposals } = this.state;
-    let members: Member[] = [];
-    councils
-      .filter((c) => c)
-      .map((seats) =>
-        seats.forEach(async (seat: Seat) => {
-          if (members.find((member) => member.account === seat.member)) return;
-          const member = this.state.members.find(
-            (m) => m.account === seat.member
-          );
-          if (member) members.push(member);
-        })
-      );
-
-    const { id } = proposal;
-    proposal.votesByAccount = await Promise.all(
-      members.map(async (member) => {
-        const vote = await this.fetchVoteByProposalByVoter(api, id, member.id);
-        return { vote, handle: member.handle };
-      })
-    );
-    proposals[id] = proposal;
-    this.save("proposals", proposals);
-  }
-
-  async fetchVoteByProposalByVoter(
-    api: Api,
-    proposalId: number,
-    voterId: number
-  ): Promise<string> {
-    const vote: VoteKind = await api.query.proposalsEngine.voteExistsByProposalByVoter(
-      proposalId,
-      voterId
-    );
-    const hasVoted: number = (
-      await api.query.proposalsEngine.voteExistsByProposalByVoter.size(
-        proposalId,
-        voterId
-      )
-    ).toNumber();
-
-    return hasVoted ? String(vote) : "";
-  }
-
   // validators
 
   async fetchValidators(api: Api) {
@@ -542,7 +282,7 @@ class App extends React.Component<IProps, IState> {
       "stashes",
       stashes.map((s: any) => String(s))
     );
-    this.enqueue("nominators", () => this.fetchNominators(api));
+    this.fetchNominators(api);
     return validators;
   }
 
@@ -584,51 +324,6 @@ class App extends React.Component<IProps, IState> {
     this.setState({ rewardPoints: data.toJSON() });
   }
 
-  // data objects
-  fetchDataObjects() {
-    // TODO dataDirectory.knownContentIds: Vec<ContentId>
-  }
-
-  // accounts
-  async fetchMemberByAccount(api: Api, account: string): Promise<Member> {
-    const empty = { id: -1, handle: `?`, account, about: ``, registeredAt: 0 };
-    if (!account) return empty;
-    const exists = this.state.members.find(
-      (m: Member) => String(m.account) === String(account)
-    );
-    if (exists) return exists;
-
-    const id = Number(await get.memberIdByAccount(api, account));
-    if (!id) return empty;
-    return await this.fetchMember(api, id);
-  }
-  async fetchMember(api: Api, id: number): Promise<Member> {
-    const exists = this.state.members.find((m: Member) => m.id === id);
-    if (exists) {
-      setTimeout(() => this.fetchMember(api, id--), 0);
-      return exists;
-    }
-    const membership = await get.membership(api, id);
-
-    const handle = String(membership.handle);
-    const account = String(membership.root_account);
-    const about = String(membership.about);
-    const registeredAt = Number(membership.registered_at_block);
-    const member: Member = { id, handle, account, registeredAt, about };
-    const members = this.addOrReplace(this.state.members, member);
-    this.save(`members`, members);
-    this.updateHandles(members);
-    if (id > 1)
-      this.enqueue(`member ${id - 1}`, () => this.fetchMember(api, id - 1));
-    return member;
-  }
-  updateHandles(members: Member[]) {
-    if (!members.length) return;
-    let handles: Handles = {};
-    members.forEach((m) => (handles[String(m.account)] = m.handle));
-    this.save(`handles`, handles);
-  }
-
   // Validators
   toggleStar(account: string) {
     let { stars } = this.state;
@@ -684,12 +379,19 @@ class App extends React.Component<IProps, IState> {
     );
   }
 
+  getMember(handle: string) {
+    const { members } = this.state;
+    const member = members.find((m) => m.handle === handle);
+    if (member) return member;
+    return members.find((m) => m.rootKey === handle);
+  }
+
   loadMembers() {
     const members = this.load("members");
     if (!members) return;
     this.setState({ members });
-    this.updateHandles(members);
   }
+
   loadPosts() {
     const posts: Post[] = this.load("posts");
     posts.forEach(({ id, text }) => {
@@ -707,6 +409,7 @@ class App extends React.Component<IProps, IState> {
   }
 
   async loadData() {
+    this.save("handles", []);
     const status = this.load("status");
     if (status) {
       console.debug(`Status`, status, `Version`, version);
@@ -715,7 +418,7 @@ class App extends React.Component<IProps, IState> {
     }
     console.debug(`Loading data`);
     this.loadMembers();
-    "assets providers councils categories channels proposals posts threads handles mints tokenomics transactions reports validators nominators stakes stars"
+    "assets providers councils categories channels proposals posts threads  mints tokenomics transactions reports validators nominators stakes stars"
       .split(" ")
       .map((key) => this.load(key));
   }
@@ -763,6 +466,7 @@ class App extends React.Component<IProps, IState> {
         <Routes
           toggleFooter={this.toggleFooter}
           toggleStar={this.toggleStar}
+          getMember={this.getMember}
           {...this.state}
         />
 
@@ -798,6 +502,10 @@ class App extends React.Component<IProps, IState> {
   componentDidMount() {
     this.loadData();
     this.connectEndpoint();
+    this.fetchProposals();
+    this.fetchPosts();
+    this.fetchMembers();
+    this.fetchCouncils();
     this.fetchStorageProviders();
     this.fetchAssets();
     setTimeout(() => this.fetchTokenomics(), 30000);
@@ -812,6 +520,7 @@ class App extends React.Component<IProps, IState> {
     this.toggleStar = this.toggleStar.bind(this);
     this.toggleFooter = this.toggleFooter.bind(this);
     this.toggleShowStatus = this.toggleShowStatus.bind(this);
+    this.getMember = this.getMember.bind(this);
   }
 }
 

+ 12 - 16
src/components/Calendar/index.tsx

@@ -38,10 +38,9 @@ class Calendar extends Component<IProps, IState> {
   }
 
   filterItems() {
-    const { status, posts, proposals, threads, handles } = this.props;
-    if (!status || !status.council) return [];
+    const { status, councils, posts, proposals, threads, handles } = this.props;
+    if (!status?.council) return [];
     const { hide } = this.state;
-    const { startTime, block, council } = status;
 
     let groups: CalendarGroup[] = [
       { id: 1, title: "RuntimeUpgrade" },
@@ -59,7 +58,7 @@ class Calendar extends Component<IProps, IState> {
     };
 
     const getTime = (block: number) =>
-      moment(startTime + 6000 * block).valueOf();
+      moment(status.startTime + 6000 * block).valueOf();
 
     // proposals
     proposals.forEach((p) => {
@@ -69,9 +68,9 @@ class Calendar extends Component<IProps, IState> {
       const id = `proposal-${p.id}`;
       const route = `/proposals/${p.id}`;
       const title = `${p.id} ${p.title} by ${p.author}`;
-      const start_time = moment(startTime + p.createdAt * 6000).valueOf();
+      const start_time = getTime(p.createdAt);
       const end_time = p.finalizedAt
-        ? moment(startTime + p.finalizedAt * 6000).valueOf()
+        ? getTime(p.finalizedAt)
         : moment().valueOf();
       items.push({ id, route, group, title, start_time, end_time });
     });
@@ -94,21 +93,18 @@ class Calendar extends Component<IProps, IState> {
       });
 
     // councils
-    const stage = council.durations;
-    const beforeTerm = stage[0] + stage[1] + stage[2];
-
-    for (let round = 1; round * stage[4] < block.id + stage[4]; round++) {
-      const title = `Round ${round}`;
+    councils.forEach((c) => {
+      const title = `Round ${c.round}`;
       const route = `/councils`;
       items.push({
-        id: `round-${round}`,
+        id: `round-${c.round}`,
         group: 2,
         route,
         title,
-        start_time: getTime(beforeTerm + (round - 1) * stage[4]),
-        end_time: getTime(beforeTerm + round * stage[4] - 1),
+        start_time: getTime(c.start),
+        end_time: getTime(c.end),
       });
-      const startBlock = (round - 1) * stage[4];
+      return; // TODO set terms on state
       items.push({
         id: `election-round-${round}`,
         group: 3,
@@ -117,7 +113,7 @@ class Calendar extends Component<IProps, IState> {
         start_time: getTime(startBlock),
         end_time: getTime(beforeTerm + startBlock),
       });
-    }
+    });
     this.setState({ groups, items });
   }
   toggleShowProposalType(id: number) {

+ 34 - 38
src/components/Council/index.tsx

@@ -2,52 +2,49 @@ import React from "react";
 import ElectionStatus from "./ElectionStatus";
 import MemberBox from "../Members/MemberBox";
 import Loading from "../Loading";
-import {Paper, Grid, makeStyles, Theme, createStyles, Toolbar, Typography, AppBar} from "@material-ui/core";
-
 import {
-  Handles,
-  Member,
-  Post,
-  ProposalDetail,
-  Seat,
-  Status,
-} from "../../types";
+  Paper,
+  Grid,
+  makeStyles,
+  Theme,
+  createStyles,
+  Toolbar,
+  Typography,
+  AppBar,
+} from "@material-ui/core";
+
+import { Council, Post, ProposalDetail, Status } from "../../types";
 
 const useStyles = makeStyles((theme: Theme) =>
-    createStyles({
-      root: {
-        flexGrow: 1,
-        backgroundColor: "#4038FF",
-      },
-      title: {
-        textAlign: "left",
-        flexGrow: 1,
-      },
-    })
+  createStyles({
+    root: {
+      flexGrow: 1,
+      backgroundColor: "#4038FF",
+    },
+    title: {
+      textAlign: "left",
+      flexGrow: 1,
+    },
+  })
 );
 
-const Council = (props: {
-  councils: Seat[][];
+const CouncilGrid = (props: {
   councilElection?: any;
-  members: Member[];
+  councils: Council[];
   proposals: ProposalDetail[];
   posts: Post[];
   validators: string[];
-  handles: Handles;
   status: Status;
   electionPeriods: number[];
 }) => {
-  const { councils, handles, members, posts, proposals, status } = props;
-  const council = councils[councils.length - 1];
+  const { getMember, councils, posts, proposals, status } = props;
+  const { council } = status;
   const classes = useStyles();
+
   if (!council) return <Loading target="council" />;
 
-  const sortCouncil = (council) =>
-    council.sort((a, b) => {
-      const handle1 = handles[a.member] || a.member;
-      const handle2 = handles[b.member] || b.member;
-      return handle1.localeCompare(handle2);
-    });
+  const sortCouncil = (consuls) =>
+    consuls.sort((a, b) => a.member.handle.localeCompare(b.member.handle));
 
   return (
     <Grid
@@ -72,14 +69,13 @@ const Council = (props: {
           </Toolbar>
         </AppBar>
         <div className="d-flex flex-wrap justify-content-between mt-2">
-          {sortCouncil(council).map((m) => (
-            <div key={m.member} className="col-12 col-md-4">
+          {sortCouncil(council.consuls).map((c) => (
+            <div key={c.memberId} className="col-12 col-md-4">
               <MemberBox
-                id={m.id || 0}
-                account={m.member}
-                handle={handles[m.member]}
-                members={members}
+                id={c.memberId}
+                member={getMember(c.member.handle)}
                 councils={councils}
+                council={council}
                 proposals={proposals}
                 placement={"bottom"}
                 posts={posts}
@@ -96,4 +92,4 @@ const Council = (props: {
   );
 };
 
-export default Council;
+export default CouncilGrid;

+ 16 - 21
src/components/Dashboard/Forum.tsx

@@ -4,14 +4,15 @@ import Loading from "../Loading";
 
 import { Handles, Post, Thread } from "../../types";
 import {
-    Grid,
-    Paper,
-    Link,
-    makeStyles,
-    Theme,
-    createStyles,
-    Toolbar,
-    AppBar, Typography,
+  Grid,
+  Paper,
+  Link,
+  makeStyles,
+  Theme,
+  createStyles,
+  Toolbar,
+  AppBar,
+  Typography,
 } from "@material-ui/core";
 
 const useStyles = makeStyles((theme: Theme) =>
@@ -29,12 +30,8 @@ const useStyles = makeStyles((theme: Theme) =>
   })
 );
 
-const Forum = (props: {
-  handles: Handles;
-  posts: Post[];
-  threads: Thread[];
-}) => {
-  const { handles, posts, threads, startTime } = props;
+const Forum = (props: { posts: Post[]; threads: Thread[] }) => {
+  const { posts, threads, startTime } = props;
   const classes = useStyles();
   if (!posts.length) return <Loading target="posts" />;
   return (
@@ -55,11 +52,11 @@ const Forum = (props: {
       >
         <AppBar className={classes.root} position="static">
           <Toolbar>
-              <Typography variant="h5" className={classes.title}>
-                  <Link style={{ color: "#fff" }} href={"/forum"}>
-                    Forum
-                  </Link>
-              </Typography>
+            <Typography variant="h5" className={classes.title}>
+              <Link style={{ color: "#fff" }} href={"/forum"}>
+                Forum
+              </Link>
+            </Typography>
           </Toolbar>
         </AppBar>
 
@@ -70,9 +67,7 @@ const Forum = (props: {
             <LatestPost
               key={post.id}
               selectThread={() => {}}
-              handles={handles}
               post={post}
-              thread={threads.find((t) => t.id === post.threadId)}
               startTime={startTime}
             />
           ))}

+ 9 - 6
src/components/Dashboard/Validators.tsx

@@ -3,8 +3,11 @@ import User from "../User";
 import { Handles } from "../../types";
 import Loading from "../Loading";
 
-const Validators = (props: { validators: string[]; handles: Handles }) => {
-  const { validators, handles } = props;
+const Validators = (props: {
+  validators: string[];
+  members: { rootKey: string }[];
+}) => {
+  const { getMember, validators } = props;
 
   const third = Math.floor(validators.length / 3) + 1;
 
@@ -19,7 +22,7 @@ const Validators = (props: { validators: string[]; handles: Handles }) => {
               <User
                 key={validator}
                 id={validator}
-                handle={handles[validator]}
+                handle={getMember(validator)?.handle || validator}
               />
             ))}
           </div>
@@ -28,7 +31,7 @@ const Validators = (props: { validators: string[]; handles: Handles }) => {
               <User
                 key={validator}
                 id={validator}
-                handle={handles[validator]}
+                handle={getMember(validator)?.handle || validator}
               />
             ))}
           </div>
@@ -37,12 +40,12 @@ const Validators = (props: { validators: string[]; handles: Handles }) => {
               <User
                 key={validator}
                 id={validator}
-                handle={handles[validator]}
+                handle={getMember(validator)?.handle || validator}
               />
             ))}
           </div>
         </div>
-      )) || <Loading />}
+      )) || <Loading target="Validators" />}
     </div>
   );
 };

+ 4 - 8
src/components/Dashboard/index.tsx

@@ -13,6 +13,7 @@ interface IProps extends IState {
 
 const Dashboard = (props: IProps) => {
   const {
+    getMember,
     toggleStar,
     councils,
     handles,
@@ -36,9 +37,9 @@ const Dashboard = (props: IProps) => {
       <Container maxWidth="xl">
         <Grid container spacing={3}>
           <Council
+            getMember={getMember}
             councils={councils}
-            members={members}
-            handles={handles}
+            council={status.council}
             posts={posts}
             proposals={proposals}
             stars={stars}
@@ -56,12 +57,7 @@ const Dashboard = (props: IProps) => {
             validators={validators}
             startTime={status.startTime}
           />
-          <Forum
-            handles={handles}
-            posts={posts}
-            threads={threads}
-            startTime={status.startTime}
-          />
+          <Forum posts={posts} threads={threads} startTime={status.startTime} />
           <Grid
             style={{
               textAlign: "center",

+ 9 - 11
src/components/Forum/LatestPost.tsx

@@ -6,18 +6,16 @@ import gfm from "remark-gfm";
 import moment from "moment";
 import { domain } from "../../config";
 
-import { Handles, Thread, Post } from "../../types";
+import { Post } from "../../types";
 
 const LatestPost = (props: {
   selectThread: (id: number) => void;
-  handles: Handles;
   post: Post;
-  thread?: Thread;
   startTime: number;
 }) => {
-  const { selectThread, handles = [], thread, post, startTime } = props;
-  const { authorId, createdAt, id, threadId, text } = post;
-
+  const { selectThread, post, startTime } = props;
+  const { author, createdAt, id, thread, text } = post;
+  const created = moment(startTime + createdAt.block * 6000);
   return (
     <div
       key={id}
@@ -27,16 +25,16 @@ const LatestPost = (props: {
       }
     >
       <div className="mb-2">
-        {moment(startTime + createdAt.block * 6000).fromNow()}
-        <User key={authorId} id={authorId} handle={handles[authorId]} />
+        {created.isValid() ? created.fromNow() : <span />}
+        <User key={author.id} id={author.id} handle={author.handle} />
         posted in
         <Link
-          to={`/forum/threads/${threadId}`}
+          to={`/forum/threads/${thread.id}`}
           className="font-weight-bold mx-2"
         >
-          {thread ? thread.title : `Thread ${threadId}`}
+          {thread ? thread.title : `Thread ${thread.id}`}
         </Link>
-        <a href={`${domain}/#/forum/threads/${threadId}`}>reply</a>
+        <a href={`${domain}/#/forum/threads/${thread.id}`}>reply</a>
       </div>
 
       <div className="overflow-hidden">

+ 5 - 8
src/components/Members/Member.tsx

@@ -1,16 +1,15 @@
 import React from "react";
-import { Member, Post, ProposalDetail, Seat, Thread } from "../../types";
+import { Council, Member, Post, ProposalDetail, Thread } from "../../types";
 import { domain } from "../../config";
 import Summary from "./Summary";
 import Posts from "./MemberPosts";
 import Proposals from "./MemberProposals";
-import Loading from "../Loading";
 import NotFound from "./NotFound";
 
 const MemberBox = (props: {
   match: { params: { handle: string } };
   members: Member[];
-  councils: Seat[][];
+  councils: Council[];
   proposals: ProposalDetail[];
   posts: Post[];
   threads: Thread[];
@@ -25,10 +24,9 @@ const MemberBox = (props: {
   );
   if (!member) return <NotFound history={props.history} />;
 
-  const council = councils[councils.length - 1];
-  if (!council) return <Loading />;
-  const isCouncilMember = council.find(
-    (seat) => seat.member === member.account
+  const council = status.council;
+  const isCouncilMember = council?.consuls.find(
+    (c) => c.member.handle === member.handle
   );
 
   const threadTitle = (id: number) => {
@@ -47,7 +45,6 @@ const MemberBox = (props: {
 
         <Summary
           councils={councils}
-          handle={member.handle}
           member={member}
           posts={posts}
           proposals={proposals}

+ 21 - 14
src/components/Members/MemberBox.tsx

@@ -1,36 +1,43 @@
 import React from "react";
 import MemberOverlay from "./MemberOverlay";
 
-import { Member, Post, ProposalDetail, Seat } from "../../types";
+import { Council, Post, ProposalDetail } from "../../types";
 import { Link } from "@material-ui/core";
 import InfoTooltip from "../Tooltip";
 
-const shortName = (name: string) => {
-  return `${name.slice(0, 5)}..${name.slice(+name.length - 5)}`;
-};
+const shortName = (key) => `${key.slice(0, 5)}..${key.slice(key.length - 5)}`;
 
 const MemberBox = (props: {
-  councils: Seat[][];
-  members: Member[];
+  council: Council;
+  councils: Council[];
+  member: { handle: string };
   proposals: ProposalDetail[];
   posts: Post[];
   id: number;
-  account: string;
-  handle: string;
   startTime: number;
   placement: "left" | "bottom" | "right" | "top";
   validators: string[];
 }) => {
-  const { account, handle, members, posts, placement, proposals } = props;
+  const {
+    account,
+    councils,
+    council,
+    member,
+    posts,
+    placement,
+    proposals,
+  } = props;
+
+  const handle = member ? member.handle : shortName(account);
   return (
     <InfoTooltip
       placement={placement}
       id={`overlay-${handle}`}
       title={
         <MemberOverlay
-          handle={handle}
-          members={members}
-          councils={props.councils}
+          member={member}
+          councils={councils}
+          council={council}
           proposals={proposals}
           posts={posts}
           startTime={props.startTime}
@@ -47,9 +54,9 @@ const MemberBox = (props: {
           border: "1px solid #fff",
           borderRadius: "4px",
         }}
-        href={`/members/${handle || account}`}
+        href={`/members/${handle}`}
       >
-        {handle || shortName(account)}
+        {handle}
       </Link>
     </InfoTooltip>
   );

+ 13 - 14
src/components/Members/MemberOverlay.tsx

@@ -1,38 +1,37 @@
 import React from "react";
-import { Member, Post, ProposalDetail, Seat } from "../../types";
+import { Council, Member, Post, ProposalDetail } from "../../types";
 import { domain } from "../../config";
 import Summary from "./Summary";
 import NotFound from "./NotFound";
 
 const MemberBox = (props: {
-  handle: string;
-  members: Member[];
-  councils: Seat[][];
+  member: Member[];
+  council: Council;
+  councils: Council[];
   proposals: ProposalDetail[];
   posts: Post[];
   startTime: number;
   validators: string[];
 }) => {
-  const { councils, handle, members, posts, proposals, startTime } = props;
-  const member = members.find((m) => m.handle === handle);
-  if (!member) return <NotFound nolink={true} />;
+  const { councils, council, member, posts, proposals, startTime } = props;
 
-  const council = councils[councils.length - 1];
   if (!council) return <div>Loading..</div>;
-  const isCouncilMember = council.find(
-    (seat) => seat.member === member.account
+  if (!member) return <NotFound nolink={true} />;
+
+  const isCouncilMember = council.consuls.find(
+    (seat) => seat.memberId === member.memberId
   );
 
   return (
-    <div style={{backgroundColor: '#4038FF', padding: 5}}>
+    <div style={{ backgroundColor: "#4038FF", padding: 5 }}>
       {isCouncilMember && <div>council member</div>}
-      <a href={`${domain}/#/members/${handle}`}>
-        <h3>{handle}</h3>
+      <a href={`${domain}/#/members/${member.handle}`}>
+        <h3>{member.handle}</h3>
       </a>
 
       <Summary
         councils={councils}
-        handle={handle}
+        council={council}
         member={member}
         posts={posts}
         proposals={proposals}

+ 3 - 6
src/components/Members/NotFound.tsx

@@ -4,13 +4,10 @@ import { Back } from "..";
 
 const NotFound = (props: { nolink?: boolean }) => {
   return (
-    <>
+    <div className="box">
+      <div>No membership found.</div>
       <Back history={props.history} />
-      <div className="box">
-        <div>No membership found.</div>
-        {props.nolink || <Link to={`/members`}>Back</Link>}
-      </div>
-    </>
+    </div>
   );
 };
 

+ 11 - 13
src/components/Members/Summary/index.tsx

@@ -16,32 +16,30 @@ interface ProposalVote {
 
 const Summary = (props: {
   councils: Seat[][];
-  handle: string;
   member: Member;
   posts: Post[];
   proposals: ProposalDetail[];
   startTime: number;
   validators: string[];
 }) => {
-  const { councils, handle, member, proposals, startTime } = props;
+  const { councils, member, proposals, startTime } = props;
 
-  const onCouncil = councils.filter((c) =>
-    c && c.find((seat) => seat.member === member.account)
+  const onCouncil = councils?.filter((c) =>
+    c?.consuls.find((c) => c.member.handle === member.handle)
   );
 
   let votes: ProposalVote[] = [];
-  proposals.forEach((p) => {
-    if (!p || !p.votesByAccount) return;
-    const vote = p.votesByAccount.find((v) => v.handle === member.handle);
-    if (vote && vote.vote !== ``) votes.push({ proposal: p, vote: vote.vote });
+  proposals.forEach((proposal) => {
+    const vote = proposal.votes.find((v) => v.member.handle === member.handle);
+    if (vote) votes.push({ proposal, vote });
   });
-  const createdProposals = proposals.filter((p) => p && p.author === handle);
+  const createdProposals = proposals.filter((p) => p?.author.handle === member.handle);
   const approved = createdProposals.filter((p) => p.result === "Approved");
   const pending = createdProposals.filter((p) => p.result === "Pending");
 
-  const posts = props.posts.filter((p) => p.authorId === member.account);
+  const posts = props.posts.filter((p) => p.author.handle === member.handle);
 
-  const time = startTime + member.registeredAt * 6000;
+  const time = startTime + member.createdAt * 6000;
   const date = moment(time);
   const created = date.isValid()
     ? date.format("DD/MM/YYYY HH:mm")
@@ -60,13 +58,13 @@ const Summary = (props: {
           This user runs a <a href={`${domain}/#/staking`}>validator node</a>.
         </div>
       )}
-
-      <Councils onCouncil={onCouncil.length} votes={votes.length} />
+     
       <Proposals
         proposals={createdProposals.length}
         approved={approved.length}
         pending={pending.length}
       />
+      <Councils onCouncil={onCouncil.length} votes={votes.length} />
       <Posts posts={posts.length} />
       <About about={member.about} />
     </div>

+ 7 - 7
src/components/Members/index.tsx

@@ -29,7 +29,7 @@ class Members extends React.Component<IProps, IState> {
   }
 
   render() {
-    const { councils, handles, members, posts, proposals, status } = this.props;
+    const { getMember,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,10 +39,10 @@ class Members extends React.Component<IProps, IState> {
 
     const quart = Math.floor(unique.length / 4) + 1;
     const cols = [
-      unique.slice(0, quart),
-      unique.slice(quart, 2 * quart),
-      unique.slice(2 * quart, 3 * quart),
-      unique.slice(3 * quart),
+      members.slice(0, quart),
+      members.slice(quart, 2 * quart),
+      members.slice(2 * quart, 3 * quart),
+      members.slice(3 * quart),
     ];
 
     return (
@@ -55,9 +55,9 @@ class Members extends React.Component<IProps, IState> {
                 <div key={String(m.account)} className="box">
                   <MemberBox
                     id={Number(m.id)}
-                    account={String(m.account)}
-                    handle={m.handle || handles[String(m.account)]}
+                    account={m.rootKey}
                     members={members}
+		    member={getMember(m.handle)}
                     councils={councils}
                     proposals={proposals}
                     placement={index === 3 ? "left" : "bottom"}

+ 13 - 5
src/components/Proposals/NavBar.tsx

@@ -1,6 +1,5 @@
 import React from "react";
 import { Button, Navbar, NavDropdown } from "react-bootstrap";
-import { Link } from "react-router-dom";
 import { Sliders } from "react-feather";
 
 const NavBar = (props: any) => {
@@ -8,13 +7,22 @@ const NavBar = (props: any) => {
   if (!show) return <div />;
   return (
     <Navbar bg="dark" variant="dark">
-      <Link to="/">
-        <Navbar.Brand>Joystream</Navbar.Brand>
-      </Link>
       <Navbar.Toggle aria-controls="basic-navbar-nav" />
       <Navbar.Collapse id="basic-navbar-nav">
         <Navbar.Brand className="mr-auto">Proposals</Navbar.Brand>
 
+        <NavDropdown
+          title={<div className="text-light">per Page</div>}
+          id="basic-nav-dropdown"
+        >
+          <NavDropdown.Divider />
+          {[10, 25, 50, 100, 250, 500].map((n) => (
+            <NavDropdown.Item key={n} onClick={() => props.setPerPage(n)}>
+              {n}
+            </NavDropdown.Item>
+          ))}
+        </NavDropdown>
+
         <NavDropdown
           title={<div className="text-light">Creator</div>}
           id="basic-nav-dropdown"
@@ -33,7 +41,7 @@ const NavBar = (props: any) => {
               className={author === props.author ? "bg-dark text-light" : ""}
               onClick={props.selectAuthor}
             >
-              {author}
+              {authors[author]}
             </NavDropdown.Item>
           ))}
         </NavDropdown>

+ 1 - 1
src/components/Proposals/NavButtons.tsx

@@ -11,7 +11,7 @@ const NavButtons = (props: {
   const { setPage, limit, page, proposals } = props;
   if (proposals < limit) return <div/>
   return (
-    <div className="text-center">
+    <div className="text-center my-2">
       <Button
         variant="secondary"
         className="btn btn-sm"

+ 28 - 10
src/components/Proposals/ProposalTable.tsx

@@ -6,8 +6,6 @@ import NavButtons from "./NavButtons";
 import Types from "./Types";
 import { Member, Post, ProposalDetail, ProposalPost, Seat } from "../../types";
 
-const LIMIT = 3;
-
 interface IProps {
   hideNav?: boolean;
   block: number;
@@ -40,11 +38,13 @@ class ProposalTable extends React.Component<IProps, IState> {
       author: "",
       showTypes: false,
       page: 1,
+      perPage: 10,
     };
     this.selectAuthor = this.selectAuthor.bind(this);
     this.toggleShowType = this.toggleShowType.bind(this);
     this.toggleShowTypes = this.toggleShowTypes.bind(this);
     this.setPage = this.setPage.bind(this);
+    this.setPerPage = this.setPerPage.bind(this);
     this.setKey = this.setKey.bind(this);
   }
 
@@ -56,6 +56,9 @@ class ProposalTable extends React.Component<IProps, IState> {
   setPage(page: number) {
     this.setState({ page });
   }
+  setPerPage(perPage: number) {
+    this.setState({ perPage });
+  }
   toggleShowTypes() {
     this.setState({ showTypes: !this.state.showTypes });
   }
@@ -67,6 +70,7 @@ class ProposalTable extends React.Component<IProps, IState> {
     this.setState({ selectedTypes });
   }
   selectAuthor(event: any) {
+    console.log(event);
     this.setState({ author: event.target.text });
   }
 
@@ -74,11 +78,14 @@ class ProposalTable extends React.Component<IProps, IState> {
     return this.filterByAuthor(this.filterByType(proposals));
   }
   filterByAuthor(proposals, author = this.state.author) {
-    if (!author.length) return proposals;
-    return proposals.filter((p) => p.author === author);
+    return author.length
+      ? proposals.filter((p) => p.author.handle === author)
+      : proposals;
   }
   filterByType(proposals, types = this.state.selectedTypes) {
-    return types.length ? filter((p) => types.includes(p.type)) : proposals;
+    return types.length
+      ? proposals.filter((p) => types.includes(p.type))
+      : proposals;
   }
   sortProposals(list: ProposalDetail[]) {
     const { asc, key } = this.state;
@@ -94,15 +101,17 @@ class ProposalTable extends React.Component<IProps, IState> {
   render() {
     const { hideNav, block, councils, members, posts } = this.props;
 
-    const { page, author, selectedTypes } = this.state;
+    const { page, perPage, author, selectedTypes } = this.state;
 
     // proposal types
     let types: { [key: string]: number } = {};
     this.props.proposals.forEach((p) => types[p.type]++);
 
     // authors
-    let authors: { [key: string]: number } = {};
-    this.props.proposals.forEach((p) => authors[p.author]++);
+    let authors: { [key]: number } = {};
+    this.props.proposals.forEach(
+      (p) => (authors[p.authorId] = p.author.handle)
+    );
 
     const proposals = this.sortProposals(
       this.filterProposals(this.props.proposals)
@@ -131,6 +140,8 @@ class ProposalTable extends React.Component<IProps, IState> {
           show={!hideNav}
           author={author}
           authors={authors}
+          perPage={perPage}
+          setPerPage={this.setPerPage}
           selectAuthor={this.selectAuthor}
           toggleShowTypes={this.toggleShowTypes}
         />
@@ -150,8 +161,15 @@ class ProposalTable extends React.Component<IProps, IState> {
           avgDays={avgDays}
           avgHours={avgHours}
         />
+        <NavButtons
+          setPage={this.setPage}
+          page={page}
+          limit={perPage + 1}
+          proposals={proposals.length}
+        />
+
         <div className="d-flex flex-column overflow-auto p-2">
-          {proposals.slice((page - 1) * LIMIT, page * LIMIT).map((p) => (
+          {proposals.slice((page - 1) * perPage, page * perPage).map((p) => (
             <Row
               key={p.id}
               {...p}
@@ -171,7 +189,7 @@ class ProposalTable extends React.Component<IProps, IState> {
         <NavButtons
           setPage={this.setPage}
           page={page}
-          limit={LIMIT+1}
+          limit={perPage + 1}
           proposals={proposals.length}
         />
       </div>

+ 20 - 32
src/components/Proposals/Row.tsx

@@ -1,9 +1,10 @@
 import React from "react";
+import { Badge } from "react-bootstrap";
 import MemberOverlay from "../Members/MemberOverlay";
 import Bar from "./Bar";
 import Posts from "./Posts";
 import Detail from "./Detail";
-import { VoteNowButton, VotesTooltip, VotesBubbles } from "..";
+import { VoteNowButton, VotesBubbles } from "..";
 import moment from "moment";
 
 import {
@@ -27,7 +28,7 @@ const colors: { [key: string]: string } = {
   Rejected: "danger ",
   Canceled: "danger ",
   Expired: "warning ",
-  Pending: "",
+  Pending: "secondary",
 };
 
 const useStyles = makeStyles({
@@ -76,13 +77,11 @@ const ProposalRow = (props: {
     type,
     votes,
     detail,
-    members,
   } = props;
 
   const url = `https://pioneer.joystreamstats.live/#/proposals/${id}`;
   let result: string = props.result ? props.result : props.stage;
   if (executed) result = Object.keys(props.executed)[0];
-  const color = colors[result];
 
   const finalized =
     finalizedAt && formatTime(props.startTime + finalizedAt * 6000);
@@ -99,34 +98,16 @@ const ProposalRow = (props: {
   const left = `${period - blocks} / ${period} blocks left (${percent}%)`;
   const classes = useStyles();
   return (
-    <div className="d-flex flex-column">
-      <div className="d-flex flex-wrap justify-content-left text-left mt-3">
-        <InfoTooltip
-          placement="right"
-          id={`votes-${id}`}
-          title={
-            <VotesTooltip votesByAccount={props.votesByAccount} votes={votes} />
-          }
-        >
-          <div
-            style={{ borderRadius: "4px" }}
-            className={`col-3 col-md-1 text-center p-2 border border-${color}`}
-          >
-            <b>{result}</b>
-            <div className="d-flex flex-row justify-content-center">
-              <VotesBubbles votes={votes} />
-            </div>
-          </div>
-        </InfoTooltip>
-
-        <div className="col-9 col-md-3 text-left">
+    <div className="box d-flex flex-column">
+      <div className="d-flex flex-row justify-content-left text-left mt-3">
+        <div className="col-5 col-md-3 text-left">
           <InfoTooltip
             placement="bottom"
             id={`overlay-${author}`}
             title={
               <MemberOverlay
-                handle={author}
-                members={members}
+                handle={author.handle}
+                member={author}
                 councils={props.councils}
                 proposals={props.proposals}
                 posts={props.forumPosts}
@@ -136,9 +117,8 @@ const ProposalRow = (props: {
             }
           >
             <div>
-              <Link className={classes.link} href={`/members/${author}`}>
-                {" "}
-                {author}
+              <Link className={classes.link} href={`/members/${author.handle}`}>
+                {author.handle}
               </Link>
             </div>
           </InfoTooltip>
@@ -153,10 +133,18 @@ const ProposalRow = (props: {
           <Detail detail={detail} type={type} />
         </div>
 
-        <div className="d-none d-md-block ml-auto">
+        <Badge className={`bg-${colors[result]} col-2 p-3 d-md-block ml-auto`}>
+          <div className={`bg-${colors[result]} mb-2`}>
+            <b>{result}</b>
+          </div>
+
           {finalized ? finalized : <VoteNowButton show={true} url={url} />}
-        </div>
+        </Badge>
       </div>
+      <div className="d-flex flex-row p-2">
+        <VotesBubbles votes={votes} />
+      </div>
+
       <Bar
         id={id}
         blocks={blocks}

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

@@ -4,9 +4,6 @@ import { IState, ProposalDetail } from "../../types";
 
 const amount = (amount: number) => (amount / 1000000).toFixed(2);
 
-const getRound = (block: number): number =>
-  Math.round((block - 57600) / 201600);
-
 const executionFailed = (result: string, executed: any) => {
   if (result !== "Approved") return result;
   if (!executed || !Object.keys(executed)) return;
@@ -25,7 +22,7 @@ const Spending = (props: IState) => {
   let sum = 0;
   let sums: number[] = [];
   spending.forEach((p) => {
-    const r = getRound(p.finalizedAt);
+    const r = p.councilRound;
     rounds[r] = rounds[r] ? rounds[r].concat(p) : [p];
     if (!sums[r]) sums[r] = 0;
     if (!p.detail) return unknown++;
@@ -40,7 +37,7 @@ const Spending = (props: IState) => {
         Total: {amount(sum)}
         {unknown ? `*` : ``} M tJOY
       </h1>
-      {unknown ? `* subject to change until all details are available` : ``}
+      {unknown ? `* may change until all details are available` : ``}
       {rounds.map((proposals, i: number) => (
         <div key={`round-${i}`} className="bg-secondary p-1 my-2">
           <h2 className="text-left mt-3">
@@ -68,7 +65,7 @@ const ProposalLine = (props: any) => {
         {detail ? amount(detail.spending[0]) : `?`} M
       </span>
       <Link to={`/proposals/${id}`}>{title}</Link> (
-      <Link to={`/members/${author}`}>{author}</Link>)
+      <Link to={`/members/${author}`}>{author.handle}</Link>)
       {failed ? ` - ${failed}` : ""}
     </div>
   );

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

@@ -26,8 +26,7 @@ const Proposals = (props: {
   if (!proposals.length)
     return (
       <div className="box">
-        <h1>Loading</h1>
-        <Loading />
+        <Loading target="Proposals" />
       </div>
     );
 

+ 3 - 3
src/components/Timeline/index.tsx

@@ -8,7 +8,7 @@ const Timeline = (props: {
   proposals: ProposalDetail[];
   status: { startTime: number };
 }) => {
-  const { handles, posts, proposals, status } = props;
+  const { posts, proposals, status } = props;
   let events: Event[] = [];
 
   proposals.forEach(
@@ -23,7 +23,7 @@ const Timeline = (props: {
         },
         link: {
           url: `/proposals/${p.id}`,
-          text: `Proposal ${p.id}: ${p.title} by ${p.author}`,
+          text: `Proposal ${p.id}: ${p.title} by ${p.author.handle}`,
         },
       })
   );
@@ -40,7 +40,7 @@ const Timeline = (props: {
         },
         link: {
           url: `/forum/threads/${p.threadId}`,
-          text: `Post ${p.id} by ${handles[p.authorId]}`,
+          text: `Post ${p.id} by ${p.author.handle}`,
         },
       })
   );

+ 3 - 7
src/components/User/index.tsx

@@ -1,16 +1,12 @@
 import React from "react";
 import { Link } from "react-router-dom";
 
-const shortName = (name: string) => {
-  return `${name.slice(0, 5)}..${name.slice(+name.length - 5)}`;
-};
-
-const User = (props: { id: string; handle?: string }) => {
-  const { id, handle } = props;
+const User = (props: { id: string; handle: string }) => {
+  const { handle } = props;
 
   return (
     <span className="user mx-1">
-      <Link to={`/members/${handle || id}`}>{handle || shortName(id)}</Link>
+      <Link to={`/members/${handle}`}>{handle}</Link>
     </span>
   );
 };

+ 2 - 2
src/components/Validators/Nominators.tsx

@@ -20,7 +20,7 @@ const Nominators = (props: {
   nominators?: Stake[];
   reward: number;
 }) => {
-  const { fNum, sortBy, handles, nominators, reward } = props;
+  const { fNum, sortBy, nominators, reward } = props;
 
   if (!nominators || !nominators.length) return <div />;
 
@@ -51,7 +51,7 @@ const Nominators = (props: {
                 {Reward(reward * (n.value / sum))}
               </td>
               <td>
-                <User id={n.who} handle={handles[n.who]} />
+                <User id={n.who} handle={n.who} />
               </td>
             </tr>
           ))}

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

@@ -58,12 +58,12 @@ class Validator extends Component<IProps, IState> {
 
   render() {
     const {
-      sortBy,
+      sortBy = () => {},
       toggleStar,
-      handles,
-      members,
+      member,
       validator,
       councils,
+      council,
       posts,
       proposals,
       startTime,
@@ -116,8 +116,8 @@ class Validator extends Component<IProps, IState> {
             account={validator}
             placement={"right"}
             councils={councils}
-            handle={handles[validator]}
-            members={members}
+            council={council}
+            member={member}
             posts={posts}
             proposals={proposals}
             startTime={startTime}

+ 7 - 6
src/components/Validators/index.tsx

@@ -116,6 +116,7 @@ const Validators = (iProps: IProps) => {
   };
 
   const {
+    getMember,
     councils,
     handles,
     members,
@@ -189,7 +190,7 @@ const Validators = (iProps: IProps) => {
           reward={status.lastReward}
         />
 
-        <div className="d-flex flex-column">
+        <div className="d-flex flex-column mt-3">
           {sortValidators(sortBy, starred).map((v) => (
             <Validator
               key={v}
@@ -200,8 +201,8 @@ const Validators = (iProps: IProps) => {
               validator={v}
               reward={lastReward / validators.length}
               councils={councils}
-              handles={handles}
-              members={members}
+              council={status.council}
+              member={getMember(v)}
               posts={posts}
               proposals={proposals}
               validators={validators}
@@ -218,15 +219,15 @@ const Validators = (iProps: IProps) => {
             sortValidators(sortBy, unstarred).map((v) => (
               <Validator
                 key={v}
-                sortBy={sortBy}
+                sortBy={setSortBy}
                 starred={stars[v] ? `teal` : undefined}
                 toggleStar={props.toggleStar}
                 startTime={startTime}
                 validator={v}
                 reward={lastReward / validators.length}
                 councils={councils}
-                handles={handles}
-                members={members}
+                council={status.council}
+                member={getMember(v)}
                 posts={posts}
                 proposals={proposals}
                 validators={validators}

+ 9 - 15
src/components/Votes.tsx

@@ -1,7 +1,6 @@
 import React from "react";
 import { Button } from "react-bootstrap";
 import { Vote } from "../types";
-import { VotingResults } from "@joystream/types/proposals";
 
 export const voteKeys: { [key: string]: string } = {
   abstensions: "Abstain",
@@ -44,34 +43,29 @@ export const VoteNowButton = (props: { show: boolean; url: string }) => {
 
 const VoteBubble = (props: {
   detailed?: boolean;
-  vote: string;
+  vote: Vote;
   count: number;
 }) => {
-  const { count, detailed, vote } = props;
-  if (!count) return <span />;
+  const { detailed, vote } = props;
 
   return (
-    <Button className="btn-sm m-1" variant={voteStyles[voteKeys[vote]]}>
-      {count} {detailed && vote}
+    <Button className="btn-sm m-1" variant={voteStyles[vote.vote]}>
+      {vote.member.handle} {detailed && vote.vote}
     </Button>
   );
 };
 
-export const VotesBubbles = (props: {
-  detailed?: boolean;
-  votes: VotingResults | { [key: string]: number }; // VotingResults refuses string keys
-}) => {
-  const { detailed } = props;
-  const votes = JSON.parse(JSON.stringify(props.votes)); // TODO
+export const VotesBubbles = (props: { detailed?: boolean; votes: Vote[] }) => {
+  const { detailed, votes } = props;
 
   return (
     <div>
-      {Object.keys(votes).map((vote: string) => (
+      {votes.map((vote: Vote) => (
         <VoteBubble
-          key={vote}
+          key={vote.id}
           detailed={detailed}
           vote={vote}
-          count={votes[vote]}
+          count={votes.length}
         />
       ))}
     </div>

+ 1 - 1
src/config.ts

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

+ 0 - 180
src/proposalPosts.js

@@ -1,180 +0,0 @@
-const posts = [
-  {
-    threadId: 3,
-    text: "Our first valid proposal, in this testnet. \\m/",
-    id: 1,
-  },
-  {
-    threadId: 6,
-    text:
-      "Tokens minted [here](https://testnet.joystream.org/#/explorer/query/0xe649437a8e4142f353d473936f5495e276de8f5d3f6886c8bf70c811812a66e8)\nand transferred over the next blocks.",
-    id: 2,
-  },
-  {
-    threadId: 7,
-    text:
-      "This has been cancelled as we can just repurpose the existing category for Joystream bounties for this purpose.",
-    id: 3,
-  },
-  { threadId: 8, text: "Sounds good to me!", id: 4 },
-  { threadId: 8, text: "Yup. Same here!", id: 5 },
-  { threadId: 8, text: "Approved! :)", id: 6 },
-  {
-    threadId: 15,
-    text:
-      "I will give some percentage of the rewards to @tomato for creating the report template and @nexusfallout/Finnyfound for trying to help me :)",
-    id: 7,
-  },
-  {
-    threadId: 15,
-    text: "😊 Thats okay. I was not able to do much work anyway.",
-    id: 8,
-  },
-  { threadId: 15, text: "Hey, you deserve all the tokens! Great work!", id: 9 },
-  {
-    threadId: 17,
-    text:
-      "PS: Typo on Report Generator Report ->  Council Report Generator\n\nIf you want to find more information about the Council Report Generator you can check the PR here:\nhttps://github.com/Joystream/community-repo/pull/29",
-    id: 10,
-  },
-  {
-    threadId: 19,
-    text:
-      "Hi betilada, welcome to the community. This amount sounds fair to me and I'll be voting to approve! If the proposal is approved I look forward to seeing some of your videos!",
-    id: 11,
-  },
-  {
-    threadId: 19,
-    text:
-      "Hi @betilada, welcome!\n\n\nIs really great to see new content in Joystream so I will approve! \n\n\nThe amount you requested seems good to me.",
-    id: 12,
-  },
-  {
-    threadId: 21,
-    text: "I suggested 40 slots but I can agree with 35 :)",
-    id: 13,
-  },
-  {
-    threadId: 21,
-    text: "Sorry I just saw your post, we really need that Telegram bot back!",
-    id: 14,
-  },
-  { threadId: 25, text: "Seems good! ", id: 15 },
-  {
-    threadId: 27,
-    text: "I like the ideas, it should help to keep things organized!",
-    id: 16,
-  },
-  {
-    threadId: 29,
-    text: "PS: Typo on Council Generator Report -> Council Report Generator",
-    id: 17,
-  },
-  {
-    threadId: 31,
-    text:
-      "For KPI 3.3-Appoint New Council Secretary,the payment is $50,As the price now,it's about 1.01M tjoy,isn't it?",
-    id: 18,
-  },
-  {
-    threadId: 30,
-    text: "For KPI 2.2 Council report,the payment is $0,isn't it?",
-    id: 19,
-  },
-  {
-    threadId: 31,
-    text:
-      "Hi @anthony, the $50 is the rewards the council receives for completing the KPI, the KPI itself says `The Council informally appoints a Council Secretary through a spending proposal and pays them an appropriate rate for their responsibilities.` I put a similar amount for the past council sessions and we have passed this KPI.",
-    id: 20,
-  },
-  {
-    threadId: 30,
-    text:
-      "@anthony the council reports are mandatory, so there is no payment. It is a pass/fail for the council. When there is work for the council to do, there's nothing stopping people from creating spending proposals for completing the work. It's up to the council to vote on the proposals as they want to.\nThe KPI rewards are explained here: https://github.com/Joystream/helpdesk/blob/master/tokenomics/README.md#council-kpis",
-    id: 21,
-  },
-  {
-    threadId: 32,
-    text:
-      "From what I've seen in the test channel it looks like the bot does just about everything it used to. I can't judge the coding myself though.",
-    id: 22,
-  },
-  {
-    threadId: 35,
-    text:
-      "A small correction: For example, the storage lead has an `agreed payment of $35 per week`, but this is currently an `actual payment of $34 per week`",
-    id: 23,
-  },
-  {
-    threadId: 34,
-    text:
-      "This spot check includes this `at this time the Curator Lead has opted to not recieve payments (this was communicated on Telegram)` which is wrong. It is contradicted by the payment information which is correct.",
-    id: 24,
-  },
-  { threadId: 34, text: "Great work!", id: 25 },
-  {
-    threadId: 35,
-    text:
-      "I approve this changes but we really should think about cutting the slots for the validators, since this should increase the reward of each validator, right?\nAs reported in Telegram it seems that validators are not getting enough tokens to compensate for the cost of the servers, which will increase in the future, with the chain size increasing.",
-    id: 26,
-  },
-  {
-    threadId: 35,
-    text:
-      "On top of this is a role which is a bit difficult to control the payments, since it depends in the amount in stake.",
-    id: 27,
-  },
-  {
-    threadId: 35,
-    text:
-      "@freakstatic I think this is because a lot of validators don't have a large amount of stake, so it is easy for a nominator to put backing behind the validator which reduces the validator's rewards a lot. You can see this on the targets page, where the ratio of `own stake` : `total stake` is as high as 1:10.",
-    id: 28,
-  },
-  { threadId: 35, text: "Interesting I didn't know about that", id: 29 },
-  { threadId: 33, text: "You are doing great job @tomato", id: 30 },
-  {
-    threadId: 36,
-    text:
-      "According to JS Team burning of tokens will not change the value of tokens.",
-    id: 31,
-  },
-  {
-    threadId: 36,
-    text:
-      "@nexusfallout it appears correct. If tokens are burned then it shouldn't affect the token value.",
-    id: 32,
-  },
-  { threadId: 36, text: "Totally agreed ", id: 33 },
-  {
-    threadId: 45,
-    text:
-      "I'm resubmitting this because the last proposal failed due to low council mint capacity",
-    id: 34,
-  },
-  {
-    threadId: 47,
-    text:
-      "I forgot to include my github link, here it is: \nhttps://github.com/freakstatic",
-    id: 35,
-  },
-  {
-    threadId: 47,
-    text: "Thank you for stepping up! The community appreciates it! :)",
-    id: 36,
-  },
-  { threadId: 57, text: "This is a very clear report thanks @tomato.", id: 37 },
-  {
-    threadId: 58,
-    text:
-      "Hope it will be more heavy role once Atlas is live. Waiting for that.",
-    id: 38,
-  },
-  {
-    threadId: 60,
-    text:
-      'I forgot to note that "valid proposals" just means all proposals (including expired proposals) that were not cancelled or withdrawn.',
-    id: 39,
-  },
-];
-
-export default posts;

+ 16 - 22
src/types.ts

@@ -1,11 +1,5 @@
-import { ApiPromise } from "@polkadot/api";
-import { MemberId } from "@joystream/types/members";
-import {
-  ProposalParameters,
-  ProposalStatus,
-  VotingResults,
-} from "@joystream/types/proposals";
-import { AccountId, Nominations } from "@polkadot/types/interfaces";
+import { ProposalParameters, VotingResults } from "@joystream/types/proposals";
+import { Nominations } from "@polkadot/types/interfaces";
 import { Option } from "@polkadot/types/codec";
 import { StorageKey } from "@polkadot/types/primitive";
 
@@ -15,6 +9,11 @@ export interface Api {
   derive: any;
 }
 
+export interface Council {
+  round: number;
+  consuls: { member: { handle: string } }[];
+}
+
 export interface Status {
   now: number;
   block: Block;
@@ -22,7 +21,8 @@ export interface Status {
   block: number;
   connecting: boolean;
   loading: string;
-  council?: { stage: any; round: number; termEndsAt: number };
+  council?: Council;
+  //{ stage: any; round: number; termEndsAt: number };
   durations: number[];
   issued: number;
   price: number;
@@ -42,7 +42,6 @@ export interface IState {
   processingTasks: number;
   fetching: string;
   providers: any[];
-  queue: { key: string; action: any }[];
   status: Status;
   blocks: Block[];
   nominators: string[];
@@ -56,7 +55,6 @@ export interface IState {
   threads: Thread[];
   domain: string;
   proposalPosts: any[];
-  handles: Handles;
   members: Member[];
   mints: any[];
   tokenomics?: Tokenomics;
@@ -131,9 +129,10 @@ export interface ProposalDetail {
   detail?: any;
 }
 
-export interface Vote {
-  vote: string;
-  handle: string;
+interface Vote {
+  id: number;
+  vote: String;
+  member: { id: number; handle: string };
 }
 
 export type ProposalArray = number[];
@@ -227,10 +226,6 @@ export interface ProviderStatus {
   [propName: string]: boolean;
 }
 
-export interface Handles {
-  [key: string]: string;
-}
-
 export interface Tokenomics {
   price: string;
   totalIssuance: string;
@@ -283,12 +278,12 @@ export interface Transaction {
 }
 
 export interface Burner {
-  wallet: string,
+  wallet: string;
   totalburned: number;
 }
 
 export interface Burner {
-  wallet: string,
+  wallet: string;
   totalburned: number;
 }
 
@@ -302,7 +297,7 @@ export interface ValidatorApiResponse {
   startEra: number;
   endEra: number;
   totalBlocks: number;
-  report: ValidatorReportLineItem[]
+  report: ValidatorReportLineItem[];
 }
 
 export interface ValidatorReportLineItem {
@@ -315,7 +310,6 @@ export interface ValidatorReportLineItem {
   blocksCount: number;
 }
 
-
 export interface CalendarGroup {
   id: number;
   title: string;