Browse Source

Previous Councils + LeaderBoard

Joystream Stats 4 years ago
parent
commit
6104798c00

+ 128 - 116
src/App.tsx

@@ -1,4 +1,5 @@
 import React from "react";
+
 import "bootstrap/dist/css/bootstrap.min.css";
 import "./index.css";
 import { Routes, Loading } from "./components";
@@ -8,15 +9,27 @@ import proposalPosts from "./proposalPosts"; // TODO OPTIMIZE
 import axios from "axios";
 
 // types
-import { Api, Block, IState } from "./types";
+import { Api, Block, Handles, IState, Member } from "./types";
 import { types } from "@joystream/types";
 import { Seat } from "@joystream/types/augment/all/types";
 import { ApiPromise, WsProvider } from "@polkadot/api";
 import { AccountId, Header } from "@polkadot/types/interfaces";
 import { MemberId } from "@joystream/types/members";
+import { VoteKind } from "@joystream/types/proposals";
 
 interface IProps {}
 
+const councils = [
+  [129, 321, 439, 4, 2, 319],
+  [129, 318, 321, 4, 2, 319],
+  [495, 129, 321, 4, 2, 319],
+  [361, 129, 318, 321, 439, 319],
+  [361, 129, 321, 4, 2, 319],
+  [361, 129, 318, 321, 2, 319],
+];
+
+const version = 0.1;
+
 const initialState = {
   blocks: [],
   now: 0,
@@ -28,12 +41,14 @@ const initialState = {
   channels: [],
   posts: [],
   council: [],
+  councils,
   categories: [],
   threads: [],
   proposals: [],
   proposalCount: 0,
   domain,
   handles: {},
+  members: [],
   proposalPosts,
   reports: {},
 };
@@ -43,6 +58,7 @@ class App extends React.Component<IProps, IState> {
     const provider = new WsProvider(wsLocation);
     const api = await ApiPromise.create({ provider, types });
     await api.isReady;
+    console.log(`Connected to ${wsLocation}`);
 
     let blocks: Block[] = [];
     let lastBlock: Block = { id: 0, timestamp: 0, duration: 6 };
@@ -53,8 +69,7 @@ class App extends React.Component<IProps, IState> {
     );
     let stage: any = await api.query.councilElection.stage();
     let councilElection = { termEndsAt, stage: stage.toJSON(), round };
-    let councils = this.calculatePreviousCouncils(councilElection);
-    this.setState({ councilElection, councils });
+    this.setState({ councilElection });
     let stageEndsAt: number = termEndsAt;
 
     // let channels = [];
@@ -99,44 +114,42 @@ class App extends React.Component<IProps, IState> {
         const json = stage.toJSON();
         const key = Object.keys(json)[0];
         stageEndsAt = json[key];
-        console.log(id, stageEndsAt, json, key);
+        //console.log(id, stageEndsAt, json, key);
 
         // TODO duplicate code
         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 };
-        councils = this.calculatePreviousCouncils(councilElection);
-        this.setState({ councilElection, councils });
+        this.setState({ councilElection });
       }
     );
 
-    if (!this.state.council.length) this.fetchCouncil(api);
+    this.fetchCouncil(api);
     this.fetchProposals(api);
-    if (!this.state.validators.length) this.fetchValidators(api);
-    if (!this.state.nominators.length) this.fetchNominators(api);
-    //this.fetchTokenomics(api);
+    this.fetchValidators(api);
+    this.fetchNominators(api);
+
+    councils.map((council) =>
+      council.map((seat) => this.fetchMember(api, seat))
+    );
   }
 
