App.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815
  1. import React from "react";
  2. import "bootstrap/dist/css/bootstrap.min.css";
  3. import "./index.css";
  4. import { Modals, Routes, Loading, Footer, Status } from "./components";
  5. import * as get from "./lib/getters";
  6. import { domain, wsLocation } from "./config";
  7. //import proposalPosts from "./proposalPosts";
  8. import axios from "axios";
  9. import { ProposalDetail } from "./types";
  10. //import socket from "./socket";
  11. import {
  12. Api,
  13. Handles,
  14. IState,
  15. Member,
  16. Category,
  17. Channel,
  18. Post,
  19. Seat,
  20. Thread,
  21. // Status,
  22. } from "./types";
  23. import { types } from "@joystream/types";
  24. import { ApiPromise, WsProvider } from "@polkadot/api";
  25. import { Header } from "@polkadot/types/interfaces";
  26. import { VoteKind } from "@joystream/types/proposals";
  27. interface IProps {}
  28. const version = 5;
  29. const userLink = `${domain}/#/members/joystreamstats`;
  30. const initialState = {
  31. assets: [],
  32. connected: false,
  33. fetching: "",
  34. tasks: 0,
  35. queue: [],
  36. blocks: [],
  37. nominators: [],
  38. validators: [],
  39. mints: {},
  40. channels: [],
  41. posts: [],
  42. councils: [],
  43. categories: [],
  44. threads: [],
  45. proposals: [],
  46. domain,
  47. handles: {},
  48. members: [],
  49. proposalPosts: [],
  50. providers: [],
  51. reports: {},
  52. stakes: {},
  53. stashes: [],
  54. stars: {},
  55. hideFooter: true,
  56. showStatus: false,
  57. status: { era: 0, block: { id: 0, era: 0, timestamp: 0, duration: 6 } },
  58. };
  59. class App extends React.Component<IProps, IState> {
  60. initializeSocket() {
  61. socket.on("disconnect", () => setTimeout(this.initializeSocket, 1000));
  62. socket.on("connect", () => {
  63. if (!socket.id) return console.log("no websocket connection");
  64. console.log("my socketId:", socket.id);
  65. socket.emit("get posts", this.state.posts.length);
  66. });
  67. socket.on("posts", (posts: Post[]) => {
  68. console.log(`received ${posts.length} posts`);
  69. this.setState({ posts });
  70. });
  71. }
  72. async handleApi(api: Api) {
  73. api.rpc.chain.subscribeNewHeads((head: Header) =>
  74. this.handleBlock(api, head)
  75. );
  76. this.fetchMints(api, [2, 3, 4]);
  77. this.updateStatus(api);
  78. let { status } = this.state;
  79. let blockHash = await api.rpc.chain.getBlockHash(1);
  80. status.startTime = (await api.query.timestamp.now.at(blockHash)).toNumber();
  81. this.save("status", status);
  82. }
  83. async fetchMints(api: Api, ids: number[]) {
  84. console.debug(`Fetching mints`);
  85. let mints = {};
  86. ids.map(
  87. async (id) => (mints[id] = (await api.query.minting.mints(id)).toJSON())
  88. );
  89. this.save(`mints`, mints);
  90. }
  91. async fetchAssets() {
  92. const url = "https://hydra.joystream.org/graphql";
  93. const request = {
  94. query: "query {\n dataObjects(where: {}) { joystreamContentId }\n}",
  95. };
  96. console.debug(`Fetching data IDs (from ${url})`);
  97. const { data } = await axios.post(url, request);
  98. let assets = [];
  99. data.data.dataObjects.forEach((p) => assets.push(p.joystreamContentId));
  100. //console.log(`assets`, data);
  101. this.save(`assets`, assets);
  102. }
  103. async fetchStorageProviders() {
  104. const url = "https://hydra.joystream.org/graphql";
  105. const request = {
  106. query:
  107. 'query {\n workers(where: {metadata_contains: "http", isActive_eq: true, type_eq: STORAGE}){\n metadata\n }\n}',
  108. };
  109. console.debug(`Fetching storage providers (from ${url})`);
  110. const { data } = await axios.post(url, request);
  111. const providers = data.data.workers.map((p) => {
  112. return {
  113. url: p.metadata,
  114. };
  115. });
  116. this.save(`providers`, providers);
  117. }
  118. async getStorageProviders(api: Api) {
  119. console.debug(`Fetching storage providers (from chain)`);
  120. let providers = [];
  121. const worker = await api.query.storageWorkingGroup.nextWorkerId();
  122. console.log(`next provider: ${worker}`);
  123. for (let i = 0; i < Number(worker); ++i) {
  124. let storageProvider = (await api.query.storageWorkingGroup.workerById(
  125. i
  126. )) as WorkerOf;
  127. if (storageProvider.is_active) {
  128. const storage = (await api.query.storageWorkingGroup.workerStorage(
  129. i
  130. )) as Bytes;
  131. const url = Buffer.from(storage.toString().substr(2), "hex").toString();
  132. let membership = (await api.query.members.membershipById(
  133. storageProvider.member_id
  134. )) as Membership;
  135. providers[i] = {
  136. owner: membership.handle,
  137. account: membership.root_account,
  138. storage,
  139. url,
  140. };
  141. }
  142. this.save(`providers`, providers);
  143. }
  144. }
  145. async handleBlock(api, header: Header) {
  146. let { blocks, status, queue } = this.state;
  147. const id = header.number.toNumber();
  148. if (blocks.find((b) => b.id === id)) return;
  149. const timestamp = (await api.query.timestamp.now()).toNumber();
  150. const duration = status.block ? timestamp - status.block.timestamp : 6000;
  151. status.block = { id, timestamp, duration };
  152. this.save("status", status);
  153. blocks = this.addOrReplace(blocks, status.block);
  154. this.setState({ blocks });
  155. if (id / 50 === Math.floor(id / 50)) {
  156. this.updateStatus(api, id);
  157. this.fetchTokenomics();
  158. }
  159. if (!queue.length) this.findJob(api);
  160. }
  161. async updateStatus(api: Api, id = 0) {
  162. console.debug(`Updating status for block ${id}`);
  163. let { status } = this.state;
  164. status.era = await this.updateEra(api);
  165. status.council = await this.updateCouncil(api);
  166. const nextMemberId = await await api.query.members.nextMemberId();
  167. status.members = nextMemberId - 1;
  168. status.proposals = await get.proposalCount(api);
  169. status.posts = await get.currentPostId(api);
  170. status.threads = await get.currentThreadId(api);
  171. status.categories = await get.currentCategoryId(api);
  172. //status.channels = await get.currentChannelId(api);
  173. status.proposalPosts = await api.query.proposalsDiscussion.postCount();
  174. status.version = version;
  175. await this.save("status", status);
  176. this.findJob(api);
  177. }
  178. async updateEra(api: Api) {
  179. const era = Number(await api.query.staking.currentEra());
  180. this.fetchEraRewardPoints(api, era);
  181. const { status } = this.state;
  182. if (era > status.era) {
  183. console.debug(`Updating validators`);
  184. this.fetchLastReward(api, status.era);
  185. const validators = await this.fetchValidators(api);
  186. this.enqueue("stakes", () => this.fetchStakes(api, era, validators));
  187. } else if (!status.lastReward) this.fetchLastReward(api);
  188. return era;
  189. }
  190. // queue management
  191. enqueue(key: string, action: () => void) {
  192. this.setState({ queue: this.state.queue.concat({ key, action }) });
  193. this.processTask();
  194. }
  195. findJob(api: Api) {
  196. const { status, proposals, posts, members } = this.state;
  197. if (!status.lastReward) this.fetchLastReward(api);
  198. if (
  199. status.council &&
  200. status.council.stageEndsAt > 0 &&
  201. status.council.stageEndsAt < status.block.id
  202. )
  203. this.updateCouncil(api);
  204. if (
  205. status.proposals > proposals.filter((p) => p && p.votesByAccount).length
  206. )
  207. this.fetchProposal(api, status.proposals);
  208. if (status.posts > posts.length) this.fetchPost(api, status.posts);
  209. if (status.members > members.length) this.fetchMember(api, status.members);
  210. }
  211. async processTask() {
  212. // check status
  213. let { tasks } = this.state;
  214. if (tasks > 1) return;
  215. if (tasks < 1) setTimeout(() => this.processTask(), 0);
  216. // pull task
  217. let { queue } = this.state;
  218. const task = queue.shift();
  219. if (!task) {
  220. if (!tasks) this.setState({ fetching: "" });
  221. return;
  222. }
  223. this.setState({ fetching: task.key, queue, tasks: tasks + 1 });
  224. await task.action();
  225. this.setState({ tasks: this.state.tasks - 1 });
  226. setTimeout(() => this.processTask(), 100);
  227. }
  228. addOrReplace(array, item) {
  229. return array.filter((i) => i.id !== item.id).concat(item);
  230. }
  231. async fetchLastReward(api: Api) {
  232. const era: number = await this.updateEra(api);
  233. const lastReward = Number(
  234. await api.query.staking.erasValidatorReward(era - 2)
  235. );
  236. console.debug(`reward era ${era}: ${lastReward} tJOY`);
  237. let { status } = this.state;
  238. status.lastReward = lastReward;
  239. this.save("status", status);
  240. }
  241. async fetchTokenomics() {
  242. console.debug(`Updating tokenomics`);
  243. const { data } = await axios.get("https://status.joystream.org/status");
  244. if (!data || data.error) return;
  245. this.save("tokenomics", data);
  246. }
  247. async fetchChannel(api: Api, id: number) {
  248. if (this.state.channels.find((c) => c.id === id)) return;
  249. const data = await api.query.contentWorkingGroup.channelById(id);
  250. const handle = String(data.handle);
  251. const title = String(data.title);
  252. const description = String(data.description);
  253. const avatar = String(data.avatar);
  254. const banner = String(data.banner);
  255. const content = String(data.content);
  256. const ownerId = Number(data.owner);
  257. const accountId = String(data.role_account);
  258. const publicationStatus =
  259. data.publication_status === "Public" ? true : false;
  260. const curation = String(data.curation_status);
  261. const createdAt = data.created;
  262. const principal = Number(data.principal_id);
  263. this.fetchMemberByAccount(api, accountId);
  264. const channel: Channel = {
  265. id,
  266. handle,
  267. title,
  268. description,
  269. avatar,
  270. banner,
  271. content,
  272. ownerId,
  273. accountId,
  274. publicationStatus,
  275. curation,
  276. createdAt,
  277. principal,
  278. };
  279. const channels = this.addOrReplace(this.state.channels, channel);
  280. this.save("channels", channels);
  281. if (id > 1)
  282. this.enqueue(`channel ${id - 1}`, () => this.fetchChannel(api, id - 1));
  283. }
  284. async fetchCategory(api: Api, id: number) {
  285. if (!id) return;
  286. const exists = this.state.categories.find((c) => c.id === id);
  287. if (exists) return this.fetchCategory(api, id - 1);
  288. const data = await api.query.forum.categoryById(id);
  289. const threadId = Number(data.thread_id);
  290. const title = String(data.title);
  291. const description = String(data.description);
  292. const createdAt = Number(data.created_at.block);
  293. const deleted = data.deleted;
  294. const archived = data.archived;
  295. const subcategories = Number(data.num_direct_subcategories);
  296. const moderatedThreads = Number(data.num_direct_moderated_threads);
  297. const unmoderatedThreads = Number(data.num_direct_unmoderated_threads);
  298. const position = Number(data.position_in_parent_category);
  299. const moderatorId = String(data.moderator_id);
  300. const category: Category = {
  301. id,
  302. threadId,
  303. title,
  304. description,
  305. createdAt,
  306. deleted,
  307. archived,
  308. subcategories,
  309. moderatedThreads,
  310. unmoderatedThreads,
  311. position,
  312. moderatorId,
  313. };
  314. this.save("categories", this.addOrReplace(this.state.categories, category));
  315. this.enqueue(`category ${id - 1}`, () => this.fetchCategory(api, id - 1));
  316. }
  317. async fetchPost(api: Api, id: number) {
  318. if (!id) return;
  319. const exists = this.state.posts.find((p) => p.id === id);
  320. if (exists) return this.fetchPost(api, id - 1);
  321. const data = await api.query.forum.postById(id);
  322. const threadId = Number(data.thread_id);
  323. this.fetchThread(api, threadId);
  324. const text = data.current_text.slice(0, 1000);
  325. //const moderation = data.moderation;
  326. //const history = data.text_change_history;
  327. const createdAt = data.created_at;
  328. const authorId = String(data.author_id);
  329. this.fetchMemberByAccount(api, authorId);
  330. const post: Post = { id, threadId, text, authorId, createdAt };
  331. const posts = this.addOrReplace(this.state.posts, post);
  332. this.save("posts", posts);
  333. this.enqueue(`post ${id - 1}`, () => this.fetchPost(api, id - 1));
  334. }
  335. async fetchThread(api: Api, id: number) {
  336. if (!id) return;
  337. const exists = this.state.threads.find((t) => t.id === id);
  338. if (exists) return this.fetchThread(api, id - 1);
  339. const data = await api.query.forum.threadById(id);
  340. const title = String(data.title);
  341. const categoryId = Number(data.category_id);
  342. const nrInCategory = Number(data.nr_in_category);
  343. const moderation = data.moderation;
  344. const createdAt = String(data.created_at.block);
  345. const authorId = String(data.author_id);
  346. const thread: Thread = {
  347. id,
  348. title,
  349. categoryId,
  350. nrInCategory,
  351. moderation,
  352. createdAt,
  353. authorId,
  354. };
  355. const threads = this.addOrReplace(this.state.threads, thread);
  356. this.save("threads", threads);
  357. this.enqueue(`thread ${id - 1}`, () => this.fetchThread(api, id - 1));
  358. }
  359. // council
  360. async fetchCouncils(api: Api, currentRound: number) {
  361. for (let round = currentRound; round > 0; round--)
  362. this.enqueue(`council ${round}`, () => this.fetchCouncil(api, round));
  363. }
  364. async fetchCouncil(api: Api, round: number) {
  365. let { councils } = this.state;
  366. councils[round] = (await api.query.council.activeCouncil()).toJSON();
  367. councils[round].map((c) => this.fetchMemberByAccount(api, c.member));
  368. this.save("councils", councils);
  369. }
  370. async updateCouncil(api: Api, block: number) {
  371. console.debug(`Updating council`);
  372. const round = Number((await api.query.councilElection.round()).toJSON());
  373. const termEndsAt = Number((await api.query.council.termEndsAt()).toJSON());
  374. const stage = (await api.query.councilElection.stage()).toJSON();
  375. let stageEndsAt = 0;
  376. if (stage) {
  377. const key = Object.keys(stage)[0];
  378. stageEndsAt = stage[key];
  379. }
  380. const stages = [
  381. "announcingPeriod",
  382. "votingPeriod",
  383. "revealingPeriod",
  384. "newTermDuration",
  385. ];
  386. let durations = await Promise.all(
  387. stages.map((s) => api.query.councilElection[s]())
  388. ).then((stages) => stages.map((stage) => stage.toJSON()));
  389. durations.push(durations.reduce((a, b) => a + b, 0));
  390. this.fetchCouncils(api, round);
  391. return { round, stageEndsAt, termEndsAt, stage, durations };
  392. }
  393. // proposals
  394. async fetchProposal(api: Api, id: number) {
  395. if (id > 1)
  396. this.enqueue(`proposal ${id - 1}`, () => this.fetchProposal(api, id - 1));
  397. // find existing
  398. const { proposals } = this.state;
  399. const exists = this.state.proposals.find((p) => p && p.id === id);
  400. // check if complete
  401. if (
  402. exists &&
  403. exists.detail &&
  404. exists.stage === "Finalized" &&
  405. exists.executed
  406. )
  407. if (exists.votesByAccount && exists.votesByAccount.length) return;
  408. else
  409. return this.enqueue(`votes for proposal ${id}`, () =>
  410. this.fetchProposalVotes(api, exists)
  411. );
  412. // fetch
  413. const proposal = await get.proposalDetail(api, id);
  414. if (proposal.type !== "text")
  415. proposal.detail = (
  416. await api.query.proposalsCodex.proposalDetailsByProposalId(id)
  417. ).toJSON();
  418. proposals[id] = proposal;
  419. this.save("proposals", proposals);
  420. this.enqueue(`votes for proposal ${id}`, () =>
  421. this.fetchProposalVotes(api, proposal)
  422. );
  423. }
  424. async fetchProposalVotes(api: Api, proposal: ProposalDetail) {
  425. const { votesByAccount } = proposal;
  426. if (votesByAccount && votesByAccount.length) return;
  427. const { councils, proposals } = this.state;
  428. let members: Member[] = [];
  429. councils
  430. .filter((c) => c)
  431. .map((seats) =>
  432. seats.forEach(async (seat: Seat) => {
  433. if (members.find((member) => member.account === seat.member)) return;
  434. const member = this.state.members.find(
  435. (m) => m.account === seat.member
  436. );
  437. if (member) members.push(member);
  438. })
  439. );
  440. const { id } = proposal;
  441. proposal.votesByAccount = await Promise.all(
  442. members.map(async (member) => {
  443. const vote = await this.fetchVoteByProposalByVoter(api, id, member.id);
  444. return { vote, handle: member.handle };
  445. })
  446. );
  447. proposals[id] = proposal;
  448. this.save("proposals", proposals);
  449. }
  450. async fetchVoteByProposalByVoter(
  451. api: Api,
  452. proposalId: number,
  453. voterId: number
  454. ): Promise<string> {
  455. const vote: VoteKind = await api.query.proposalsEngine.voteExistsByProposalByVoter(
  456. proposalId,
  457. voterId
  458. );
  459. const hasVoted: number = (
  460. await api.query.proposalsEngine.voteExistsByProposalByVoter.size(
  461. proposalId,
  462. voterId
  463. )
  464. ).toNumber();
  465. return hasVoted ? String(vote) : "";
  466. }
  467. // validators
  468. async fetchValidators(api: Api) {
  469. const validatorEntries = await api.query.session.validators();
  470. const validators = validatorEntries.map((v: any) => String(v));
  471. this.save("validators", validators);
  472. const stashes = await api.derive.staking.stashes();
  473. this.save(
  474. "stashes",
  475. stashes.map((s: any) => String(s))
  476. );
  477. this.enqueue("nominators", () => this.fetchNominators(api));
  478. return validators;
  479. }
  480. async fetchNominators(api: Api) {
  481. const nominatorEntries = await api.query.staking.nominators.entries();
  482. const nominators = nominatorEntries.map((n: any) => String(n[0].toHuman()));
  483. this.save("nominators", nominators);
  484. }
  485. async fetchStakes(api: Api, era: number, validators: string[]) {
  486. // TODO staking.bondedEras: Vec<(EraIndex,SessionIndex)>
  487. const { stashes } = this.state;
  488. if (!stashes) return;
  489. stashes.forEach(async (validator: string) => {
  490. try {
  491. const prefs = await api.query.staking.erasValidatorPrefs(
  492. era,
  493. validator
  494. );
  495. const commission = Number(prefs.commission) / 10000000;
  496. const data = await api.query.staking.erasStakers(era, validator);
  497. let { total, own, others } = data.toJSON();
  498. let { stakes = {} } = this.state;
  499. stakes[validator] = { total, own, others, commission };
  500. this.save("stakes", stakes);
  501. } catch (e) {
  502. console.warn(
  503. `Failed to fetch stakes for ${validator} in era ${era}`,
  504. e
  505. );
  506. }
  507. });
  508. }
  509. async fetchEraRewardPoints(api: Api, era: number) {
  510. const data = await api.query.staking.erasRewardPoints(era);
  511. this.setState({ rewardPoints: data.toJSON() });
  512. }
  513. // data objects
  514. fetchDataObjects() {
  515. // TODO dataDirectory.knownContentIds: Vec<ContentId>
  516. }
  517. // accounts
  518. async fetchMemberByAccount(api: Api, account: string): Promise<Member> {
  519. const empty = { id: -1, handle: `?`, account, about: ``, registeredAt: 0 };
  520. if (!account) return empty;
  521. const exists = this.state.members.find(
  522. (m: Member) => String(m.account) === String(account)
  523. );
  524. if (exists) return exists;
  525. const id = Number(await get.memberIdByAccount(api, account));
  526. if (!id) return empty;
  527. return await this.fetchMember(api, id);
  528. }
  529. async fetchMember(api: Api, id: number): Promise<Member> {
  530. const exists = this.state.members.find((m: Member) => m.id === id);
  531. if (exists) {
  532. setTimeout(() => this.fetchMember(api, id--), 0);
  533. return exists;
  534. }
  535. const membership = await get.membership(api, id);
  536. const handle = String(membership.handle);
  537. const account = String(membership.root_account);
  538. const about = String(membership.about);
  539. const registeredAt = Number(membership.registered_at_block);
  540. const member: Member = { id, handle, account, registeredAt, about };
  541. const members = this.addOrReplace(this.state.members, member);
  542. this.save(`members`, members);
  543. this.updateHandles(members);
  544. if (id > 1)
  545. this.enqueue(`member ${id - 1}`, () => this.fetchMember(api, id - 1));
  546. return member;
  547. }
  548. updateHandles(members: Member[]) {
  549. if (!members.length) return;
  550. let handles: Handles = {};
  551. members.forEach((m) => (handles[String(m.account)] = m.handle));
  552. this.save(`handles`, handles);
  553. }
  554. // Validators
  555. toggleStar(account: string) {
  556. let { stars } = this.state;
  557. stars[account] = !stars[account];
  558. this.save("stars", stars);
  559. }
  560. // Reports
  561. async fetchReports() {
  562. const domain = `https://raw.githubusercontent.com/Joystream/community-repo/master/council-reports`;
  563. const apiBase = `https://api.github.com/repos/joystream/community-repo/contents/council-reports`;
  564. const urls: { [key: string]: string } = {
  565. alexandria: `${apiBase}/alexandria-testnet`,
  566. archive: `${apiBase}/archived-reports`,
  567. template: `${domain}/templates/council_report_template_v1.md`,
  568. };
  569. ["alexandria", "archive"].map((folder) =>
  570. this.fetchGithubDir(urls[folder])
  571. );
  572. // template
  573. this.fetchGithubFile(urls.template);
  574. }
  575. async saveReport(name: string, content: Promise<string>) {
  576. const { reports } = this.state;
  577. reports[name] = await content;
  578. this.save("reports", reports);
  579. }
  580. async fetchGithubFile(url: string): Promise<string> {
  581. const { data } = await axios.get(url);
  582. return data;
  583. }
  584. async fetchGithubDir(url: string) {
  585. const { data } = await axios.get(url);
  586. data.forEach(
  587. async (o: {
  588. name: string;
  589. type: string;
  590. url: string;
  591. download_url: string;
  592. }) => {
  593. const match = o.name.match(/^(.+)\.md$/);
  594. const name = match ? match[1] : o.name;
  595. if (o.type === "file")
  596. this.saveReport(name, this.fetchGithubFile(o.download_url));
  597. else this.fetchGithubDir(o.url);
  598. }
  599. );
  600. }
  601. loadMembers() {
  602. const members = this.load("members");
  603. if (!members) return;
  604. this.setState({ members });
  605. this.updateHandles(members);
  606. }
  607. loadPosts() {
  608. const posts: Post[] = this.load("posts");
  609. posts.forEach(({ id, text }) => {
  610. if (text && text.length > 500)
  611. console.debug(`post ${id}: ${(text.length / 1000).toFixed(1)} KB`);
  612. });
  613. if (posts) this.setState({ posts });
  614. }
  615. clearData() {
  616. console.log(`Resetting db to version ${version}`);
  617. this.save("status", { version });
  618. this.save("proposals", []);
  619. this.save("posts", []);
  620. }
  621. async loadData() {
  622. const status = this.load("status");
  623. if (status) {
  624. console.debug(`Status`, status, `Version`, version);
  625. if (status.version !== version) return this.clearData();
  626. this.setState({ status });
  627. }
  628. console.debug(`Loading data`);
  629. this.loadMembers();
  630. "assets providers councils categories channels proposals posts threads handles mints tokenomics transactions reports validators nominators stakes stars"
  631. .split(" ")
  632. .map((key) => this.load(key));
  633. }
  634. load(key: string) {
  635. //console.debug(`loading ${key}`);
  636. try {
  637. const data = localStorage.getItem(key);
  638. if (!data) return;
  639. const size = data.length;
  640. if (size > 10240)
  641. console.debug(` -${key}: ${(size / 1024).toFixed(1)} KB`);
  642. this.setState({ [key]: JSON.parse(data) });
  643. return JSON.parse(data);
  644. } catch (e) {
  645. console.warn(`Failed to load ${key}`, e);
  646. }
  647. }
  648. save(key: string, data: any) {
  649. this.setState({ [key]: data });
  650. try {
  651. localStorage.setItem(key, JSON.stringify(data));
  652. } catch (e) {
  653. console.warn(`Failed to save ${key} (${data.length}KB)`, e);
  654. //if (key !== `posts`) {
  655. // localStorage.setItem(`posts`, `[]`);
  656. // localStorage.setItem(`channels`, `[]`);
  657. //}
  658. }
  659. }
  660. toggleShowStatus() {
  661. this.setState({ showStatus: !this.state.showStatus });
  662. }
  663. toggleFooter() {
  664. this.setState({ hideFooter: !this.state.hideFooter });
  665. }
  666. render() {
  667. if (this.state.loading) return <Loading />;
  668. const { connected, fetching, hideFooter } = this.state;
  669. return (
  670. <>
  671. <Routes
  672. toggleFooter={this.toggleFooter}
  673. toggleStar={this.toggleStar}
  674. {...this.state}
  675. />
  676. <Modals toggleShowStatus={this.toggleShowStatus} {...this.state} />
  677. <Footer
  678. show={!hideFooter}
  679. toggleHide={this.toggleFooter}
  680. link={userLink}
  681. />
  682. <Status
  683. toggleShowStatus={this.toggleShowStatus}
  684. connected={connected}
  685. fetching={fetching}
  686. />
  687. </>
  688. );
  689. }
  690. connectEndpoint() {
  691. console.debug(`Connecting to ${wsLocation}`);
  692. const provider = new WsProvider(wsLocation);
  693. ApiPromise.create({ provider, types }).then((api) =>
  694. api.isReady.then(() => {
  695. console.log(`Connected to ${wsLocation}`);
  696. this.setState({ connected: true });
  697. this.handleApi(api);
  698. })
  699. );
  700. }
  701. componentDidMount() {
  702. this.loadData();
  703. this.connectEndpoint();
  704. this.fetchStorageProviders();
  705. this.fetchAssets();
  706. this.fetchTransactions();
  707. setTimeout(() => this.fetchTokenomics(), 30000);
  708. //this.initializeSocket();
  709. }
  710. componentWillUnmount() {}
  711. constructor(props: IProps) {
  712. super(props);
  713. this.state = initialState;
  714. this.fetchTokenomics = this.fetchTokenomics.bind(this);
  715. this.load = this.load.bind(this);
  716. this.toggleStar = this.toggleStar.bind(this);
  717. this.toggleFooter = this.toggleFooter.bind(this);
  718. this.toggleShowStatus = this.toggleShowStatus.bind(this);
  719. }
  720. }
  721. export default App;