Browse Source

Upgrade joy-forum

Leszek Wiesner 4 years ago
parent
commit
ad0377e439
29 changed files with 300 additions and 278 deletions
  1. 1 0
      package.json
  2. 0 1
      pioneer/.eslintignore
  3. 3 0
      pioneer/packages/apps-routing/src/index.ts
  4. 15 0
      pioneer/packages/apps-routing/src/joy-forum.ts
  5. 0 0
      pioneer/packages/joy-forum/.skip-build
  6. 3 3
      pioneer/packages/joy-forum/package.json
  7. 20 20
      pioneer/packages/joy-forum/src/CategoryList.tsx
  8. 14 15
      pioneer/packages/joy-forum/src/Context.tsx
  9. 13 13
      pioneer/packages/joy-forum/src/EditCategory.tsx
  10. 8 11
      pioneer/packages/joy-forum/src/EditReply.tsx
  11. 13 12
      pioneer/packages/joy-forum/src/EditThread.tsx
  12. 5 5
      pioneer/packages/joy-forum/src/ForumRoot.tsx
  13. 11 10
      pioneer/packages/joy-forum/src/ForumSudo.tsx
  14. 6 9
      pioneer/packages/joy-forum/src/Moderate.tsx
  15. 4 4
      pioneer/packages/joy-forum/src/ViewReply.tsx
  16. 21 35
      pioneer/packages/joy-forum/src/ViewThread.tsx
  17. 10 12
      pioneer/packages/joy-forum/src/calls.tsx
  18. 9 11
      pioneer/packages/joy-forum/src/index.tsx
  19. 12 8
      pioneer/packages/joy-forum/src/style.ts
  20. 1 1
      pioneer/packages/joy-forum/src/validation.tsx
  21. 1 2
      pioneer/packages/joy-members/src/MemberPreview.tsx
  22. 0 39
      pioneer/packages/joy-utils-old/src/Sudo.tsx
  23. 50 16
      pioneer/packages/joy-utils/src/react/components/MemberByAccountPreview.tsx
  24. 32 22
      pioneer/packages/joy-utils/src/react/components/MemberProfilePreview.tsx
  25. 1 1
      pioneer/packages/joy-utils/src/react/components/index.tsx
  26. 36 3
      pioneer/packages/joy-utils/src/react/hocs/guards.tsx
  27. 0 15
      pioneer/packages/old-apps/apps-routing/src/joy-forum.ts
  28. 2 1
      pioneer/tsconfig.json
  29. 9 9
      types/src/forum.ts

+ 1 - 0
package.json

@@ -30,6 +30,7 @@
     "pioneer/packages/joy-proposals",
     "pioneer/packages/joy-roles",
     "pioneer/packages/joy-media",