-  async fetchStatus() {
+  async fetchTokenomics() {
+    console.log(`Updating tokenomics`);
     const { data } = await axios.get("https://status.joystream.org/status");
     if (!data) return;
     this.save("tokenomics", data);
   }
 
-  async fetchTokenomics(api: Api) {
-    const totalIssuance = (await api.query.balances.totalIssuance()).toNumber();
-    const tokenomics = { totalIssuance };
-    this.save("tokenomics", tokenomics);
-  }
-
   async fetchCouncil(api: Api) {
     const council: any = await api.query.council.activeCouncil();
     this.setState({ council });
     this.save(`council`, council);
-    council.map((seat: Seat) => this.fetchHandle(api, seat.member));
+    council.map((seat: Seat) => this.fetchMemberByAccount(api, seat.member));
   }
 
+  // proposals
   async fetchProposals(api: Api) {
     const proposalCount = await get.proposalCount(api);
     for (let i = proposalCount; i > 0; i--) this.fetchProposal(api, i);
@@ -145,7 +158,8 @@ class App extends React.Component<IProps, IState> {
     let { proposals } = this.state;
     const exists = proposals.find((p) => p && p.id === id);
 
-    if (exists && exists.stage === "Finalized") return;
+    if (exists && exists.votesByMemberId && exists.stage === "Finalized")
+      return;
     const proposal = await get.proposalDetail(api, id);
     if (!proposal) return;
     proposals[id] = proposal;
@@ -154,19 +168,21 @@ class App extends React.Component<IProps, IState> {
   }
 
   async fetchVotesPerProposal(api: Api, proposalId: number) {
-    const { proposals } = this.state;
+    const { councils, proposals } = this.state;
     const proposal = proposals.find((p) => p && p.id === proposalId);
     if (!proposal) return;
-    const { id, createdAt } = proposal;
 
-    const council = this.getCouncilAtBlock(createdAt);
+    let memberIds: { [key: string]: number } = {};
+    councils.map((ids: number[]) =>
+      ids.map((memberId: number) => memberIds[`${memberId}`]++)
+    );
 
-    proposal.votesByMember = await Promise.all(
-      council.map(async (seat: Seat) => {
-        const memberId = await this.getMemberIdByAccount(api, seat.member);
-        const handle = await this.getHandleByAccount(api, seat.member);
+    const { id } = proposal;
+    proposal.votesByMemberId = await Promise.all(
+      Object.keys(memberIds).map(async (key: string) => {
+        const memberId = parseInt(key);
         const vote = await this.fetchVoteByProposalByVoter(api, id, memberId);
-        return { vote: String(vote), account: seat.member, handle };
+        return { vote, memberId };
       })
     );
     proposals[id] = proposal;
@@ -176,92 +192,35 @@ class App extends React.Component<IProps, IState> {
   async fetchVoteByProposalByVoter(
     api: Api,
     proposalId: number,
-    memberId: MemberId
-  ) {
-    //const councilPerBlock
-    const vote = await api.query.proposalsEngine.voteExistsByProposalByVoter(
+    voterId: MemberId | number
+  ): Promise<string> {
+    const vote: VoteKind = await api.query.proposalsEngine.voteExistsByProposalByVoter(
       proposalId,
-      memberId
+      voterId
     );
-    return vote.toHuman();
-  }
-
-  calculatePreviousCouncils(council: {
-    stage: any;
-    round: number;
-    termEndsAt: number;
-  }) {
-    const rounds = [
-      [0, 0],
-      [57601, 259201, 201601], // r 144000
-      [259201, 460801], // r 201600
-      [460801, 662401], // r 201600
-      [662401, 864001], // r 201600
-      [864001, 1065601, 1008001], // 144000
-      [1065601, 1267201, 1209601], // 144000
-    ];
-
-    let councils = [];
-
-    const termDuration = 144000;
-    const announcingPeriod = 28800;
-    const votingPeriod = 14400;
-    const revealingPeriod = 14400;
-    const startToStart =
-      termDuration + announcingPeriod + votingPeriod + revealingPeriod; // 201600
-
-    const { stage, termEndsAt, round } = council;
-    let startsAt = termEndsAt - termDuration;
-    let endsAt = startsAt + startToStart;
-
-    for (let r = stage ? round - 1 : round; startsAt > 0; r--) {
-      if (rounds[r]) {
-        if (rounds[r][0] !== startsAt)
-          console.log(`wrong start`, round, rounds[r][0], startsAt);
-        if (rounds[r][1] !== endsAt)
-          console.log(`wrong end`, round, rounds[r][1], endsAt);
-      }
-      councils.push({ round: r, endsAt, startsAt, seats: [] }); // TODO
-      startsAt = startsAt - startToStart;
-      endsAt = startsAt + startToStart;
-    }
-    return councils;
-  }
+    const hasVoted: number = (
+      await api.query.proposalsEngine.voteExistsByProposalByVoter.size(
+        proposalId,
+        voterId
+      )
+    ).toNumber();
 
-  getCouncilAtBlock(block: number): Seat[] {
-    const { councils } = this.state;
-    try {
-      for (let round = councils.length; round > 0; round--) {
-        if (!councils[round]) continue;
-        if (block > councils[round].start) {
-          console.log(`block in council`, block, round);
-          return councils[round].seats;
-        }
-      }
-    } catch (e) {
-      console.log(`failed to find council at block`, block, e);
-    }
-    return this.state.council;
-  }
+    return hasVoted ? String(vote) : "";
 
-  async getHandleByAccount(api: Api, accountId: AccountId): Promise<string> {
-    return await this.fetchHandle(api, accountId);
-  }
-  async getMemberIdByAccount(
-    api: Api,
-    accountId: AccountId
-  ): Promise<MemberId> {
-    const id: MemberId = await api.query.members.memberIdsByRootAccountId(
-      accountId
-    );
-    return id;
+    //const vote = await api.query.proposalsEngine.voteExistsByProposalByVoter(
+    //  proposalId,
+    //  memberId
+    //);
+    //return String(vote.toHuman());
   }
 
+  // nominators, validators
+
   async fetchNominators(api: Api) {
     const nominatorEntries = await api.query.staking.nominators.entries();
     const nominators = nominatorEntries.map((n: any) => {
       const name = n[0].toHuman();
-      this.fetchHandle(api, `${name}`);
+      this.fetchMemberByAccount(api, name);
       return `${name}`;
     });
     this.save("nominators", nominators);
@@ -269,21 +228,55 @@ class App extends React.Component<IProps, IState> {
   async fetchValidators(api: Api) {
     const validatorEntries = await api.query.session.validators();
     const validators = await validatorEntries.map((v: any) => {
-      this.fetchHandle(api, v.toJSON());
+      this.fetchMemberByAccount(api, v.toJSON());
       return String(v);
     });
     this.save("validators", validators);
   }
-  async fetchHandle(api: Api, id: AccountId | string): Promise<string> {
-    let { handles } = this.state;
-    const exists = handles[String(id)];
+
+  // accounts
+  getHandle(account: AccountId | string): string {
+    const member = this.state.members.find(
+      (m) => String(m.account) === String(account)
+    );
+    return member ? member.handle : String(account);
+  }
+  async fetchMemberByAccount(
+    api: Api,
+    account: AccountId | string
+  ): Promise<Member> {
+    const exists = this.state.members.find(
+      (m: Member) => String(m.account) === String(account)
+    );
+    if (exists) return exists;
+
+    const id = await get.memberIdByAccount(api, account);
+    if (!id) return { id: -1, handle: `unknown`, account };
+    return await this.fetchMember(api, id);
+  }
+  async fetchMember(api: Api, id: MemberId | number): Promise<Member> {
+    const exists = this.state.members.find((m: Member) => m.id === id);
     if (exists) return exists;
 
-    const handle = await get.memberHandleByAccount(api, id);
-    handles[String(id)] = handle;
-    this.save("handles", handles);
-    return handle;
+    const membership = await get.membership(api, id);
+
+    const handle = String(membership.handle);
+    const account = String(membership.root_account);
+    const member: Member = { id, handle, account };
+    const members = this.state.members.concat(member);
+
+    if (members.length) this.save(`members`, members);
+    this.updateHandles(members);
+    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);
   }
+
+  // Reports
   async fetchReports() {
     const domain = `https://raw.githubusercontent.com/Joystream/community-repo/master/council-reports`;
     const apiBase = `https://api.github.com/repos/joystream/community-repo/contents/council-reports`;
@@ -331,6 +324,12 @@ class App extends React.Component<IProps, IState> {
     );
   }
 
+  loadMembers() {
+    const members = this.load("members");
+    if (!members) return;
+    this.updateHandles(members);
+    this.setState({ members });
+  }
   loadCouncil() {
     const council = this.load("council");
     if (council) this.setState({ council });
@@ -359,14 +358,25 @@ class App extends React.Component<IProps, IState> {
   loadReports() {
     const reports = this.load("reports");
     if (!reports) return this.fetchReports();
-    //console.log(`loaded reports`, reports);
     this.setState({ reports });
   }
   loadTokenomics() {
     const tokenomics = this.load("tokenomics");
-    this.setState({ tokenomics });
+    if (tokenomics) this.setState({ tokenomics });
+  }
+  loadMint() {
+    const mint = this.load("mint");
+    if (mint) this.setState({ mint });
+  }
+  clearData() {
+    this.save("version", version);
+    this.save("proposals", []);
   }
   async loadData() {
+    const lastVersion = this.load("version");
+    if (lastVersion !== version) return this.clearData();
+    console.log(`Loading data`);
+    await this.loadMembers();
     await this.loadCouncil();
     await this.loadProposals();
     await this.loadThreads();
@@ -399,13 +409,14 @@ class App extends React.Component<IProps, IState> {
 
   render() {
     if (this.state.loading) return <Loading />;
-    return <Routes {...this.state} />;
+    return <Routes getHandle={this.getHandle} {...this.state} />;
   }
 
   componentDidMount() {
     this.loadData();
-    this.fetchStatus();
     this.initializeSocket();
+    this.fetchTokenomics();
+    setInterval(this.fetchTokenomics, 300000);
   }
   componentWillUnmount() {
     console.log("unmounting...");
@@ -413,8 +424,9 @@ class App extends React.Component<IProps, IState> {
   constructor(props: IProps) {
     super(props);
     this.state = initialState;
+    this.fetchTokenomics = this.fetchTokenomics.bind(this);
     this.fetchProposal = this.fetchProposal.bind(this);
-    this.fetchHandle = this.fetchHandle.bind(this);
+    this.getHandle = this.getHandle.bind(this);
   }
 }
 

+ 3 - 2
src/components/Council/index.tsx

@@ -24,20 +24,21 @@ const Council = (props: {
           <div className="d-flex flex-row">
             {council.slice(0, half).map((seat: Seat) => (
               <div key={String(seat.member)} className="col">
-                <User id={String(seat.member)} handle={handles[seat.member]} />
+                <User id={String(seat.member)} handle={handles[String(seat.member)]} />
               </div>
             ))}
           </div>
           <div className="d-flex flex-row">
             {council.slice(half).map((seat: Seat) => (
               <div key={String(seat.member)} className="col">
-                <User id={String(seat.member)} handle={handles[seat.member]} />
+                <User id={String(seat.member)} handle={handles[String(seat.member)]} />
               </div>
             ))}
           </div>
         </div>
       )) || <Loading />}
       <hr />
+
       <Link to={`/tokenomics`}>Reports</Link>
     </div>
   );

+ 150 - 0
src/components/Councils/Leaderboard.tsx

@@ -0,0 +1,150 @@
+import React from "react";
+import { Table } from "react-bootstrap";
+
+import { Member, ProposalDetail } from "../../types";
+
+interface CouncilMember {
+  handle: string;
+  votes: number;
+  proposalCount: number;
+  percentage: number;
+}
+
+interface CouncilVotes {
+  proposalCount: number;
+  members: CouncilMember[];
+}
+
+const LeaderBoard = (props: {
+  proposals: ProposalDetail[];
+  members: Member[];
+  councils: number[][];
+  cycle: number;
+}) => {
+  const { cycle, councils, proposals } = props;
+
+  const summarizeVotes = (id: number, propList: ProposalDetail[]) => {
+    let votes = 0;
+    propList.forEach((p) => {
+      if (!p || !p.votesByMemberId) return;
+      const vote = p.votesByMemberId.find((v) => v.memberId === id);
+      if (vote && vote.vote !== `Reject`) votes++;
+    });
+    return votes;
+  };
+
+  let councilMembers: Member[] = [];
+
+  const councilVotes: CouncilVotes[] = councils.map(
+    (council, i: number): CouncilVotes => {
+      const start = 57601 + i * cycle;
+      const end = 57601 + (i + 1) * cycle;
+      const proposalsRound = proposals.filter(
+        (p) => p && p.createdAt > start && p.createdAt < end
+      );
+      const proposalCount = proposalsRound.length;
+
+      const members: CouncilMember[] = council.map(
+        (id: number): CouncilMember => {
+          const member = props.members.find((m) => m.id === id);
+          if (!member)
+            return { handle: ``, votes: 0, proposalCount, percentage: 0 };
+
+          councilMembers.find((m) => m.id === id) ||
+            councilMembers.push(member);
+
+          let votes = summarizeVotes(Number(member.id), proposalsRound);
+          const percentage = Math.round((100 * votes) / proposalCount);
+          return { handle: member.handle, votes, proposalCount, percentage };
+        }
+      );
+
+      return { proposalCount, members };
+    }
+  );
+
+  councilMembers = councilMembers
+    .map((m) => {
+      return { ...m, id: summarizeVotes(Number(m.id), props.proposals) };
+    })
+    .sort((a, b) => b.id - a.id);
+
+  return (
+    <div className={`text-light m-3`}>
+      <h2 className="w-100 text-center text-light">Votes per Council Member</h2>
+      <Table className={`text-light`}>
+        <thead>
+          <tr>
+            <th>Council Member</th>
+            <th>Total Votes</th>
+            {councilVotes.map((c, i: number) => (
+              <th key={`round-${i + 1}`}>Round {1 + i}</th>
+            ))}
+          </tr>
+        </thead>
+        <tbody>
+          {councilMembers.map((member: Member) => (
+            <MemberRow
+              key={member.handle}
+              member={member}
+              votes={councilVotes}
+            />
+          ))}
+          <tr>
+            <td>Proposals</td>
+            <td>{proposals.length}</td>
+            {councilVotes.map((round, i: number) => (
+              <td key={`round-${i + 1}`}>{round.proposalCount}</td>
+            ))}
+          </tr>
+        </tbody>
+      </Table>
+    </div>
+  );
+};
+
+const MemberRow = (props: { member: Member; votes: CouncilVotes[] }) => {
+  const { votes } = props;
+  const { handle } = props.member;
+  let totalVotes = 0;
+  let totalProposals = 0;
+
+  votes.forEach((c) => {
+    const m = c.members.find((member) => member.handle === handle);
+    if (!m) return;
+    totalVotes += m.votes;
+    totalProposals += m.proposalCount;
+  });
+
+  const totalPercent = Math.round((100 * totalVotes) / totalProposals);
+
+  return (
+    <tr key={handle}>
+      <td>{handle}</td>
+      <td>
+        {totalVotes} / {totalProposals} ({totalPercent}%)
+      </td>
+      {props.votes.map((c, round: number) => (
+        <RoundVotes
+          key={`round-${round + 1}-${handle}`}
+          member={c.members.find((member) => member.handle === handle)}
+        />
+      ))}
+    </tr>
+  );
+};
+
+const RoundVotes = (props: {
+  member?: { handle: string; votes: number; percentage: number };
+}) => {
+  if (!props.member || props.member.handle === ``) return <td />;
+
+  const { votes, percentage } = props.member;
+  return (
+    <td>
+      {votes} ({percentage}%)
+    </td>
+  );
+};
+
+export default LeaderBoard;

+ 176 - 0
src/components/Councils/index.tsx

@@ -0,0 +1,176 @@
+import React from "react";
+import { Button, OverlayTrigger, Tooltip, Table } from "react-bootstrap";
+import { Link } from "react-router-dom";
+import { Member, ProposalDetail } from "../../types";
+import LeaderBoard from "./Leaderboard";
+
+const announcingPeriod = 28800;
+const votingPeriod = 14400; // 43200
+const revealingPeriod = 14400;
+const termDuration = 144000;
+const cycle = termDuration + announcingPeriod + votingPeriod + revealingPeriod; // 201600
+
+const Back = () => {
+  return (
+    <Button variant="secondary" className="btn btn-secondary p-1 m-0">
+      <Link to={`/tokenomics`}>back</Link>
+    </Button>
+  );
+};
+
+const Rounds = (props: {
+  members: Member[];
+  councils: number[][];
+  proposals: any;
+}) => {
+  const { councils, members, proposals } = props;
+  return (
+    <div className="w-100">
+      <Table className="w-100 text-light">
+        <thead>
+          <tr>
+            <th>Round</th>
+            <th>Announced</th>
+            <th>Voted</th>
+            <th>Revealed</th>
+            <th>Start</th>
+            <th>End</th>
+          </tr>
+        </thead>
+        <tbody>
+          {councils.map((council, i: number) => (
+            <tr key={i} className="">
+              <td>{i + 1}</td>
+              <td>{1 + i * cycle}</td>
+              <td>{28801 + i * cycle}</td>
+              <td>{43201 + i * cycle}</td>
+              <td>{57601 + i * cycle}</td>
+              <td>{57601 + 201600 + i * cycle}</td>
+            </tr>
+          ))}
+        </tbody>
+      </Table>
+
+      <LeaderBoard
+        cycle={cycle}
+        councils={councils}
+        members={members}
+        proposals={proposals}
+      />
+
+      <h2 className="w-100 text-center text-light">Votes per Proposal</h2>
+      {councils.map((council, i: number) => (
+        <CouncilVotes
+          key={i}
+          round={i + 1}
+          council={council}
+          members={props.members}
+          proposals={props.proposals.filter(
+            (p: ProposalDetail) =>
+              p &&
+              p.createdAt > 57601 + i * cycle &&
+              p.createdAt < 57601 + (i + 1) * cycle
+          )}
+        />
+      ))}
+    </div>
+  );
+};
+
+const CouncilVotes = (props: {
+  round: number;
+  council: number[];
+  members: Member[];
+  proposals: ProposalDetail[];
+}) => {
+  const { council, members, proposals, round } = props;
+
+  let councilMembers: Member[] = [];
+  council.forEach((id) => {
+    const member = members.find((m) => m.id === id);
+    councilMembers.push(
+      member || { id, handle: String(id), account: String(id) }
+    ); // TODO
+  });
+
+  const fail = "btn btn-outline-danger";
+  const styles: { [key: string]: string } = {
+    Approved: "btn btn-outline-success",
+    Rejected: fail,
+    Canceled: fail,
+    Expired: fail,
+    Pending: "btn btn-outline-warning",
+  };
+
+  return (
+    <Table className="text-light text-center">
+      <thead>
+        <tr>
+          <th className={`text-left`}>Round {round}</th>
+          {councilMembers.map((member) => (
+            <th key={member.handle}>{member.handle}</th>
+          ))}
+        </tr>
+      </thead>
+      <tbody>
+        {proposals
+          .sort((a, b) => a.createdAt - b.createdAt)
+          .map((p) => (
+            <tr key={p.id} className={`text-left`}>
+              <OverlayTrigger
+                placement="right"
+                overlay={
+                  <Tooltip id={`tooltip-${p.id}`}>
+                    <div>{p.result}</div>
+                  </Tooltip>
+                }
+              >
+                <td className={`text-left p-1 ${styles[p.result]}`}>
+                  {p.title} ({p.id})
+                </td>
+              </OverlayTrigger>
+
+              {p.votesByMemberId &&
+                council.map((memberId) => (
+                  <td key={memberId}>
+                    <Vote votes={p.votesByMemberId} memberId={memberId} />
+                  </td>
+                ))}
+            </tr>
+          ))}
+        <tr>
+          <td className="text-center" colSpan={7}>
+            <Back />
+          </td>
+        </tr>
+      </tbody>
+    </Table>
+  );
+};
+
+const Vote = (props: {
+  votes?: { vote: string; memberId: number }[];
+  memberId: number;
+}) => {
+  const { votes, memberId } = props;
+
+  if (!votes) return <div></div>;
+  const v = votes.find((v) => v.memberId === memberId);
+  if (!v) return <div></div>;
+
+  const styles: { [key: string]: string } = {
+    Approve: "btn btn-success",
+    Reject: "btn btn-outline-danger",
+    Abstain: "btn btn-outline-light",
+  };
+
+  //console.log(rejections)
+
+  return (
+    <div style={{ width: 100 }} className={`text-center p-1 ${styles[v.vote]}`}>
+      {v.vote}
+    </div>
+  );
+};
+
+export default Rounds;

+ 67 - 35
src/components/Proposals/Row.tsx

@@ -1,4 +1,6 @@
 import React from "react";
+import { Member, ProposalPost, Vote } from "../../types";
+import { ProposalParameters, VotingResults } from "@joystream/types/proposals";
 import { Button, OverlayTrigger, Tooltip, Table } from "react-bootstrap";
 import Bar from "./Bar";
 import Posts from "./Posts";
@@ -16,8 +18,61 @@ const colors: { [key: string]: string } = {
   Pending: "",
 };
 
-const ProposalRow = (props: any) => {
-  const { block, createdAt, description, finalizedAt, id, title, type } = props;
+const VotesTooltip = (props: {
+  getHandle: (id: number) => string;
+  votes?: Vote[];
+}) => {
+  const { getHandle } = props;
+  let votes;
+
+  if (props.votes)
+    votes = props.votes.filter((v) => (v.vote === `` ? false : true));
+  if (!votes) return <div>Fetching votes..</div>;
+  if (!votes.length) return <div>No votes yet.</div>;
+
+  return (
+    <Table className="text-left text-light">
+      <tbody>
+        {votes.map((v: { memberId: number; vote: string }) => (
+          <tr key={`vote-${v.memberId}`}>
+            <td>{getHandle(v.memberId)}:</td>
+            <td>{v.vote}</td>
+          </tr>
+        ))}
+      </tbody>
+    </Table>
+  );
+};
+
+const ProposalRow = (props: {
+  block: number;
+  createdAt: number;
+  finalizedAt: number;
+  startTime: number;
+  description: string;
+  id: number;
+  parameters: ProposalParameters;
+  exec: boolean;
+  result: string;
+  stage: string;
+  title: string;
+  type: string;
+  votes: VotingResults;
+  members: Member[];
+  posts: ProposalPost[];
+  votesByMemberId?: Vote[];
+}) => {
+  const {
+    block,
+    createdAt,
+    description,
+    finalizedAt,
+    id,
+    title,
+    type,
+    votes,
+    members,
+  } = props;
 
   const url = `https://pioneer.joystreamstats.live/#/proposals/${id}`;
   let result: string = props.result ? props.result : props.stage;
@@ -28,8 +83,7 @@ const ProposalRow = (props: any) => {
   const finalized =
     finalizedAt && formatTime(props.startTime + finalizedAt * 6000);
 
-  let { votingPeriod } = props.parameters;
-  if (votingPeriod.toNumber) votingPeriod = votingPeriod.toNumber();
+  const period = +props.parameters.votingPeriod;
 
   let blocks = finalizedAt ? finalizedAt - createdAt : block - createdAt;
   //if (blocks < 0) blocks = 0; // TODO make sure block is defined
@@ -39,11 +93,10 @@ const ProposalRow = (props: any) => {
   const hoursStr = hours ? `${hours}h` : "";
   const duration = blocks ? `${daysStr} ${hoursStr} / ${blocks} blocks` : "";
 
-  const { abstensions, approvals, rejections, slashes } = props.votes;
-  const votes = [abstensions, approvals, rejections, slashes];
-  votes.map((o: number | { toNumber: () => number }) =>
-    typeof o === "number" ? o : o.toNumber && o.toNumber()
-  );
+  const getHandle = (memberId: number): string => {
+    const member = members.find((m) => m.id === memberId);
+    return member ? member.handle : String(memberId);
+  };
 
   return (
     <tr key={id}>
@@ -63,43 +116,22 @@ const ProposalRow = (props: any) => {
       </td>
 
       <OverlayTrigger
+        placement="left"
         overlay={
-          <Tooltip id={id}>
-            <div>
-              {props.votesByMember ? (
-                <Table className="text-left text-light">
-                  <tbody>
-                    {props.votesByMember.map(
-                      (v: { handle: string; vote: string }) => (
-                        <tr key={`${id}-${v.handle}`}>
-                          <td>{v.handle}:</td>
-                          <td>{v.vote}</td>
-                        </tr>
-                      )
-                    )}
-                  </tbody>
-                </Table>
-              ) : (
-                `Fetching votes..`
-              )}
-            </div>
+          <Tooltip id={`votes-${id}`}>
+            <VotesTooltip getHandle={getHandle} votes={props.votesByMemberId} />
           </Tooltip>
         }
       >
         <td className={color}>
           <b>{result}</b>
           <br />
-          {votes.join(" / ")}
+          {JSON.stringify(Object.values(votes))}
         </td>
       </OverlayTrigger>
 
       <td className="text-left  justify-content-center">
-        <Bar
-          id={id}
-          blocks={blocks}
-          period={votingPeriod}
-          duration={duration}
-        />
+        <Bar id={id} blocks={blocks} period={period} duration={duration} />
       </td>
 
       <td className="text-right">{created}</td>

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

@@ -1,7 +1,7 @@
 import React from "react";
 import { Button, Table } from "react-bootstrap";
 import { Link } from "react-router-dom";
-import { ProposalDetail } from "../../types";
+import { Member, ProposalDetail, ProposalPost } from "../../types";
 import Loading from "..//Loading";
 import Row from "./Row";
 
@@ -9,9 +9,10 @@ const Proposals = (props: {
   now: number;
   block: number;
   proposals: ProposalDetail[];
-  proposalPosts: any[];
+  proposalPosts: ProposalPost[];
+  members: Member[];
 }) => {
-  const { proposalPosts, block, now } = props;
+  const { proposalPosts, block, now, members } = props;
   const startTime: number = now - block * 6000;
 
   // prepare proposals
@@ -74,6 +75,7 @@ const Proposals = (props: {
               key={p.id}
               {...p}
               block={block}
+              members={members}
               startTime={startTime}
               posts={proposalPosts.filter((post) => post.threadId === p.id)}
             />

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

@@ -1,5 +1,12 @@
 import { Switch, Route } from "react-router-dom";
-import { Council, Dashboard, Proposals, Proposal, Tokenomics } from "..";
+import {
+  Council,
+  Councils,
+  Dashboard,
+  Proposals,
+  Proposal,
+  Tokenomics,
+} from "..";
 import { IState } from "../../types";
 
 const Routes = (props: IState) => {
@@ -15,6 +22,7 @@ const Routes = (props: IState) => {
         render={(routeprops) => <Proposal {...routeprops} {...props} />}
       />
       <Route path="/proposals" render={() => <Proposals {...props} />} />
+      <Route path="/councils" render={() => <Councils {...props} />} />
       <Route path="/council" render={() => <Council {...props} />} />
       <Route path="/" render={() => <Dashboard {...props} />} />
     </Switch>

+ 4 - 0
src/components/Tokenomics/index.tsx

@@ -33,6 +33,10 @@ const CouncilReports = (props: IProps) => {
           {tokenomics ? <Overview {...tokenomics} /> : <Loading />}
         </div>
 
+        <Link to={`/councils`}>
+          <Button variant="dark">Previous Councils</Button>
+        </Link>
+
         <Back />
       </div>
 

+ 6 - 5
src/components/index.ts

@@ -1,10 +1,11 @@
-export { default as Routes } from "./Routes"
-export { default as Council } from "./Council"
+export { default as Routes } from "./Routes";
+export { default as Council } from "./Council";
+export { default as Councils } from "./Councils";
 export { default as Dashboard } from "./Dashboard";
 export { default as Proposals } from "./Proposals";
-export { default as ProposalLink } from "./Proposals/ProposalLink"
-export { default as Proposal } from "./Proposals/Proposal"
-export { default as ActiveProposals } from "./Proposals/Active"
+export { default as ProposalLink } from "./Proposals/ProposalLink";
+export { default as Proposal } from "./Proposals/Proposal";
+export { default as ActiveProposals } from "./Proposals/Active";
 export { default as Loading } from "./Loading";
 export { default as User } from "./User";
 export { default as Tokenomics } from "./Tokenomics";

+ 136 - 142
src/lib/announcements.ts

@@ -1,35 +1,28 @@
-import {
-  Api,
-  Council,
-  Member,
-  ProposalDetail,
-  Proposals,
-  Summary,
-} from '../types'
-import { BlockNumber } from '@polkadot/types/interfaces'
-import { Channel } from '@joystream/types/augment'
-import { Category, Thread, Post } from '@joystream/types/forum'
-import { formatTime } from './util'
+import { Api, Council, ProposalDetail, Proposals, Summary } from "../types";
+import { BlockNumber } from "@polkadot/types/interfaces";
+import { Channel } from "@joystream/types/augment";
+import { Category, Thread, Post } from "@joystream/types/forum";
+import { formatTime } from "./util";
 import {
   categoryById,
   memberHandle,
   memberHandleByAccount,
   proposalDetail,
-} from './getters'
-import {domain} from '../config'
-import moment from 'moment'
+} from "./getters";
+import { domain } from "../config";
+import moment from "moment";
 
-const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
+const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
 
 // query API repeatedly to ensure a result
 const query = async (test: string, cb: () => Promise<any>): Promise<any> => {
-  let result = await cb()
+  let result = await cb();
   for (let i: number = 0; i < 10; i++) {
-    if (result[test] !== '') return result
-    result = await cb()
-    await sleep(5000)
+    if (result[test] !== "") return result;
+    result = await cb();
+    await sleep(5000);
   }
-}
+};
 
 // announce latest channels
 export const channels = async (
@@ -37,23 +30,23 @@ export const channels = async (
   channels: number[],
   sendMessage: (msg: string) => void
 ): Promise<number> => {
-  const [last, current] = channels
-  const messages: string[] = []
+  const [last, current] = channels;
+  const messages: string[] = [];
 
   for (let id: number = +last + 1; id <= current; id++) {
-    const channel: Channel = await query('title', () =>
+    const channel: Channel = await query("title", () =>
       api.query.contentWorkingGroup.channelById(id)
-    )
-    const member: Member = { id: channel.owner, handle: '', url: '' }
-    member.handle = await memberHandle(api, member.id.toJSON())
-    member.url = `${domain}/#/members/${member.handle}`
+    );
+    const member: any = { id: channel.owner, handle: "", url: "" };
+    member.handle = await memberHandle(api, member.id.toJSON());
+    member.url = `${domain}/#/members/${member.handle}`;
     messages.push(
       `<b>Channel <a href="${domain}/#//media/channels/${id}">${channel.title}</a> by <a href="${member.url}">${member.handle} (${member.id})</a></b>`
-    )
+    );
   }
-  sendMessage(messages.join('\r\n\r\n'))
-  return current
-}
+  sendMessage(messages.join("\r\n\r\n"));
+  return current;
+};
 
 // announce council change
 export const council = async (
@@ -62,34 +55,34 @@ export const council = async (
   currentBlock: number,
   sendMessage: (msg: string) => void
 ): Promise<Council> => {
-  const round: number = await api.query.councilElection.round()
-  const stage: any = await api.query.councilElection.stage()
-  let stageString = Object.keys(JSON.parse(JSON.stringify(stage)))[0]
-  let msg = ''
+  const round: number = await api.query.councilElection.round();
+  const stage: any = await api.query.councilElection.stage();
+  let stageString = Object.keys(JSON.parse(JSON.stringify(stage)))[0];
+  let msg = "";
 
   if (!stage || stage.toJSON() === null) {
-    stageString = 'elected'
-    const councilEnd: BlockNumber = await api.query.council.termEndsAt()
-    const termDuration: BlockNumber = await api.query.councilElection.newTermDuration()
-    const block = councilEnd.toNumber() - termDuration.toNumber()
-    const remainingBlocks: number = councilEnd.toNumber() - currentBlock
+    stageString = "elected";
+    const councilEnd: BlockNumber = await api.query.council.termEndsAt();
+    const termDuration: BlockNumber = await api.query.councilElection.newTermDuration();
+    const block = councilEnd.toNumber() - termDuration.toNumber();
+    const remainingBlocks: number = councilEnd.toNumber() - currentBlock;
     const endDate = moment()
       // .add(remainingBlocks * 6, "s")
-      .format('DD/MM/YYYY')
-    msg = `<a href="${domain}/#/council/members">Council ${round}</a> elected at block ${block} until block ${councilEnd}. Next election: ${endDate} (${remainingBlocks} blocks)`
+      .format("DD/MM/YYYY");
+    msg = `<a href="${domain}/#/council/members">Council ${round}</a> elected at block ${block} until block ${councilEnd}. Next election: ${endDate} (${remainingBlocks} blocks)`;
   } else {
-    if (stageString === 'Announcing') {
-      msg = `Announcing election for round ${round} started.<a href="${domain}/#/council/applicants">Apply now!</a>`
-    } else if (stageString === 'Voting') {
-      msg = `Voting stage for council election started. <a href="${domain}/#/council/applicants">Vote now!</a>`
-    } else if (stageString === 'Revealing') {
-      msg = `Revealing stage for council election started. <a href="${domain}/#/council/votes">Don't forget to reveal your vote!</a>`
-    } else console.log(`[council] unrecognized stage: ${stageString}`)
+    if (stageString === "Announcing") {
+      msg = `Announcing election for round ${round} started.<a href="${domain}/#/council/applicants">Apply now!</a>`;
+    } else if (stageString === "Voting") {
+      msg = `Voting stage for council election started. <a href="${domain}/#/council/applicants">Vote now!</a>`;
+    } else if (stageString === "Revealing") {
+      msg = `Revealing stage for council election started. <a href="${domain}/#/council/votes">Don't forget to reveal your vote!</a>`;
+    } else console.log(`[council] unrecognized stage: ${stageString}`);
   }
 
-  if (round !== council.round && stageString !== council.last) sendMessage(msg)
-  return { round, last: stageString }
-}
+  if (round !== council.round && stageString !== council.last) sendMessage(msg);
+  return { round, last: stageString };
+};
 
 // forum
 // announce latest categories
@@ -98,17 +91,17 @@ export const categories = async (
   category: number[],
   sendMessage: (msg: string) => void
 ): Promise<number> => {
-  const messages: string[] = []
+  const messages: string[] = [];
 
   for (let id: number = +category[0] + 1; id <= category[1]; id++) {
-    const cat: Category = await query('title', () => categoryById(api, id))
-    const msg = `Category ${id}: <b><a href="${domain}/#/forum/categories/${id}">${cat.title}</a></b>`
-    messages.push(msg)
+    const cat: Category = await query("title", () => categoryById(api, id));
+    const msg = `Category ${id}: <b><a href="${domain}/#/forum/categories/${id}">${cat.title}</a></b>`;
+    messages.push(msg);
   }
 
-  sendMessage(messages.join('\r\n\r\n'))
-  return category[1]
-}
+  sendMessage(messages.join("\r\n\r\n"));
+  return category[1];
+};
 
 // announce latest posts
 export const posts = async (
@@ -116,32 +109,32 @@ export const posts = async (
   posts: number[],
   sendMessage: (msg: string) => void
 ): Promise<number> => {
-  const [last, current] = posts
-  const messages: string[] = []
+  const [last, current] = posts;
+  const messages: string[] = [];
 
   for (let id: number = +last + 1; id <= current; id++) {
-    const post: Post = await query('current_text', () =>
+    const post: Post = await query("current_text", () =>
       api.query.forum.postById(id)
-    )
-    const replyId: number = post.nr_in_thread.toNumber()
-    const message: string = post.current_text
-    const excerpt: string = message.substring(0, 100)
-    const threadId: number = post.thread_id.toNumber()
-    const thread: Thread = await query('title', () =>
+    );
+    const replyId: number = post.nr_in_thread.toNumber();
+    const message: string = post.current_text;
+    const excerpt: string = message.substring(0, 100);
+    const threadId: number = post.thread_id.toNumber();
+    const thread: Thread = await query("title", () =>
       api.query.forum.threadById(threadId)
-    )
-    const threadTitle: string = thread.title
-    const category: Category = await query('title', () =>
+    );
+    const threadTitle: string = thread.title;
+    const category: Category = await query("title", () =>
       categoryById(api, thread.category_id.toNumber())
-    )
-    const handle = await memberHandleByAccount(api, post.author_id.toJSON())
-    const msg = `<b><a href="${domain}/#/members/${handle}">${handle}</a> posted <a href="${domain}/#/forum/threads/${threadId}?replyIdx=${replyId}">${threadTitle}</a> in <a href="${domain}/#/forum/categories/${category.id}">${category.title}</a>:</b>\n\r<i>${excerpt}</i> <a href="${domain}/#/forum/threads/${threadId}?replyIdx=${replyId}">more</a>`
-    messages.push(msg)
+    );
+    const handle = await memberHandleByAccount(api, post.author_id.toJSON());
+    const msg = `<b><a href="${domain}/#/members/${handle}">${handle}</a> posted <a href="${domain}/#/forum/threads/${threadId}?replyIdx=${replyId}">${threadTitle}</a> in <a href="${domain}/#/forum/categories/${category.id}">${category.title}</a>:</b>\n\r<i>${excerpt}</i> <a href="${domain}/#/forum/threads/${threadId}?replyIdx=${replyId}">more</a>`;
+    messages.push(msg);
   }
 
-  sendMessage(messages.join('\r\n\r\n'))
-  return current
-}
+  sendMessage(messages.join("\r\n\r\n"));
+  return current;
+};
 
 // announce latest threads
 export const threads = async (
@@ -149,25 +142,25 @@ export const threads = async (
   threads: number[],
   sendMessage: (msg: string) => void
 ): Promise<number> => {
-  const [last, current] = threads
-  const messages: string[] = []
+  const [last, current] = threads;
+  const messages: string[] = [];
 
   for (let id: number = +last + 1; id <= current; id++) {
-    const thread: Thread = await query('title', () =>
+    const thread: Thread = await query("title", () =>
       api.query.forum.threadById(id)
-    )
-    const { title, author_id } = thread
-    const handle: string = await memberHandleByAccount(api, author_id.toJSON())
-    const category: Category = await query('title', () =>
+    );
+    const { title, author_id } = thread;
+    const handle: string = await memberHandleByAccount(api, author_id.toJSON());
+    const category: Category = await query("title", () =>
       categoryById(api, thread.category_id.toNumber())
-    )
-    const msg = `Thread ${id}: <a href="${domain}/#/forum/threads/${id}">"${title}"</a> by <a href="${domain}/#/members/${handle}">${handle}</a> in category "<a href="${domain}/#/forum/categories/${category.id}">${category.title}</a>" `
-    messages.push(msg)
+    );
+    const msg = `Thread ${id}: <a href="${domain}/#/forum/threads/${id}">"${title}"</a> by <a href="${domain}/#/members/${handle}">${handle}</a> in category "<a href="${domain}/#/forum/categories/${category.id}">${category.title}</a>" `;
+    messages.push(msg);
   }
 
-  sendMessage(messages.join('\r\n\r\n'))
-  return current
-}
+  sendMessage(messages.join("\r\n\r\n"));
+  return current;
+};
 
 // announce latest proposals
 export const proposals = async (
@@ -175,51 +168,51 @@ export const proposals = async (
   prop: Proposals,
   sendMessage: (msg: string) => void
 ): Promise<Proposals> => {
-  let { current, last, active, executing } = prop
+  let { current, last, active, executing } = prop;
 
   for (let id: number = +last + 1; id <= current; id++) {
-    const proposal: ProposalDetail = await proposalDetail(api, id)
-    const { createdAt, message, parameters } = proposal
-    const votingEndsAt = createdAt + parameters.votingPeriod.toNumber()
-    const msg = `Proposal ${id} <b>created</b> at block ${createdAt}.\r\n${message}\r\nYou can vote until block ${votingEndsAt}.`
-    sendMessage(msg)
-    active.push(id)
+    const proposal: ProposalDetail = await proposalDetail(api, id);
+    const { createdAt, message, parameters } = proposal;
+    const votingEndsAt = createdAt + parameters.votingPeriod.toNumber();
+    const msg = `Proposal ${id} <b>created</b> at block ${createdAt}.\r\n${message}\r\nYou can vote until block ${votingEndsAt}.`;
+    sendMessage(msg);
+    active.push(id);
   }
 
   for (const id of active) {
-    const proposal: ProposalDetail = await proposalDetail(api, id)
-    const { finalizedAt, message, parameters, result, stage } = proposal
-    if (stage === 'Finalized') {
-      let label: string = result
-      if (result === 'Approved') {
-        const executed = parameters.gracePeriod.toNumber() > 0 ? false : true
-        label = executed ? 'Executed' : 'Finalized'
-        if (!executed) executing.push(id)
+    const proposal: ProposalDetail = await proposalDetail(api, id);
+    const { finalizedAt, message, parameters, result, stage } = proposal;
+    if (stage === "Finalized") {
+      let label: string = result;
+      if (result === "Approved") {
+        const executed = parameters.gracePeriod.toNumber() > 0 ? false : true;
+        label = executed ? "Executed" : "Finalized";
+        if (!executed) executing.push(id);
       }
-      const msg = `Proposal ${id} <b>${label}</b> at block ${finalizedAt}.\r\n${message}`
-      sendMessage(msg)
-      active = active.filter(a => a !== id)
+      const msg = `Proposal ${id} <b>${label}</b> at block ${finalizedAt}.\r\n${message}`;
+      sendMessage(msg);
+      active = active.filter((a) => a !== id);
     }
   }
 
   for (const id of executing) {
-    const proposal = await proposalDetail(api, id)
-    const { exec, finalizedAt, message, parameters } = proposal
-    const execStatus = exec ? Object.keys(exec)[0] : ''
-    const label = execStatus === 'Executed' ? 'has been' : 'failed to be'
-    const block = +finalizedAt + parameters.gracePeriod.toNumber()
-    const msg = `Proposal ${id} <b>${label} executed</b> at block ${block}.\r\n${message}`
-    sendMessage(msg)
-    executing = executing.filter(e => e !== id)
+    const proposal = await proposalDetail(api, id);
+    const { exec, finalizedAt, message, parameters } = proposal;
+    const execStatus = exec ? Object.keys(exec)[0] : "";
+    const label = execStatus === "Executed" ? "has been" : "failed to be";
+    const block = +finalizedAt + parameters.gracePeriod.toNumber();
+    const msg = `Proposal ${id} <b>${label} executed</b> at block ${block}.\r\n${message}`;
+    sendMessage(msg);
+    executing = executing.filter((e) => e !== id);
   }
 
-  return { current, last: current, active, executing }
-}
+  return { current, last: current, active, executing };
+};
 
 // heartbeat
 
 const getAverage = (array: number[]) =>
-  array.reduce((a: number, b: number) => a + b, 0) / array.length
+  array.reduce((a: number, b: number) => a + b, 0) / array.length;
 
 export const heartbeat = async (
   api: Api,
@@ -228,26 +221,27 @@ export const heartbeat = async (
   accountId: string,
   sendMessage: (msg: string) => void
 ): Promise<void> => {
-  const { blocks, nominators, validators } = summary
-  const avgDuration = blocks.reduce((a, b) => a + b.duration, 0) / blocks.length
-  const era: any = await api.query.staking.currentEra()
-  const totalStake: any = await api.query.staking.erasTotalStake(parseInt(era))
-  const stakers = await api.query.staking.erasStakers(parseInt(era), accountId)
-  const stakerCount = stakers.others.length
-  const avgStake = parseInt(totalStake.toString()) / stakerCount
+  const { blocks, nominators, validators } = summary;
+  const avgDuration =
+    blocks.reduce((a, b) => a + b.duration, 0) / blocks.length;
+  const era: any = await api.query.staking.currentEra();
+  const totalStake: any = await api.query.staking.erasTotalStake(parseInt(era));
+  const stakers = await api.query.staking.erasStakers(parseInt(era), accountId);
+  const stakerCount = stakers.others.length;
+  const avgStake = parseInt(totalStake.toString()) / stakerCount;
 
   console.log(`
   Blocks produced during ${timePassed}h in era ${era}: ${blocks.length}
   Average blocktime: ${Math.floor(avgDuration) / 1000} s
   Average stake: ${avgStake / 1000000} M JOY (${stakerCount} stakers)
   Average number of nominators: ${getAverage(nominators)}
-  Average number of validators: ${getAverage(validators)}`)
-}
+  Average number of validators: ${getAverage(validators)}`);
+};
 
 export const formatProposalMessage = (data: string[]): string => {
-  const [id, title, type, stage, result, handle] = data
-  return `<b>Type</b>: ${type}\r\n<b>Proposer</b>: <a href="${domain}/#/members/${handle}">${handle}</a>\r\n<b>Title</b>: <a href="${domain}/#/proposals/${id}">${title}</a>\r\n<b>Stage</b>: ${stage}\r\n<b>Result</b>: ${result}`
-}
+  const [id, title, type, stage, result, handle] = data;
+  return `<b>Type</b>: ${type}\r\n<b>Proposer</b>: <a href="${domain}/#/members/${handle}">${handle}</a>\r\n<b>Title</b>: <a href="${domain}/#/proposals/${id}">${title}</a>\r\n<b>Stage</b>: ${stage}\r\n<b>Result</b>: ${result}`;
+};
 
 // providers
 
@@ -257,20 +251,20 @@ export const provider = (
   status: string,
   sendMessage: (msg: string) => void
 ): void => {
-  const msg = `[${formatTime()}] Storage Provider ${id} (${address}) is ${status}`
-  sendMessage(msg)
-}
+  const msg = `[${formatTime()}] Storage Provider ${id} (${address}) is ${status}`;
+  sendMessage(msg);
+};
 
 export const newOpening = (id: number, sendMessage: (msg: string) => void) => {
-  const msg = `New opening: <b><a href="${domain}/#/working-groups/opportunities/curators/${id}">Storage Provider ${id}</a></b>`
-  sendMessage(msg)
-}
+  const msg = `New opening: <b><a href="${domain}/#/working-groups/opportunities/curators/${id}">Storage Provider ${id}</a></b>`;
+  sendMessage(msg);
+};
 
 export const closeOpening = (
   id: number,
   handle: string,
   sendMessage: (msg: string) => void
 ): void => {
-  const msg = `<a href="${domain}/#/members/${handle}">${handle}</a> was choosen as <b><a href="${domain}/#/working-groups/opportunities/curators/${id}">Storage Provider ${id}</a></b>`
-  sendMessage(msg)
-}
+  const msg = `<a href="${domain}/#/members/${handle}">${handle}</a> was choosen as <b><a href="${domain}/#/working-groups/opportunities/curators/${id}">Storage Provider ${id}</a></b>`;
+  sendMessage(msg);
+};

+ 23 - 6
src/lib/getters.ts

@@ -22,9 +22,26 @@ export const currentChannelId = async (api: Api): Promise<number> => {
   return id.toNumber() - 1;
 };
 
+// members
+
+export const membership = async (
+  api: Api,
+  id: MemberId | number
+): Promise<Membership> => {
+  return await api.query.members.membershipById(id);
+};
+
 export const memberHandle = async (api: Api, id: MemberId): Promise<string> => {
-  const membership: Membership = await api.query.members.membershipById(id);
-  return membership.handle.toJSON();
+  const member: Membership = await membership(api, id);
+  return member.handle.toJSON();
+};
+
+export const memberIdByAccount = async (
+  api: Api,
+  account: AccountId | string
+): Promise<MemberId | number> => {
+  const ids = await api.query.members.memberIdsByRootAccountId(account);
+  return ids.length ? ids[0] : 0;
 };
 
 export const memberHandleByAccount = async (
@@ -35,8 +52,7 @@ export const memberHandleByAccount = async (
     account
   );
   const handle: string = await memberHandle(api, id);
-  if (handle === "joystream_storage_member") return "joystream";
-  return handle;
+  return handle === "joystream_storage_member" ? "joystream" : handle;
 };
 
 // forum
@@ -117,13 +133,14 @@ export const proposalDetail = async (
     : "Pending";
   const exec = proposalStatus ? proposalStatus["Approved"] : null;
 
-  const { description, parameters, proposerId } = proposal;
+  const { description, parameters, proposerId, votingResults } = proposal;
   const author: string = await memberHandle(api, proposerId);
   const title: string = proposal.title.toString();
   const type: string = await getProposalType(api, id);
   const args: string[] = [String(id), title, type, stage, result, author];
   const message: string = formatProposalMessage(args);
   const createdAt: number = proposal.createdAt.toNumber();
+
   return {
     id,
     title,
@@ -135,7 +152,7 @@ export const proposalDetail = async (
     result,
     exec,
     description,
-    votes: proposal.votingResults,
+    votes: votingResults,
     type,
   };
 };

+ 21 - 5
src/types.ts

@@ -14,6 +14,7 @@ export interface Api {
 }
 
 export interface IState {
+  //gethandle: (account: AccountId | string)  => string;
   now: number;
   block: number;
   blocks: Block[];
@@ -21,6 +22,7 @@ export interface IState {
   validators: string[];
   loading: boolean;
   council: Seat[];
+  councils: number[][];
   councilElection?: { stage: any; round: number; termEndsAt: number };
   channels: number[];
   proposals: ProposalDetail[];
@@ -30,13 +32,16 @@ export interface IState {
   domain: string;
   proposalCount: number;
   proposalPosts: any[];
-  handles: { [key: string]: string };
+  handles: Handles;
+  members: Member[];
   tokenomics?: Tokenomics;
   reports: { [key: string]: string };
   [key: string]: any;
 }
 
-export type Seat = any;
+export interface Seat {
+  member: AccountId;
+}
 
 export interface Council {
   round: number;
@@ -64,11 +69,22 @@ export interface ProposalDetail {
   description: any;
   votes: VotingResults;
   type: string;
-  votesByMember?: { vote: string; account: AccountId; handle: string }[];
+  votesByMemberId?: Vote[];
+}
+
+export interface Vote {
+  vote: string;
+  memberId: number;
 }
 
 export type ProposalArray = number[];
 
+export interface ProposalPost {
+  threadId: number;
+  text: string;
+  id: number;
+}
+
 export interface Proposals {
   current: number;
   last: number;
@@ -77,9 +93,9 @@ export interface Proposals {
 }
 
 export interface Member {
-  id: MemberId;
+  account: AccountId | string;
   handle: string;
-  url?: string;
+  id: MemberId | number;
 }
 
 export interface Block {