|
@@ -0,0 +1,166 @@
|
|
|
+import React, { useState, useEffect } from 'react';
|
|
|
+import { Link } from 'react-router-dom';
|
|
|
+import styled from 'styled-components';
|
|
|
+import { orderBy } from 'lodash';
|
|
|
+import BN from 'bn.js';
|
|
|
+
|
|
|
+import Section from '@polkadot/joy-utils/Section';
|
|
|
+import { withMulti, withApi } from '@polkadot/react-api';
|
|
|
+import { PostId, Post, Thread } from '@joystream/types/forum';
|
|
|
+import { bnToStr } from '@polkadot/joy-utils/';
|
|
|
+import { ApiProps } from '@polkadot/react-api/types';
|
|
|
+import { MemberPreview } from '@polkadot/joy-members/MemberPreview';
|
|
|
+
|
|
|
+import { CategoryCrumbs, RecentActivityPostsCount, ReplyIdxQueryParam, TimeAgoDate } from './utils';
|
|
|
+import { withForumCalls } from './calls';
|
|
|
+import { CategoryList } from './CategoryList';
|
|
|
+
|
|
|
+const ForumRoot: React.FC = () => {
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ <CategoryCrumbs root />
|
|
|
+ <Section title="Top categories">
|
|
|
+ <CategoryList />
|
|
|
+ </Section>
|
|
|
+ <RecentActivity />
|
|
|
+ </>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const RecentActivityEntry = styled.div`
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 10px 0;
|
|
|
+
|
|
|
+ &:not(:last-child) {
|
|
|
+ border-bottom: 1px solid #ddd;
|
|
|
+ }
|
|
|
+`;
|
|
|
+
|
|
|
+const StyledMemberPreview = styled(MemberPreview)`
|
|
|
+ && {
|
|
|
+ margin-right: .3rem;
|
|
|
+ }
|
|
|
+`;
|
|
|
+
|
|
|
+const StyledPostLink = styled(Link)`
|
|
|
+ margin: 0 .3rem;
|
|
|
+ font-weight: 700;
|
|
|
+`;
|
|
|
+
|
|
|
+type RecentActivityProps = ApiProps & {
|
|
|
+ nextPostId?: PostId;
|
|
|
+};
|
|
|
+
|
|
|
+const InnerRecentActivity: React.FC<RecentActivityProps> = ({ nextPostId, api }) => {
|
|
|
+ const [recentPosts, setRecentPosts] = useState<Post[]>([]);
|
|
|
+ const [loaded, setLoaded] = useState(false);
|
|
|
+ const [threadsLookup, setThreadsLookup] = useState<Record<number, Thread>>({});
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ const loadPosts = async () => {
|
|
|
+ if (!nextPostId) return;
|
|
|
+
|
|
|
+ const newId = (id: number | BN) => new PostId(id);
|
|
|
+ const apiCalls: Promise<Post>[] = [];
|
|
|
+ let id = newId(1);
|
|
|
+ while (nextPostId.gt(id)) {
|
|
|
+ apiCalls.push(api.query.forum.postById(id) as Promise<Post>);
|
|
|
+ id = newId(id.add(newId(1)));
|
|
|
+ }
|
|
|
+
|
|
|
+ const allPosts = await Promise.all(apiCalls);
|
|
|
+ const sortedPosts = orderBy(
|
|
|
+ allPosts,
|
|
|
+ [x => x.id.toNumber()],
|
|
|
+ ['desc']
|
|
|
+ );
|
|
|
+
|
|
|
+ const threadsIdsLookup = {} as Record<number, boolean>;
|
|
|
+ const postsWithUniqueThreads = sortedPosts.reduce((acc, post) => {
|
|
|
+ const threadId = post.thread_id.toNumber();
|
|
|
+ if (threadsIdsLookup[threadId]) return acc;
|
|
|
+
|
|
|
+ threadsIdsLookup[threadId] = true;
|
|
|
+ return [
|
|
|
+ ...acc,
|
|
|
+ post
|
|
|
+ ];
|
|
|
+ }, [] as Post[]);
|
|
|
+
|
|
|
+ const recentUniquePosts = postsWithUniqueThreads.slice(0, RecentActivityPostsCount);
|
|
|
+ setRecentPosts(recentUniquePosts);
|
|
|
+ setLoaded(true);
|
|
|
+ };
|
|
|
+
|
|
|
+ loadPosts();
|
|
|
+ }, [bnToStr(nextPostId)]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ const loadThreads = async () => {
|
|
|
+ const apiCalls: Promise<Thread>[] = recentPosts
|
|
|
+ .filter(p => !threadsLookup[p.thread_id.toNumber()])
|
|
|
+ .map(p => api.query.forum.threadById(p.thread_id) as Promise<Thread>);
|
|
|
+
|
|
|
+ const threads = await Promise.all(apiCalls);
|
|
|
+ const newThreadsLookup = threads.reduce((acc, thread) => {
|
|
|
+ acc[thread.id.toNumber()] = thread;
|
|
|
+ return acc;
|
|
|
+ }, {} as Record<number, Thread>);
|
|
|
+ const newLookup = {
|
|
|
+ ...threadsLookup,
|
|
|
+ ...newThreadsLookup
|
|
|
+ };
|
|
|
+
|
|
|
+ setThreadsLookup(newLookup);
|
|
|
+ };
|
|
|
+
|
|
|
+ loadThreads();
|
|
|
+ }, [recentPosts]);
|
|
|
+
|
|
|
+ const renderSectionContent = () => {
|
|
|
+ if (!loaded) {
|
|
|
+ return <i>Loading recent activity...</i>;
|
|
|
+ }
|
|
|
+ if (loaded && !recentPosts.length) {
|
|
|
+ return <span>No recent activity</span>;
|
|
|
+ }
|
|
|
+
|
|
|
+ return recentPosts.map(p => {
|
|
|
+ const threadId = p.thread_id.toNumber();
|
|
|
+
|
|
|
+ const postLinkSearch = new URLSearchParams();
|
|
|
+ postLinkSearch.set(ReplyIdxQueryParam, p.nr_in_thread.toString());
|
|
|
+ const postLinkPathname = `/forum/threads/${threadId}`;
|
|
|
+
|
|
|
+ const thread = threadsLookup[threadId];
|
|
|
+
|
|
|
+ return (
|
|
|
+ <RecentActivityEntry key={p.id.toNumber()}>
|
|
|
+ <StyledMemberPreview accountId={p.author_id} inline />
|
|
|
+ posted in
|
|
|
+ {thread && (
|
|
|
+ <StyledPostLink to={{ pathname: postLinkPathname, search: postLinkSearch.toString() }}>{thread.title}</StyledPostLink>
|
|
|
+ )}
|
|
|
+ <TimeAgoDate date={p.created_at.momentDate} id={p.id.toNumber()} />
|
|
|
+ </RecentActivityEntry>
|
|
|
+ );
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Section title="Recent activity">
|
|
|
+ {renderSectionContent()}
|
|
|
+ </Section>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const RecentActivity = withMulti<RecentActivityProps>(
|
|
|
+ InnerRecentActivity,
|
|
|
+ withApi,
|
|
|
+ withForumCalls(
|
|
|
+ ['nextPostId', { propName: 'nextPostId' }]
|
|
|
+ )
|
|
|
+);
|
|
|
+
|
|
|
+export default ForumRoot;
|