+    "pioneer/packages/joy-forum",
     "utils/api-examples"
   ],
   "resolutions": {

+ 0 - 1
pioneer/.eslintignore

@@ -2,7 +2,6 @@
 **/coverage/*
 **/node_modules/*
 packages/old-apps/*
-packages/joy-forum/*
 packages/joy-help/*
 packages/joy-settings/*
 packages/joy-utils-old/*

+ 3 - 0
pioneer/packages/apps-routing/src/index.ts

@@ -25,6 +25,7 @@ import election from './joy-election';
 import proposals from './joy-proposals';
 import roles from './joy-roles';
 import media from './joy-media';
+import forum from './joy-forum';
 
 export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Routes {
   return appSettings.uiMode === 'light'
@@ -34,6 +35,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
       roles(t),
       election(t),
       proposals(t),
+      forum(t),
       staking(t),
       null,
       transfer(t),
@@ -49,6 +51,7 @@ export default function create (t: <T = string> (key: string, text: string, opti
       roles(t),
       election(t),
       proposals(t),
+      forum(t),
       staking(t),
       null,
       transfer(t),

+ 15 - 0
pioneer/packages/apps-routing/src/joy-forum.ts

@@ -0,0 +1,15 @@
+import { Route } from './types';
+
+import Forum from '@polkadot/joy-forum/index';
+
+export default function create (t: <T = string> (key: string, text: string, options: { ns: string }) => T): Route {
+  return {
+    Component: Forum,
+    display: {
+      needsApi: ['query.forum.threadById']
+    },
+    text: t<string>('nav.forum', 'Forum', { ns: 'apps-routing' }),
+    icon: 'comment-dots',
+    name: 'forum'
+  };
+}

+ 0 - 0
pioneer/packages/joy-forum/.skip-build


+ 3 - 3
pioneer/packages/joy-forum/package.json

@@ -7,10 +7,10 @@
   "author": "Joystream contributors",
   "maintainers": [],
   "dependencies": {
-    "@babel/runtime": "^7.7.1",
+    "@babel/runtime": "^7.10.5",
+    "@polkadot/react-components": "0.51.1",
+    "@polkadot/react-query": "0.51.1",
     "@polkadot/joy-utils": "^0.1.1",
-    "@polkadot/react-components": "0.37.0-beta.63",
-    "@polkadot/react-query": "0.37.0-beta.63",
     "lodash": "^4.17.15"
   }
 }

+ 20 - 20
pioneer/packages/joy-forum/src/CategoryList.tsx

@@ -1,26 +1,22 @@
 import React, { useEffect, useState } from 'react';
 import { Link } from 'react-router-dom';
 import ReactMarkdown from 'react-markdown';
-import { Table, Dropdown, Button, Segment, Label } from 'semantic-ui-react';
+import { Table, Dropdown, Button, Segment, Label, SemanticICONS, Icon } from 'semantic-ui-react';
 import styled from 'styled-components';
 import orderBy from 'lodash/orderBy';
 import BN from 'bn.js';
-
-import { Option, bool } from '@polkadot/types';
 import { ThreadId } from '@joystream/types/common';
 import { CategoryId, Category, Thread } from '@joystream/types/forum';
 import { ViewThread } from './ViewThread';
-import { MutedSpan } from '@polkadot/joy-utils/MutedText';
+import { MutedSpan, Section, JoyWarn, SemanticTxButton } from '@polkadot/joy-utils/react/components';
 import { UrlHasIdProps, CategoryCrumbs, Pagination, ThreadsPerPage, usePagination } from './utils';
-import Section from '@polkadot/joy-utils/Section';
-import { JoyWarn } from '@polkadot/joy-utils/JoyStatus';
 import { withForumCalls } from './calls';
 import { withMulti, withApi } from '@polkadot/react-api';
 import { ApiProps } from '@polkadot/react-api/types';
-import { bnToStr, isEmptyArr } from '@polkadot/joy-utils/index';
-import TxButton from '@polkadot/joy-utils/TxButton';
+import { bnToStr, isEmptyArr } from '@polkadot/joy-utils/functions/misc';
 import { IfIAmForumSudo } from './ForumSudo';
-import { MemberPreview } from '@polkadot/joy-members/MemberPreview';
+import MemberPreview from '@polkadot/joy-utils/react/components/MemberByAccountPreview';
+import { useApi } from '@polkadot/react-hooks';
 
 type CategoryActionsProps = {
   id: CategoryId;
@@ -29,23 +25,25 @@ type CategoryActionsProps = {
 
 function CategoryActions (props: CategoryActionsProps) {
   const { id, category } = props;
+  const { api } = useApi();
   const className = 'ui button ActionButton';
 
   type BtnProps = {
     label: string;
-    icon?: string;
+    icon?: SemanticICONS;
     archive?: boolean;
     delete?: boolean;
   };
 
   const UpdateCategoryButton = (btnProps: BtnProps) => {
-    return <TxButton
+    return <SemanticTxButton
       className='item'
-      isPrimary={false}
-      label={<><i className={`${btnProps.icon} icon`} />{btnProps.label}</>}
-      params={[id, new Option(bool, btnProps.archive), new Option(bool, btnProps.delete)]}
+      params={[id, api.createType('Option<bool>', btnProps.archive), api.createType('Option<bool>', btnProps.delete)]}
       tx={'forum.updateCategory'}
-    />;
+    >
+      <Icon name={btnProps.icon}/>
+      { btnProps.label }
+    </SemanticTxButton>;
   };
 
   if (category.archived) {
@@ -160,8 +158,9 @@ function InnerViewCategory (props: InnerViewCategoryProps) {
     }
 
     <Segment>
-      <div>
-        <MemberPreview accountId={category.moderator_id} prefixLabel='Creator:' />
+      <div style={{ display: 'flex', alignItems: 'center' }}>
+        <div style={{ marginRight: '0.5em', color: '#777' }}>Creator:</div>
+        <MemberPreview accountId={category.moderator_id} showCouncilBadge showId={false}/>
       </div>
       <div style={{ marginTop: '1rem' }}>
         <ReactMarkdown className='JoyMemo--full' source={category.description} linkTarget='_blank' />
@@ -221,7 +220,7 @@ function InnerCategoryThreads (props: CategoryThreadsProps) {
     const loadThreads = async () => {
       if (!nextThreadId || threadCount === 0) return;
 
-      const newId = (id: number | BN) => new ThreadId(id);
+      const newId = (id: number | BN) => api.createType('ThreadId', id);
       const apiCalls: Promise<Thread>[] = [];
       let id = newId(1);
       while (nextThreadId.gt(id)) {
@@ -319,8 +318,9 @@ type ViewCategoryByIdProps = UrlHasIdProps & {
 
 export function ViewCategoryById (props: ViewCategoryByIdProps) {
   const { match: { params: { id } } } = props;
+  const { api } = useApi();
   try {
-    return <ViewCategory id={new CategoryId(id)} />;
+    return <ViewCategory id={api.createType('CategoryId', id)} />;
   } catch (err) {
     return <em>Invalid category ID: {id}</em>;
   }
@@ -340,7 +340,7 @@ function InnerCategoryList (props: CategoryListProps) {
     const loadCategories = async () => {
       if (!nextCategoryId) return;
 
-      const newId = (id: number | BN) => new CategoryId(id);
+      const newId = (id: number | BN) => api.createType('CategoryId', id);
       const apiCalls: Promise<Category>[] = [];
       let id = newId(1);
       while (nextCategoryId.gt(id)) {

+ 14 - 15
pioneer/packages/joy-forum/src/Context.tsx

@@ -2,9 +2,8 @@
 // NOTE: The purpose of this context is to immitate a Substrate storage for the forum until it's implemented as a substrate runtime module.
 
 import React, { useReducer, createContext, useContext } from 'react';
-import { Category, Thread, Reply, ModerationAction } from '@joystream/types/forum';
-import { BlockAndTime } from '@joystream/types/common';
-import { Option, Text, GenericAccountId } from '@polkadot/types';
+import { Category, Thread, Reply } from '@joystream/types/forum';
+import { createMock } from '@joystream/types';
 
 type CategoryId = number;
 type ThreadId = number;
@@ -220,14 +219,14 @@ function reducer (state: ForumState, action: ForumAction): ForumState {
       const { threadById } = state;
 
       const thread = threadById.get(id) as Thread;
-      const moderation = new ModerationAction({
-        moderated_at: BlockAndTime.newEmpty(),
-        moderator_id: new GenericAccountId(moderator),
-        rationale: new Text(rationale)
+      const moderation = createMock('ModerationAction', {
+        moderated_at: createMock('BlockAndTime', {}),
+        moderator_id: createMock('AccountId', moderator),
+        rationale: createMock('Text', rationale)
       });
-      const threadUpd = new Thread(Object.assign(
+      const threadUpd = createMock('Thread', Object.assign(
         thread.cloneValues(),
-        { moderation: new Option(ModerationAction, moderation) }
+        { moderation: createMock('Option<ModerationAction>', moderation) }
       ));
       threadById.set(id, threadUpd);
 
@@ -285,14 +284,14 @@ function reducer (state: ForumState, action: ForumAction): ForumState {
       const { replyById } = state;
 
       const reply = replyById.get(id) as Reply;
-      const moderation = new ModerationAction({
-        moderated_at: BlockAndTime.newEmpty(),
-        moderator_id: new GenericAccountId(moderator),
-        rationale: new Text(rationale)
+      const moderation = createMock('ModerationAction', {
+        moderated_at: createMock('BlockAndTime', {}),
+        moderator_id: createMock('AccountId', moderator),
+        rationale: createMock('Text', rationale)
       });
-      const replyUpd = new Reply(Object.assign(
+      const replyUpd = createMock('Reply', Object.assign(
         reply.cloneValues(),
-        { moderation: new Option(ModerationAction, moderation) }
+        { moderation: createMock('Option<ModerationAction>', moderation) }
       ));
       replyById.set(id, replyUpd);
 

+ 13 - 13
pioneer/packages/joy-forum/src/EditCategory.tsx

@@ -4,21 +4,20 @@ import { Form, Field, withFormik, FormikProps } from 'formik';
 import * as Yup from 'yup';
 import { History } from 'history';
 
-import TxButton from '@polkadot/joy-utils/TxButton';
+import { TxButton, Section } from '@polkadot/joy-utils/react/components';
 import { SubmittableResult } from '@polkadot/api';
-import { withMulti } from '@polkadot/react-api/with';
+import { withMulti } from '@polkadot/react-api/hoc';
 
-import * as JoyForms from '@polkadot/joy-utils/forms';
-import { Text } from '@polkadot/types';
-import { Option } from '@polkadot/types/codec';
+import * as JoyForms from '@polkadot/joy-utils/react/components/forms';
 import { CategoryId, Category } from '@joystream/types/forum';
-import Section from '@polkadot/joy-utils/Section';
-import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext';
+import { useMyAccount } from '@polkadot/joy-utils/react/hooks';
 import { UrlHasIdProps, CategoryCrumbs } from './utils';
 import { withOnlyForumSudo } from './ForumSudo';
 import { withForumCalls } from './calls';
 import { ValidationProps, withCategoryValidation } from './validation';
 import { TxFailedCallback, TxCallback } from '@polkadot/react-components/Status/types';
+import { createMock } from '@joystream/types';
+import { useApi } from '@polkadot/react-hooks';
 
 const buildSchema = (props: ValidationProps) => {
   const {
@@ -128,9 +127,9 @@ const InnerForm = (props: FormProps) => {
 
     if (isNew) {
       return [
-        new Option(CategoryId, parentId),
-        new Text(title),
-        new Text(description)
+        createMock('Option<CategoryId>', parentId),
+        title,
+        description
       ];
     } else {
       // NOTE: currently update_category doesn't support title and description updates.
@@ -152,7 +151,6 @@ const InnerForm = (props: FormProps) => {
       <LabelledField {...props}>
         <TxButton
           type='submit'
-          size='large'
           label={isNew
             ? `Create a ${categoryWord}`
             : 'Update a category'
@@ -232,8 +230,9 @@ function FormOrLoading (props: OuterProps) {
 function withIdFromUrl (Component: React.ComponentType<OuterProps>) {
   return function (props: UrlHasIdProps) {
     const { match: { params: { id } } } = props;
+    const { api } = useApi();
     try {
-      return <Component id={new CategoryId(id)} />;
+      return <Component id={api.createType('CategoryId', id)} />;
     } catch (err) {
       return <em>Invalid category ID: {id}</em>;
     }
@@ -242,8 +241,9 @@ function withIdFromUrl (Component: React.ComponentType<OuterProps>) {
 
 function NewSubcategoryForm (props: UrlHasIdProps & OuterProps) {
   const { match: { params: { id } } } = props;
+  const { api } = useApi();
   try {
-    return <EditForm {...props} parentId={new CategoryId(id)} />;
+    return <EditForm {...props} parentId={api.createType('CategoryId', id)} />;
   } catch (err) {
     return <em>Invalid parent category id: {id}</em>;
   }

+ 8 - 11
pioneer/packages/joy-forum/src/EditReply.tsx

@@ -4,17 +4,16 @@ import styled from 'styled-components';
 import { Form, Field, withFormik, FormikProps } from 'formik';
 import * as Yup from 'yup';
 
-import TxButton from '@polkadot/joy-utils/TxButton';
+import { TxButton } from '@polkadot/joy-utils/react/components';
 import { SubmittableResult } from '@polkadot/api';
-import { withMulti } from '@polkadot/react-api/with';
+import { withMulti } from '@polkadot/react-api/hoc';
 
-import * as JoyForms from '@polkadot/joy-utils/forms';
-import { Text } from '@polkadot/types';
+import * as JoyForms from '@polkadot/joy-utils/react/components/forms';
 import { PostId, ThreadId } from '@joystream/types/common';
 import { Post } from '@joystream/types/forum';
-import { withOnlyMembers } from '@polkadot/joy-utils/MyAccount';
-import Section from '@polkadot/joy-utils/Section';
-import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext';
+import { withOnlyMembers } from '@polkadot/joy-utils/react/hocs/guards';
+import { Section } from '@polkadot/joy-utils/react/components';
+import { useMyAccount } from '@polkadot/joy-utils/react/hooks';
 import { withForumCalls } from './calls';
 import { ValidationProps, withReplyValidation } from './validation';
 import { TxFailedCallback, TxCallback } from '@polkadot/react-components/Status/types';
@@ -105,11 +104,10 @@ const InnerForm = (props: FormProps) => {
   const buildTxParams = () => {
     if (!isValid) return [];
 
-    const textParam = new Text(text);
     if (!id) {
-      return [threadId, textParam];
+      return [threadId, text];
     } else {
-      return [id, textParam];
+      return [id, text];
     }
   };
 
@@ -125,7 +123,6 @@ const InnerForm = (props: FormProps) => {
           <div>
             <TxButton
               type='submit'
-              size='large'
               label={isNew
                 ? 'Post a reply'
                 : 'Update a reply'

+ 13 - 12
pioneer/packages/joy-forum/src/EditThread.tsx

@@ -6,21 +6,21 @@ import { Form, Field, withFormik, FormikProps } from 'formik';
 import * as Yup from 'yup';
 import { History } from 'history';
 
-import TxButton from '@polkadot/joy-utils/TxButton';
+import { TxButton } from '@polkadot/joy-utils/react/components';
 import { SubmittableResult } from '@polkadot/api';
-import { withMulti } from '@polkadot/react-api/with';
+import { withMulti } from '@polkadot/react-api/hoc';
 
-import * as JoyForms from '@polkadot/joy-utils/forms';
-import { Text } from '@polkadot/types';
+import * as JoyForms from '@polkadot/joy-utils/react/components/forms';
 import { ThreadId } from '@joystream/types/common';
 import { Thread, CategoryId } from '@joystream/types/forum';
-import { withOnlyMembers } from '@polkadot/joy-utils/MyAccount';
-import Section from '@polkadot/joy-utils/Section';
-import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext';
+import { withOnlyMembers } from '@polkadot/joy-utils/react/hocs/guards';
+import { Section } from '@polkadot/joy-utils/react/components';
+import { useMyAccount } from '@polkadot/joy-utils/react/hooks';
 import { UrlHasIdProps, CategoryCrumbs } from './utils';
 import { withForumCalls } from './calls';
 import { ValidationProps, withThreadValidation } from './validation';
 import { TxFailedCallback, TxCallback } from '@polkadot/react-components/Status/types';
+import { useApi } from '@polkadot/react-hooks';
 
 const buildSchema = (props: ValidationProps) => {
   const {
@@ -141,8 +141,8 @@ const InnerForm = (props: FormProps) => {
     if (isNew) {
       return [
         resolvedCategoryId,
-        new Text(title),
-        new Text(text)
+        title,
+        text
       ];
     } else {
       // NOTE: currently forum SRML doesn't support thread update.
@@ -182,7 +182,6 @@ const InnerForm = (props: FormProps) => {
       <LabelledField {...props}>
         <TxButton
           type='submit'
-          size='large'
           label={isNew
             ? 'Create a thread'
             : 'Update a thread'
@@ -260,8 +259,9 @@ function FormOrLoading (props: OuterProps) {
 function withCategoryIdFromUrl (Component: React.ComponentType<OuterProps>) {
   return function (props: UrlHasIdProps) {
     const { match: { params: { id } } } = props;
+    const { api } = useApi();
     try {
-      return <Component {...props} categoryId={new CategoryId(id)} />;
+      return <Component {...props} categoryId={api.createType('CategoryId', id)} />;
     } catch (err) {
       return <em>Invalid category ID: {id}</em>;
     }
@@ -271,8 +271,9 @@ function withCategoryIdFromUrl (Component: React.ComponentType<OuterProps>) {
 function withIdFromUrl (Component: React.ComponentType<OuterProps>) {
   return function (props: UrlHasIdProps) {
     const { match: { params: { id } } } = props;
+    const { api } = useApi();
     try {
-      return <Component {...props} id={new ThreadId(id)} />;
+      return <Component {...props} id={api.createType('ThreadId', id)} />;
     } catch (err) {
       return <em>Invalid thread ID: {id}</em>;
     }

+ 5 - 5
pioneer/packages/joy-forum/src/ForumRoot.tsx

@@ -4,13 +4,13 @@ import styled from 'styled-components';
 import { orderBy } from 'lodash';
 import BN from 'bn.js';
 
-import Section from '@polkadot/joy-utils/Section';
+import { Section } from '@polkadot/joy-utils/react/components';
 import { withMulti, withApi } from '@polkadot/react-api';
 import { PostId } from '@joystream/types/common';
 import { Post, Thread } from '@joystream/types/forum';
-import { bnToStr } from '@polkadot/joy-utils/';
+import { bnToStr } from '@polkadot/joy-utils/functions/misc';
 import { ApiProps } from '@polkadot/react-api/types';
-import { MemberPreview } from '@polkadot/joy-members/MemberPreview';
+import MemberPreview from '@polkadot/joy-utils/react/components/MemberByAccountPreview';
 
 import { CategoryCrumbs, RecentActivityPostsCount, ReplyIdxQueryParam, TimeAgoDate } from './utils';
 import { withForumCalls } from './calls';
@@ -62,7 +62,7 @@ const InnerRecentActivity: React.FC<RecentActivityProps> = ({ nextPostId, api })
     const loadPosts = async () => {
       if (!nextPostId) return;
 
-      const newId = (id: number | BN) => new PostId(id);
+      const newId = (id: number | BN) => api.createType('PostId', id);
       const apiCalls: Promise<Post>[] = [];
       let id = newId(1);
       while (nextPostId.gt(id)) {
@@ -138,7 +138,7 @@ const InnerRecentActivity: React.FC<RecentActivityProps> = ({ nextPostId, api })
 
       return (
         <RecentActivityEntry key={p.id.toNumber()}>
-          <StyledMemberPreview accountId={p.author_id} inline />
+          <StyledMemberPreview accountId={p.author_id} size="small" showId={false}/>
           posted in
           {thread && (
             <StyledPostLink to={{ pathname: postLinkPathname, search: postLinkSearch.toString() }}>{thread.title}</StyledPostLink>

+ 11 - 10
pioneer/packages/joy-forum/src/ForumSudo.tsx

@@ -3,21 +3,22 @@ import { Button } from 'semantic-ui-react';
 import { Form, Field, withFormik, FormikProps, FieldProps } from 'formik';
 import * as Yup from 'yup';
 
-import TxButton from '@polkadot/joy-utils/TxButton';
+import { TxButton } from '@polkadot/joy-utils/react/components';
 import { SubmittableResult } from '@polkadot/api';
 import { InputAddress } from '@polkadot/react-components/index';
-import { withMulti } from '@polkadot/react-api/with';
+import { withMulti } from '@polkadot/react-api/hoc';
 
-import * as JoyForms from '@polkadot/joy-utils/forms';
+import * as JoyForms from '@polkadot/joy-utils/react/components/forms';
 import { Option } from '@polkadot/types/codec';
-import Section from '@polkadot/joy-utils/Section';
-import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext';
-import { withOnlySudo } from '@polkadot/joy-utils/Sudo';
+import { Section } from '@polkadot/joy-utils/react/components';
+import { useMyAccount } from '@polkadot/joy-utils/react/hooks';
+import { withOnlySudo } from '@polkadot/joy-utils/react/hocs/guards';
 import { AccountId } from '@polkadot/types/interfaces';
-import { JoyError } from '@polkadot/joy-utils/JoyStatus';
-import AddressMini from '@polkadot/react-components/AddressMiniJoy';
+import { JoyError } from '@polkadot/joy-utils/react/components';
+import AddressMini from '@polkadot/react-components/AddressMini';
 import { withForumCalls } from './calls';
 import { TxFailedCallback, TxCallback } from '@polkadot/react-components/Status/types';
+import { useApi } from '@polkadot/react-hooks';
 
 const buildSchema = () => Yup.object().shape({});
 
@@ -42,6 +43,7 @@ const InnerForm = (props: FormProps) => {
     isSubmitting,
     setSubmitting
   } = props;
+  const { api } = useApi();
 
   const {
     sudo
@@ -75,7 +77,7 @@ const InnerForm = (props: FormProps) => {
 
   const buildTxParams = () => {
     if (!isValid) return [];
-    return [new Option('AccountId', sudo)];
+    return [api.createType('Option<AccountId>', sudo)];
   };
 
   type SudoInputAddressProps = FieldProps<FormValues>; /* & InputAddressProps */
@@ -108,7 +110,6 @@ const InnerForm = (props: FormProps) => {
       <LabelledField {...props}>
         <TxButton
           type='submit'
-          size='large'
           label={isNotSet
             ? 'Set forum sudo'
             : 'Update forum sudo'

+ 6 - 9
pioneer/packages/joy-forum/src/Moderate.tsx

@@ -3,15 +3,14 @@ import { Button } from 'semantic-ui-react';
 import { Form, Field, withFormik, FormikProps } from 'formik';
 import * as Yup from 'yup';
 
-import TxButton from '@polkadot/joy-utils/TxButton';
+import { TxButton } from '@polkadot/joy-utils/react/components';
 import { SubmittableResult } from '@polkadot/api';
-import { withMulti } from '@polkadot/react-api/with';
+import { withMulti } from '@polkadot/react-api/hoc';
 
-import * as JoyForms from '@polkadot/joy-utils/forms';
-import { Text } from '@polkadot/types';
+import * as JoyForms from '@polkadot/joy-utils/react/components/forms';
 import { ThreadId } from '@joystream/types/common';
 import { ReplyId } from '@joystream/types/forum';
-import Section from '@polkadot/joy-utils/Section';
+import { Section } from '@polkadot/joy-utils/react/components';
 import { withOnlyForumSudo } from './ForumSudo';
 import { ValidationProps, withPostModerationValidation } from './validation';
 import { TxFailedCallback, TxCallback } from '@polkadot/react-components/Status/types';
@@ -90,11 +89,10 @@ const InnerForm = (props: FormProps) => {
   const buildTxParams = () => {
     if (!isValid) return [];
 
-    const rationaleParam = new Text(rationale);
     if (isThread) {
-      return [id, rationaleParam];
+      return [id, rationale];
     } else {
-      return [id, rationaleParam];
+      return [id, rationale];
     }
   };
 
@@ -108,7 +106,6 @@ const InnerForm = (props: FormProps) => {
       <LabelledField {...props}>
         <TxButton
           type='submit'
-          size='large'
           label={'Moderate'}
           isDisabled={!dirty || isSubmitting}
           params={buildTxParams()}

+ 4 - 4
pioneer/packages/joy-forum/src/ViewReply.tsx

@@ -6,10 +6,10 @@ import { Button, Icon } from 'semantic-ui-react';
 
 import { Post, Category, Thread } from '@joystream/types/forum';
 import { Moderate } from './Moderate';
-import { JoyWarn } from '@polkadot/joy-utils/JoyStatus';
-import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext';
+import { JoyWarn } from '@polkadot/joy-utils/react/components';
+import { useMyAccount } from '@polkadot/joy-utils/react/hooks';
 import { IfIAmForumSudo } from './ForumSudo';
-import { MemberPreview } from '@polkadot/joy-members/MemberPreview';
+import MemberPreview from '@polkadot/joy-utils/react/components/MemberByAccountPreview';
 import { TimeAgoDate, ReplyIdxQueryParam } from './utils';
 
 const HORIZONTAL_PADDING = '1em';
@@ -129,7 +129,7 @@ export const ViewReply = React.forwardRef((props: ViewReplyProps, ref: React.Ref
     <ReplyContainer className="ui segment" ref={ref} selected={selected}>
       <ReplyHeader>
         <ReplyHeaderAuthorRow>
-          <MemberPreview accountId={reply.author_id} />
+          <MemberPreview accountId={reply.author_id} showCouncilBadge showId={false}/>
         </ReplyHeaderAuthorRow>
         <ReplyHeaderDetailsRow>
           <TimeAgoDate date={reply.created_at.momentDate} id={reply.id} />

+ 21 - 35
pioneer/packages/joy-forum/src/ViewThread.tsx

@@ -1,26 +1,27 @@
 import React, { useState, useEffect, useRef } from 'react';
-import { Link } from 'react-router-dom';
+import { Link, RouteComponentProps } from 'react-router-dom';
 import ReactMarkdown from 'react-markdown';
 import styled from 'styled-components';
 import { Table, Button, Label, Icon } from 'semantic-ui-react';
 import BN from 'bn.js';
 
-import { ThreadId, PostId } from '@joystream/types/common';
+import { ThreadId } from '@joystream/types/common';
 import { Category, Thread, Post } from '@joystream/types/forum';
 import { Pagination, RepliesPerPage, CategoryCrumbs, TimeAgoDate, usePagination, useQueryParam, ReplyIdxQueryParam, ReplyEditIdQueryParam } from './utils';
 import { ViewReply } from './ViewReply';
 import { Moderate } from './Moderate';
-import { MutedSpan } from '@polkadot/joy-utils/MutedText';
-import { JoyWarn } from '@polkadot/joy-utils/JoyStatus';
+import { MutedSpan } from '@polkadot/joy-utils/react/components';
+import { JoyWarn } from '@polkadot/joy-utils/react/components';
 import { withForumCalls } from './calls';
 import { withApi, withMulti } from '@polkadot/react-api';
 import { ApiProps } from '@polkadot/react-api/types';
 import { orderBy } from 'lodash';
-import { bnToStr } from '@polkadot/joy-utils/index';
+import { bnToStr } from '@polkadot/joy-utils/functions/misc';
 import { IfIAmForumSudo } from './ForumSudo';
-import { MemberPreview } from '@polkadot/joy-members/MemberPreview';
+import MemberPreview from '@polkadot/joy-utils/react/components/MemberByAccountPreview';
 import { formatDate } from '@polkadot/joy-utils/functions/date';
 import { NewReply, EditReply } from './EditReply';
+import { useApi } from '@polkadot/react-hooks';
 
 type ThreadTitleProps = {
   thread: Thread;
@@ -68,14 +69,7 @@ const ThreadInfo = styled.span`
 `;
 
 const ThreadInfoMemberPreview = styled(MemberPreview)`
-  && {
-    margin: 0 .2rem;
-
-    .PrefixLabel {
-      color: inherit;
-      margin-right: .2rem;
-    }
-  }
+  margin: 0 .5rem;
 `;
 
 const ReplyEditContainer = styled.div`
@@ -110,7 +104,7 @@ const ThreadPreview: React.FC<ThreadPreviewProps> = ({ thread, repliesCount }) =
         {repliesCount}
       </Table.Cell>
       <Table.Cell>
-        <MemberPreview accountId={thread.author_id} />
+        <MemberPreview accountId={thread.author_id} showCouncilBadge showId={false}/>
       </Table.Cell>
       <Table.Cell>
         {formatDate(thread.created_at.momentDate)}
@@ -144,9 +138,9 @@ function InnerViewThread (props: ViewThreadProps) {
   const parsedSelectedPostIdx = rawSelectedPostIdx ? parseInt(rawSelectedPostIdx) : null;
   const selectedPostIdx = (parsedSelectedPostIdx && !Number.isNaN(parsedSelectedPostIdx)) ? parsedSelectedPostIdx : null;
 
-  const { category, thread, preview = false } = props;
+  const { category, thread, preview = false, api, nextPostId } = props;
 
-  const editedPostId = rawEditedPostId && new PostId(rawEditedPostId);
+  const editedPostId = rawEditedPostId && api.createType('PostId', rawEditedPostId);
 
   if (!thread) {
     return <em>Loading thread details...</em>;
@@ -178,7 +172,6 @@ function InnerViewThread (props: ViewThreadProps) {
     return <ThreadPreview thread={thread} repliesCount={totalPostsInThread - 1} />;
   }
 
-  const { api, nextPostId } = props;
   const [loaded, setLoaded] = useState(false);
   const [posts, setPosts] = useState(new Array<Post>());
 
@@ -187,7 +180,7 @@ function InnerViewThread (props: ViewThreadProps) {
     const loadPosts = async () => {
       if (!nextPostId || totalPostsInThread === 0) return;
 
-      const newId = (id: number | BN) => new PostId(id);
+      const newId = (id: number | BN) => api.createType('PostId', id);
       const apiCalls: Promise<Post>[] = [];
       let id = newId(1);
       while (nextPostId.gt(id)) {
@@ -384,8 +377,8 @@ function InnerViewThread (props: ViewThreadProps) {
       </h1>
       <ThreadInfoAndActions>
         <ThreadInfo>
-          Created
-          <ThreadInfoMemberPreview accountId={thread.author_id} inline prefixLabel="by" />
+          Created by
+          <ThreadInfoMemberPreview accountId={thread.author_id} size="small" showId={false}/>
           <TimeAgoDate date={thread.created_at.momentDate} id="thread" />
         </ThreadInfo>
         {renderActions()}
@@ -424,28 +417,23 @@ export const ViewThread = withMulti(
   )
 );
 
-type ViewThreadByIdProps = ApiProps & {
-  match: {
-    params: {
-      id: string;
-    };
-  };
-};
+type ViewThreadByIdProps = RouteComponentProps<{ id: string }>;
 
-function InnerViewThreadById (props: ViewThreadByIdProps) {
-  const { api, match: { params: { id } } } = props;
+export function ViewThreadById (props: ViewThreadByIdProps) {
+  const { api } = useApi();
+  const { match: { params: { id } } } = props;
 
   let threadId: ThreadId;
   try {
-    threadId = new ThreadId(id);
+    threadId = api.createType('ThreadId', id);
   } catch (err) {
     console.log('Failed to parse thread id form URL');
     return <em>Invalid thread ID: {id}</em>;
   }
 
   const [loaded, setLoaded] = useState(false);
-  const [thread, setThread] = useState(Thread.newEmpty());
-  const [category, setCategory] = useState(Category.newEmpty());
+  const [thread, setThread] = useState(api.createType('Thread', {}));
+  const [category, setCategory] = useState(api.createType('Category', {}));
 
   useEffect(() => {
     const loadThreadAndCategory = async () => {
@@ -478,5 +466,3 @@ function InnerViewThreadById (props: ViewThreadByIdProps) {
 
   return <ViewThread id={threadId} category={category} thread={thread} />;
 }
-
-export const ViewThreadById = withApi(InnerViewThreadById);

+ 10 - 12
pioneer/packages/joy-forum/src/calls.tsx

@@ -1,14 +1,12 @@
 
 import React from 'react';
 import { ApiProps, SubtractProps } from '@polkadot/react-api/types';
-import { Options } from '@polkadot/react-api/with/types';
-import { withApi, withCall as withSubstrateCall } from '@polkadot/react-api';
-import { Option } from '@polkadot/types/codec';
-import { AccountId } from '@polkadot/types/interfaces';
+import { Options } from '@polkadot/react-api/hoc/types';
+import { withApi, withCall as withSubstrateCall } from '@polkadot/react-api/hoc';
 import { u64 } from '@polkadot/types';
-import { Constructor } from '@polkadot/types/types';
-import { Category, Thread, Reply } from '@joystream/types/forum';
+import { InterfaceTypes } from '@polkadot/types/types/registry';
 import { useForum, ForumState } from './Context';
+import { createMock } from '@joystream/types';
 
 type Call = string | [string, Options];
 
@@ -19,17 +17,17 @@ const storage: StorageType = 'substrate';
 type EntityMapName = 'categoryById' | 'threadById' | 'replyById';
 
 const getReactValue = (state: ForumState, endpoint: string, paramValue: any): any => {
-  const getEntityById = (mapName: EntityMapName, constructor: Constructor): any => {
+  const getEntityById = (mapName: EntityMapName, type: keyof InterfaceTypes): any => {
     const id = (paramValue as u64).toNumber();
     const entity = state[mapName].get(id);
-    return new constructor(entity);
+    return createMock(type, entity);
   };
 
   switch (endpoint) {
-    case 'forumSudo': return new Option<AccountId>('AccountId', state.sudo);
-    case 'categoryById': return getEntityById(endpoint, Category);
-    case 'threadById': return getEntityById(endpoint, Thread);
-    case 'replyById': return getEntityById(endpoint, Reply);
+    case 'forumSudo': return createMock('Option<AccountId>', state.sudo);
+    case 'categoryById': return getEntityById(endpoint, 'Category');
+    case 'threadById': return getEntityById(endpoint, 'Thread');
+    case 'replyById': return getEntityById(endpoint, 'Reply');
     default: throw new Error('Unknown endpoint for Forum storage');
   }
 };

+ 9 - 11
pioneer/packages/joy-forum/src/index.tsx

@@ -3,25 +3,23 @@ import React from 'react';
 import { Route, Switch } from 'react-router';
 import styled from 'styled-components';
 
-import { AppProps, I18nProps } from '@polkadot/react-components/types';
-
-import './index.css';
+import { RouteProps as AppMainRouteProps } from '@polkadot/apps-routing/types';
+import { I18nProps } from '@polkadot/react-components/types';
 
+import style from './style';
 import translate from './translate';
 import { ForumProvider } from './Context';
 import { ForumSudoProvider } from './ForumSudo';
-import { NewSubcategory, EditCategory } from './EditCategory';
+import { NewSubcategory, NewCategory, EditCategory } from './EditCategory';
 import { NewThread, EditThread } from './EditThread';
 import { CategoryList, ViewCategoryById } from './CategoryList';
 import { ViewThreadById } from './ViewThread';
 import { LegacyPagingRedirect } from './LegacyPagingRedirect';
 import ForumRoot from './ForumRoot';
 
-const ForumContentWrapper = styled.main`
-  padding-top: 1.5rem;
-`;
+const ForumMain = styled.main`${style}`;
 
-type Props = AppProps & I18nProps & {};
+type Props = AppMainRouteProps & I18nProps;
 
 class App extends React.PureComponent<Props> {
   render () {
@@ -29,15 +27,15 @@ class App extends React.PureComponent<Props> {
     return (
       <ForumProvider>
         <ForumSudoProvider>
-          <ForumContentWrapper className='forum--App'>
+          <ForumMain className='forum--App'>
             <Switch>
+              <Route path={`${basePath}/categories/new`} component={NewCategory} />
               {/* routes for handling legacy format of forum paging within the routing path */}
               {/* translate page param to search query */}
               <Route path={`${basePath}/categories/:id/page/:page`} component={LegacyPagingRedirect} />
               <Route path={`${basePath}/threads/:id/page/:page`} component={LegacyPagingRedirect} />
 
               {/* <Route path={`${basePath}/sudo`} component={EditForumSudo} /> */}
-              {/* <Route path={`${basePath}/categories/new`} component={NewCategory} /> */}
 
               <Route path={`${basePath}/categories/:id/newSubcategory`} component={NewSubcategory} />
               <Route path={`${basePath}/categories/:id/newThread`} component={NewThread} />
@@ -50,7 +48,7 @@ class App extends React.PureComponent<Props> {
 
               <Route component={ForumRoot} />
             </Switch>
-          </ForumContentWrapper>
+          </ForumMain>
         </ForumSudoProvider>
       </ForumProvider>
     );

+ 12 - 8
pioneer/packages/joy-forum/src/index.css → pioneer/packages/joy-forum/src/style.ts

@@ -1,4 +1,8 @@
-.forum--App {
+import { css } from 'styled-components';
+
+export default css`
+  padding-top: 1.5rem;
+
   .ui.segment {
     background-color: #fff;
   }
@@ -11,13 +15,13 @@
       margin-right: 1rem;
     }
   }
-}
-
-.EditEntityBox {
-  width: 100%;
-  max-width: 600px;
 
-  .EditEntityForm {
+  .EditEntityBox {
     width: 100%;
+    max-width: 600px;
+
+    .EditEntityForm {
+      width: 100%;
+    }
   }
-}
+`;

+ 1 - 1
pioneer/packages/joy-forum/src/validation.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
-import { withMulti } from '@polkadot/react-api/with';
+import { withMulti } from '@polkadot/react-api/hoc';
 import { InputValidationLengthConstraint } from '@joystream/types/common';
 import { withForumCalls } from './calls';
 import { componentName } from '@polkadot/joy-utils/react/helpers';

+ 1 - 2
pioneer/packages/joy-members/src/MemberPreview.tsx

@@ -91,9 +91,8 @@ const withMemberIdByAccountId = withCalls<WithMemberIdByAccountIdProps>(
 function setMemberIdByAccountId (Component: React.ComponentType<MemberPreviewProps>) {
   return function (props: WithMemberIdByAccountIdProps & MemberPreviewProps) {
     const { memberIdsByRootAccountId, memberIdsByControllerAccountId } = props;
-
     if (memberIdsByRootAccountId && memberIdsByControllerAccountId) {
-      memberIdsByRootAccountId.concat(memberIdsByControllerAccountId);
+      memberIdsByRootAccountId.toArray().concat(memberIdsByControllerAccountId.toArray());
 
       if (memberIdsByRootAccountId.length) {
         return <Component {...props} memberId={memberIdsByRootAccountId[0]} />;

+ 0 - 39
pioneer/packages/joy-utils-old/src/Sudo.tsx

@@ -1,39 +0,0 @@
-import React from 'react';
-import { Message } from 'semantic-ui-react';
-
-import { AccountId } from '@polkadot/types/interfaces';
-import { withCalls, withMulti } from '@polkadot/react-api/with';
-import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext';
-
-type OnlySudoProps = {
-  sudo?: AccountId;
-};
-
-function OnlySudo<P extends OnlySudoProps> (Component: React.ComponentType<P>) {
-  return function (props: P) {
-    const { sudo } = props;
-    if (!sudo) {
-      return <em>Loading sudo key...</em>;
-    }
-
-    const { state: { address: myAddress } } = useMyAccount();
-    const iAmSudo = myAddress === sudo.toString();
-
-    if (iAmSudo) {
-      return <Component {...props} />;
-    } else {
-      return (
-        <Message warning className='JoyMainStatus'>
-          <Message.Header>Only sudo can access this functionality.</Message.Header>
-        </Message>
-      );
-    }
-  };
-}
-
-export const withOnlySudo = <P extends OnlySudoProps> (Component: React.ComponentType<P>) =>
-  withMulti(
-    Component,
-    withCalls(['query.sudo.key', { propName: 'sudo' }]),
-    OnlySudo
-  );

+ 50 - 16
pioneer/packages/joy-utils/src/react/components/MemberByAccountPreview.tsx

@@ -1,20 +1,30 @@
 import React from 'react';
 
-import ProfilePreview from './MemberProfilePreview';
+import { ProfilePreviewFromStruct } from './MemberProfilePreview';
 import { AccountId } from '@polkadot/types/interfaces';
 import { MemberFromAccount } from '../../types/members';
 import { useTransport, usePromise } from '../hooks';
-
 import styled from 'styled-components';
-import PromiseComponent from './PromiseComponent';
 
-const MemberByAccount = styled.div``;
+import PromiseComponent from './PromiseComponent';
 
 type Props = {
   accountId: AccountId | string;
+  className?: string;
+  showId?: boolean;
+  showCouncilBadge?: boolean;
+  link?: boolean;
+  size?: 'small' | 'medium';
 };
 
-const MemberByAccountPreview: React.FunctionComponent<Props> = ({ accountId }) => {
+const MemberByAccountPreview: React.FunctionComponent<Props> = ({
+  accountId,
+  showId = true,
+  showCouncilBadge = false,
+  link = true,
+  size,
+  className,
+}) => {
   const transport = useTransport();
   const [member, error, loading] = usePromise<MemberFromAccount | null>(
     () => transport.members.membershipFromAccount(accountId),
@@ -23,21 +33,45 @@ const MemberByAccountPreview: React.FunctionComponent<Props> = ({ accountId }) =
   );
 
   return (
-    <PromiseComponent error={error} loading={loading} message='Fetching member profile...'>
-      <MemberByAccount>
+    // Span required to allow styled(MemberByAccountPreview)
+    <span className={className}>
+      <PromiseComponent error={error} loading={loading} message='Fetching member profile...'>
         { member && (
           member.profile
-            ? <ProfilePreview
-              avatar_uri={member.profile.avatar_uri.toString()}
-              root_account={member.profile.root_account.toString()}
-              handle={member.profile.handle.toString()}
-              id={member.memberId}
-              link={true}/>
+            ? (
+              <ProfilePreviewFromStruct
+                profile={member.profile}
+                id={showId ? member.memberId : undefined}
+                link={link}
+                size={size}>
+                { showCouncilBadge && <CouncilBadge memberId={member.memberId!}/> }
+              </ProfilePreviewFromStruct>
+            )
             : 'Member profile not found!'
         ) }
-      </MemberByAccount>
-    </PromiseComponent>
+      </PromiseComponent>
+    </span>
   );
 };
 
-export default MemberByAccountPreview;
+type CouncilBadgeProps = {
+  memberId: number;
+}
+
+export function CouncilBadge({ memberId }: CouncilBadgeProps) {
+  const transport = useTransport();
+  const [councilMembers] = usePromise(() => transport.council.councilMembers(), []);
+
+  if (councilMembers && councilMembers.find(cm => cm.memberId.toNumber() === memberId)) {
+    return (
+      <b style={{ color: '#607d8b' }}>
+        <i className='university icon'></i>
+        Council member
+      </b>
+    )
+  } else {
+    return null;
+  }
+}
+
+export default styled(MemberByAccountPreview)``; // Allow extending the styles

+ 32 - 22
pioneer/packages/joy-utils/src/react/components/MemberProfilePreview.tsx

@@ -1,60 +1,69 @@
 import React from 'react';
-import { Image } from 'semantic-ui-react';
+import { Image, Label } from 'semantic-ui-react';
 import { IdentityIcon } from '@polkadot/react-components';
 import { Link } from 'react-router-dom';
 import { Text } from '@polkadot/types';
 import { AccountId } from '@polkadot/types/interfaces';
 import { MemberId, Membership } from '@joystream/types/members';
-import styled from 'styled-components';
+import styled, { css } from 'styled-components';
+
+type ProfilePreviewSize = 'small' | 'medium';
+
+const AVATAR_SIZES_PX: { [k in ProfilePreviewSize]: number } = {
+  small: 20,
+  medium: 40
+}
 
 type ProfileItemProps = {
   avatar_uri: string | Text;
   root_account: string | AccountId;
   handle: string | Text;
-  link?: boolean;
   id?: number | MemberId;
+  link?: boolean;
+  size?: ProfilePreviewSize;
 };
 
-const StyledProfilePreview = styled.div`
+type StyledPartProps = {
+  size: ProfilePreviewSize;
+}
+
+const StyledProfilePreview = styled.div<StyledPartProps>`
   display: flex;
   align-items: center;
   & .ui.avatar.image {
     margin: 0 !important;
-    width: 40px !important;
-    height: 40px !important;
+    width: ${(props) => `${AVATAR_SIZES_PX[props.size]}px` } !important;
+    height: ${(props) => `${AVATAR_SIZES_PX[props.size]}px` } !important;
   }
 `;
 
-const Details = styled.div`
-  margin-left: 1rem;
+const Details = styled.div<StyledPartProps>`
+  margin-left: ${(props) => props.size === 'small' ? '0.5rem' : '1rem' };
   display: grid;
   grid-row-gap: 0.25rem;
   grid-template-columns: 100%;
 `;
 
-const DetailsHandle = styled.h4`
+const DetailsHandle = styled.h4<StyledPartProps>`
+  ${ props => props.size === 'small' && css`font-size: 1em` };
   margin: 0;
   font-weight: bold;
   color: #333;
 `;
 
-const DetailsID = styled.div`
-  color: #777;
-`;
-
 export default function ProfilePreview (
-  { id, avatar_uri, root_account, handle, link = false, children }: React.PropsWithChildren<ProfileItemProps>
+  { id, avatar_uri, root_account, handle, link = false, children, size = 'medium' }: React.PropsWithChildren<ProfileItemProps>
 ) {
   const Preview = (
-    <StyledProfilePreview>
+    <StyledProfilePreview size={size}>
       {avatar_uri.toString() ? (
         <Image src={avatar_uri.toString()} avatar floated='left' />
       ) : (
-        <IdentityIcon className='image' value={root_account.toString()} size={40} />
+        <IdentityIcon className='image' value={root_account.toString()} size={AVATAR_SIZES_PX[size]} />
       )}
-      <Details>
-        <DetailsHandle>{handle.toString()}</DetailsHandle>
-        { id !== undefined && <DetailsID>ID: {id.toString()}</DetailsID> }
+      <Details size={size}>
+        <DetailsHandle size={size}>{handle.toString()}</DetailsHandle>
+        { id !== undefined && <Label size={size}>ID <Label.Detail>{id.toString()}</Label.Detail></Label> }
         { children }
       </Details>
     </StyledProfilePreview>
@@ -69,17 +78,18 @@ export default function ProfilePreview (
 
 type ProfilePreviewFromStructProps = {
   profile: Membership;
-  link?: boolean;
   id?: number | MemberId;
+  link?: boolean;
+  size?: ProfilePreviewSize;
 };
 
 export function ProfilePreviewFromStruct (
-  { profile, link, id, children }: React.PropsWithChildren<ProfilePreviewFromStructProps>
+  { profile, children, ...passedProps }: React.PropsWithChildren<ProfilePreviewFromStructProps>
 ) {
   const { avatar_uri, root_account, handle } = profile;
 
   return (
-    <ProfilePreview {...{ avatar_uri, root_account, handle, link, id }}>
+    <ProfilePreview {...{ avatar_uri, root_account, handle, ...passedProps }}>
       {children}
     </ProfilePreview>
   );

+ 1 - 1
pioneer/packages/joy-utils/src/react/components/index.tsx

@@ -1,5 +1,5 @@
 export { default as Section } from './Section';
-export { default as TxButton } from './TxButton';
+export { default as TxButton, SemanticTxButton } from './TxButton';
 export { MutedSpan, MutedDiv } from './MutedText';
 export { FlexCenter } from './FlexCenter';
 export { default as PromiseComponent, Error, Loading } from './PromiseComponent';

+ 36 - 3
pioneer/packages/joy-utils/src/react/hocs/guards.tsx

@@ -1,9 +1,9 @@
 import React from 'react';
 import { Message } from 'semantic-ui-react';
 import { Link } from 'react-router-dom';
-
-import { withMulti } from '@polkadot/react-api/index';
-import { useMyMembership } from '../hooks';
+import { AccountId } from '@polkadot/types/interfaces';
+import { withMulti, withCalls } from '@polkadot/react-api/index';
+import { useMyMembership, useMyAccount } from '../hooks';
 import { componentName } from '../helpers';
 import { withMyAccount, MyAccountProps } from './accounts';
 
@@ -63,6 +63,32 @@ export function AccountRequired<P extends Record<string, unknown>> (Component: R
   return ResultComponent;
 }
 
+type OnlySudoProps = {
+  sudo?: AccountId;
+};
+
+function OnlySudo<P extends OnlySudoProps> (Component: React.ComponentType<P>) {
+  return function (props: P) {
+    const { sudo } = props;
+    if (!sudo) {
+      return <em>Loading sudo key...</em>;
+    }
+
+    const { state: { address: myAddress } } = useMyAccount();
+    const iAmSudo = myAddress === sudo.toString();
+
+    if (iAmSudo) {
+      return <Component {...props} />;
+    } else {
+      return (
+        <Message warning className='JoyMainStatus'>
+          <Message.Header>Only sudo can access this functionality.</Message.Header>
+        </Message>
+      );
+    }
+  };
+}
+
 // TODO: We could probably use withAccountRequired, which wouldn't pass any addiotional props, just like withMembershipRequired.
 // Just need to make sure those passed props are not used in the extended components (they probably aren't).
 export const withOnlyAccounts = <P extends MyAccountProps>(Component: React.ComponentType<P>): React.ComponentType<P> =>
@@ -73,3 +99,10 @@ export const withMembershipRequired = <P extends Record<string, unknown>> (Compo
 
 export const withOnlyMembers = <P extends MyAccountProps>(Component: React.ComponentType<P>): React.ComponentType<P> =>
   withMulti(Component, withMyAccount, withMembershipRequired);
+
+export const withOnlySudo = <P extends OnlySudoProps> (Component: React.ComponentType<P>) =>
+  withMulti(
+    Component,
+    withCalls(['query.sudo.key', { propName: 'sudo' }]),
+    OnlySudo
+  );

+ 0 - 15
pioneer/packages/old-apps/apps-routing/src/joy-forum.ts

@@ -1,15 +0,0 @@
-import { Routes } from './types';
-
-import Forum from '@polkadot/joy-forum/index';
-
-export default ([
-  {
-    Component: Forum,
-    display: {},
-    i18n: {
-      defaultValue: 'Forum'
-    },
-    icon: 'comment alternate outline',
-    name: 'forum'
-  }
-] as Routes);

+ 2 - 1
pioneer/tsconfig.json

@@ -4,7 +4,6 @@
     "build/**/*",
     "**/build/**/*",
     "packages/old-apps/**",
-    "packages/joy-forum/**/*",
     "packages/joy-help/**/*",
     "packages/joy-settings/**/*",
     "packages/joy-utils-old/**/*"
@@ -33,6 +32,8 @@
       "@polkadot/joy-utils/*": [ "packages/joy-utils/src/*" ],
       "@polkadot/joy-media/": [ "packages/joy-media/src/" ],
       "@polkadot/joy-media/*": [ "packages/joy-media/src/*" ],
+      "@polkadot/joy-forum/": [ "packages/joy-forum/src/" ],
+      "@polkadot/joy-forum/*": [ "packages/joy-forum/src/*" ],
       "@polkadot/apps/*": ["packages/apps/src/*"],
       "@polkadot/apps": ["packages/apps/src"],
       "@polkadot/apps-config/*": [ "packages/apps-config/src/*" ],

+ 9 - 9
types/src/forum.ts

@@ -219,12 +219,12 @@ export class Thread extends JoyStructCustom({
     return this.getField('nr_in_category')
   }
 
-  get moderation(): ModerationAction | undefined {
-    return this.getField('moderation').unwrapOr(undefined)
+  get moderation(): ModerationAction | null {
+    return this.getField('moderation').unwrapOr(null)
   }
 
   get moderated(): boolean {
-    return this.moderation !== undefined
+    return this.moderation !== null
   }
 
   get num_unmoderated_posts(): u32 {
@@ -288,12 +288,12 @@ export class Post extends JoyStructCustom({
     return this.getString('current_text')
   }
 
-  get moderation(): ModerationAction | undefined {
-    return this.getField('moderation').unwrapOr(undefined)
+  get moderation(): ModerationAction | null {
+    return this.getField('moderation').unwrapOr(null)
   }
 
   get moderated(): boolean {
-    return this.moderation !== undefined
+    return this.moderation !== null
   }
 
   get text_change_history(): VecPostTextChange {
@@ -337,12 +337,12 @@ export class Reply extends JoyStructCustom({
     return this.getString('text')
   }
 
-  get moderation(): ModerationAction | undefined {
-    return this.getField('moderation').unwrapOr(undefined)
+  get moderation(): ModerationAction | null {
+    return this.getField('moderation').unwrapOr(null)
   }
 
   get moderated(): boolean {
-    return this.moderation !== undefined
+    return this.moderation !== null
   }
 }