Joystream Stats 4 tahun lalu
induk
melakukan
383ce3797c

+ 188 - 1
src/App.tsx

@@ -7,9 +7,20 @@ import * as get from "./lib/getters";
 import { domain, wsLocation } from "./config";
 import proposalPosts from "./proposalPosts";
 import axios from "axios";
+//import moment from "moment";
 
 // types
-import { Api, Block, Handles, IState, Member } from "./types";
+import {
+  Api,
+  Block,
+  Handles,
+  IState,
+  Member,
+  Category,
+  Channel,
+  Post,
+  Thread,
+} from "./types";
 import { types } from "@joystream/types";
 import { Seat } from "@joystream/types/augment/all/types";
 import { ApiPromise, WsProvider } from "@polkadot/api";
@@ -62,6 +73,18 @@ class App extends React.Component<IProps, IState> {
     this.setState({ councilElection });
     let stageEndsAt: number = termEndsAt;
 
+    let lastCategory = await get.currentCategoryId(api);
+    this.fetchCategories(api, lastCategory);
+
+    let lastChannel = await get.currentChannelId(api);
+    this.fetchChannels(api, lastChannel);
+
+    let lastPost = await get.currentPostId(api);
+    this.fetchPosts(api, lastPost);
+
+    let lastThread = await get.currentThreadId(api);
+    this.fetchThreads(api, lastThread);
+
     api.rpc.chain.subscribeNewHeads(
       async (header: Header): Promise<void> => {
         // current block
@@ -79,6 +102,22 @@ class App extends React.Component<IProps, IState> {
           this.setState({ proposalCount });
         }
 
+        const currentChannel = await get.currentChannelId(api);
+        if (currentChannel > lastChannel)
+          lastChannel = await this.fetchChannels(api, currentChannel);
+
+        const currentCategory = await get.currentCategoryId(api);
+        if (currentCategory > lastCategory)
+          lastCategory = await this.fetchCategories(api, currentCategory);
+
+        const currentPost = await get.currentPostId(api);
+        if (currentPost > lastPost)
+          lastPost = await this.fetchPosts(api, currentPost);
+
+        const currentThread = await get.currentThreadId(api);
+        if (currentThread > lastThread)
+          lastThread = await this.fetchThreads(api, currentThread);
+
         const postCount = await api.query.proposalsDiscussion.postCount();
         this.setState({ proposalComments: Number(postCount) });
 
@@ -112,6 +151,139 @@ class App extends React.Component<IProps, IState> {
     this.save("tokenomics", data);
   }
 
+  async fetchChannels(api: Api, lastId: number) {
+    for (let id = lastId; id > 0; id--) {
+      if (this.state.channels.find((c) => c.id === id)) continue;
+      console.log(`fetching channel ${id}`);
+      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);
+
+      const channel: Channel = {
+        id,
+        handle,
+        title,
+        description,
+        avatar,
+        banner,
+        content,
+        ownerId,
+        accountId,
+        publicationStatus,
+        curation,
+        createdAt,
+        principal,
+      };
+
+      //console.debug(data, channel);
+      const channels = this.state.channels.concat(channel);
+      this.save("channels", channels);
+    }
+    return lastId;
+  }
+  async fetchCategories(api: Api, lastId: number) {
+    for (let id = lastId; id > 0; id--) {
+      if (this.state.categories.find((c) => c.id === id)) continue;
+      console.debug(`fetching category ${id}`);
+      const data = await api.query.forum.categoryById(id);
+
+      const threadId = Number(data.thread_id);
+      const title = String(data.title);
+      const description = String(data.description);
+      const createdAt = Number(data.created_at.block);
+      const deleted = data.deleted;
+      const archived = data.archived;
+      const subcategories = Number(data.num_direct_subcategories);
+      const moderatedThreads = Number(data.num_direct_moderated_threads);
+      const unmoderatedThreads = Number(data.num_direct_unmoderated_threads);
+      const position = Number(data.position_in_parent_category);
+      const moderatorId = String(data.moderator_id);
+
+      const category: Category = {
+        id,
+        threadId,
+        title,
+        description,
+        createdAt,
+        deleted,
+        archived,
+        subcategories,
+        moderatedThreads,
+        unmoderatedThreads,
+        position,
+        moderatorId,
+      };
+
+      //console.debug(data, category);
+      const categories = this.state.categories.concat(category);
+      this.save("categories", categories);
+    }
+    return lastId;
+  }
+  async fetchPosts(api: Api, lastId: number) {
+    for (let id = lastId; id > 0; id--) {
+      if (this.state.posts.find((p) => p.id === id)) continue;
+      console.debug(`fetching post ${id}`);
+      const data = await api.query.forum.postById(id);
+
+      const threadId = Number(data.thread_id);
+      const text = data.current_text;
+      //const moderation = data.moderation;
+      //const history = data.text_change_history;
+      //const createdAt = moment(data.created_at);
+      const createdAt = data.created_at;
+      const authorId = String(data.author_id);
+
+      const post: Post = { id, threadId, text, authorId, createdAt };
+
+      //console.debug(data, post);
+      const posts = this.state.posts.concat(post);
+      this.save("posts", posts);
+    }
+    return lastId;
+  }
+  async fetchThreads(api: Api, lastId: number) {
+    for (let id = lastId; id > 0; id--) {
+      if (this.state.threads.find((t) => t.id === id)) continue;
+      console.debug(`fetching thread ${id}`);
+      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,
+      };
+
+      //console.debug(data, thread);
+      const threads = this.state.threads.concat(thread);
+      this.save("threads", threads);
+    }
+    return lastId;
+  }
+
   async fetchCouncils(api: Api, currentRound: number) {
     if (this.state.councils.length)
       return this.state.councils.map((council) =>
@@ -328,6 +500,18 @@ class App extends React.Component<IProps, IState> {
     const proposals = this.load("proposals");
     if (proposals) this.setState({ proposals });
   }
+  loadChannels() {
+    const channels = this.load("channels");
+    if (channels) this.setState({ channels });
+  }
+  loadCategories() {
+    const categories = this.load("categories");
+    if (categories) this.setState({ categories });
+  }
+  loadPosts() {
+    const posts = this.load("posts");
+    if (posts) this.setState({ posts });
+  }
   loadThreads() {
     const threads = this.load("threads");
     if (threads) this.setState({ threads });
@@ -368,7 +552,10 @@ class App extends React.Component<IProps, IState> {
     console.log(`Loading data`);
     await this.loadMembers();
     await this.loadCouncils();
+    await this.loadCategories();
+    await this.loadChannels();
     await this.loadProposals();
+    await this.loadPosts();
     await this.loadThreads();
     await this.loadValidators();
     await this.loadNominators();

+ 23 - 0
src/components/Forum/Categories.tsx

@@ -0,0 +1,23 @@
+import React from "react";
+//import { OverlayTrigger, Tooltip } from "react-bootstrap";
+import { Category, Thread } from "../../types";
+
+const Categories = (props: {
+  categories: Category[];
+  threads: Thread[];
+  selectCategory: (id: number) => void;
+}) => {
+  const { selectCategory, categories, threads } = props;
+  return (
+    <div>
+      <h2>Categories</h2>
+      {categories.map((c) => (
+        <div key={c.id} onClick={() => selectCategory(c.id)}>
+          {c.title} ({threads.filter((t) => t.categoryId === c.id).length})
+        </div>
+      ))}
+    </div>
+  );
+};
+
+export default Categories;

+ 48 - 0
src/components/Forum/Category.tsx

@@ -0,0 +1,48 @@
+import React from "react";
+//import { OverlayTrigger, Tooltip } from "react-bootstrap";
+import { Category, Thread, Post } from "../../types";
+import Threads from "./Threads";
+import Posts from "./Posts";
+import { ChevronLeft } from "react-feather";
+
+const CategoryThreads = (props: {
+  category?: Category;
+  thread?: number;
+  threads: Thread[];
+  posts: Post[];
+  selectCategory: (id: number) => void;
+  selectThread: (id: number) => void;
+  selectPost: (id: number) => void;
+}) => {
+  const {
+    selectCategory,
+    selectThread,
+    selectPost,
+    category,
+    threads,
+    thread,
+    posts,
+  } = props;
+  if (!category) return <div />;
+
+  return (
+    <div>
+      <h2>
+        <ChevronLeft onClick={() => selectCategory(0)} />
+        <div onClick={() => selectThread(0)}>{category.title}</div>
+      </h2>
+      {thread ? (
+        <Posts
+          selectPost={selectPost}
+          selectThread={selectThread}
+          thread={threads.find((t) => t.id === thread)}
+          posts={posts.filter((p) => p.threadId === thread)}
+        />
+      ) : (
+        <Threads selectThread={selectThread} threads={threads} posts={posts} />
+      )}
+    </div>
+  );
+};
+
+export default CategoryThreads;

+ 37 - 0
src/components/Forum/Posts.tsx

@@ -0,0 +1,37 @@
+import React from "react";
+//import { OverlayTrigger, Tooltip } from "react-bootstrap";
+import { Thread, Post } from "../../types";
+import { ChevronLeft } from "react-feather";
+
+const ThreadPosts = (props: {
+  thread?: Thread;
+  posts: Post[];
+  selectPost: (id: number) => void;
+  selectThread: (id: number) => void;
+}) => {
+  const { selectPost, selectThread, thread } = props;
+
+  // unique posts
+  let posts: Post[] = [];
+  props.posts.forEach((p) => {
+    if (!posts.find((post) => post.id === p.id)) posts.push(p);
+  });
+
+  if (!thread) return <div />;
+  return (
+    <div>
+      <h4>
+        <ChevronLeft onClick={() => selectThread(0)} />
+
+        {thread.title}
+      </h4>
+      {posts.map((p) => (
+        <div key={p.id} onClick={() => selectPost(p.id)}>
+          {p.id}: {p.text}
+        </div>
+      ))}
+    </div>
+  );
+};
+
+export default ThreadPosts;

+ 22 - 0
src/components/Forum/Threads.tsx

@@ -0,0 +1,22 @@
+import React from "react";
+//import { OverlayTrigger, Tooltip } from "react-bootstrap";
+import { Thread, Post } from "../../types";
+
+const Threads = (props: {
+  threads: Thread[];
+  posts: Post[];
+  selectThread: (id: number) => void;
+}) => {
+  const { selectThread, threads, posts } = props;
+  return (
+    <div>
+      {threads.map((t) => (
+        <div key={t.id} onClick={() => selectThread(t.id)}>
+          {t.title} ({posts.filter((p) => p.threadId === t.id).length})
+        </div>
+      ))}
+    </div>
+  );
+};
+
+export default Threads;

+ 67 - 0
src/components/Forum/index.tsx

@@ -0,0 +1,67 @@
+import React from "react";
+//import { OverlayTrigger, Tooltip } from "react-bootstrap";
+import { Category, Post, Thread } from "../../types";
+import Categories from "./Categories";
+import CategoryThreads from "./Category";
+
+interface IProps {
+  block: number;
+  posts: Post[];
+  categories: Category[];
+  threads: Thread[];
+}
+interface IState {
+  category: number;
+  thread: number;
+  post: number;
+}
+
+class Forum extends React.Component<IProps, IState> {
+  constructor(props: IProps) {
+    super(props);
+    this.state = { category: 0, thread: 0, post: 0 };
+    this.selectCategory = this.selectCategory.bind(this);
+    this.selectThread = this.selectThread.bind(this);
+    this.selectPost = this.selectPost.bind(this);
+  }
+
+  selectCategory(category: number) {
+    this.setState({ category });
+  }
+  selectThread(thread: number) {
+    this.setState({ thread });
+  }
+  selectPost(post: number) {
+    this.setState({ post });
+  }
+
+  render() {
+    const { categories, posts, threads } = this.props;
+    const { category, thread } = this.state;
+
+    return (
+      <div className="h-100 overflow-hidden bg-light">
+        <h1>Forum</h1>
+        {category ? (
+          <CategoryThreads
+            selectCategory={this.selectCategory}
+            selectThread={this.selectThread}
+            selectPost={this.selectPost}
+            category={categories.find((c) => c.id === category)}
+            threads={threads.filter((t) => t.categoryId === category)}
+            thread={thread}
+            posts={posts}
+          />
+        ) : (
+          <Categories
+            selectCategory={this.selectCategory}
+            categories={categories}
+            threads={threads}
+          />
+        )}
+      </div>
+    );
+  }
+}
+
+export default Forum;

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

@@ -1,5 +1,5 @@
 import { Switch, Route } from "react-router-dom";
-import { Councils, Dashboard, Mint, Proposals, Proposal, Tokenomics } from "..";
+import { Councils, Dashboard, Forum, Mint, Proposals, Proposal, Tokenomics } from "..";
 import { IState } from "../../types";
 
 const Routes = (props: IState) => {
@@ -16,6 +16,7 @@ const Routes = (props: IState) => {
       />
       <Route path="/proposals" render={() => <Proposals {...props} />} />
       <Route path="/councils" render={() => <Councils {...props} />} />
+      <Route path="/forum" render={() => <Forum {...props} />} />
       <Route path="/mint" render={() => <Mint {...props} />} />
       <Route path="/" render={() => <Dashboard {...props} />} />
     </Switch>

+ 1 - 0
src/components/index.ts

@@ -3,6 +3,7 @@ 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 Forum } from "./Forum";
 export { default as Mint } from "./Mint";
 export { default as Proposals } from "./Proposals";
 export { default as ProposalLink } from "./Proposals/ProposalLink";

+ 52 - 5
src/types.ts

@@ -24,10 +24,10 @@ export interface IState {
   loading: boolean;
   councils: number[][];
   councilElection?: { stage: any; round: number; termEndsAt: number };
-  channels: number[];
+  channels: Channel[];
+  categories: Category[];
   proposals: ProposalDetail[];
-  posts: number[];
-  categories: number[];
+  posts: Post[];
   threads: Thread[];
   domain: string;
   proposalCount: number;
@@ -93,6 +93,55 @@ export interface Proposals {
   executing: ProposalArray;
 }
 
+export interface Channel {
+  id: number;
+  handle: string;
+  title: string;
+  description: string;
+  avatar: string;
+  banner: string;
+  content: string;
+  ownerId: number;
+  accountId: string;
+  publicationStatus: boolean;
+  curation: string;
+  createdAt: string;
+  principal: number;
+}
+
+export interface Category {
+  id: number;
+  threadId: number;
+  title: string;
+  description: string;
+  createdAt: number;
+  deleted: boolean;
+  archived: boolean;
+  subcategories: number;
+  unmoderatedThreads: number;
+  moderatedThreads: number;
+  position: number;
+  moderatorId: string;
+}
+
+export interface Post {
+  id: number;
+  text: string;
+  threadId: number;
+  authorId: string;
+  createdAt: string;
+}
+
+export interface Thread {
+  id: number;
+  title: string;
+  categoryId: number;
+  nrInCategory: number;
+  moderation: string;
+  createdAt: string;
+  authorId: string;
+}
+
 export interface Member {
   account: AccountId | string;
   handle: string;
@@ -121,8 +170,6 @@ export interface Handles {
   [key: string]: string;
 }
 
-export interface Thread {}
-
 export interface Tokenomics {
   price: string;
   totalIssuance: string;