Przeglądaj źródła

Merge branch 'joystream-constantinople' into references-to-older-chains

Lezek123 4 lat temu
rodzic
commit
4f5232d556

+ 2 - 2
packages/app-staking/src/Actions/Accounts.tsx

@@ -8,7 +8,7 @@ import { I18nProps } from '@polkadot/react-components/types';
 import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
 import { KeyringSectionOption } from '@polkadot/ui-keyring/options/types';
 import { withCalls, withMulti } from '@polkadot/react-api/with';
-import { withAccountRequired } from '@polkadot/joy-utils/MyAccount';
+import { withOnlyAccounts } from '@polkadot/joy-utils/MyAccount';
 
 import React, { useState } from 'react';
 import styled from 'styled-components';
@@ -83,7 +83,7 @@ function Accounts({
 }
 
 export default withMulti(
-  withAccountRequired(
+  withOnlyAccounts(
     styled(Accounts)`
       .ui--CardGrid-buttons {
         text-align: right;

+ 9 - 4
packages/apps-routing/src/index.ts

@@ -25,14 +25,14 @@ import addressbook from './addressbook';
 // import dashboard from './dashboard';
 // import democracy from './democracy';
 // import explorer from './explorer';
-// import extrinsics from './extrinsics';
+import extrinsics from './extrinsics';
 // import genericAsset from './generic-asset';
 // import js from './js';
 // import parachains from './parachains';
-// import settings from './settings';
+import settings from './settings';
 import staking from './staking';
-// import storage from './storage';
-// import sudo from './sudo';
+import storage from './storage';
+import sudo from './sudo';
 // import toolbox from './toolbox';
 import transfer from './transfer';
 // import treasury from './treasury';
@@ -52,6 +52,11 @@ const routes: Routes = ([] as Routes).concat(
   election,
   proposals,
   null,
+  storage,
+  extrinsics,
+  sudo,
+  settings,
+  null,
   pages
 );
 const setup: Routing = {

+ 1 - 1
packages/joy-forum/src/CategoryList.tsx

@@ -12,7 +12,7 @@ import { ViewThread } from './ViewThread';
 import { MutedSpan } from '@polkadot/joy-utils/MutedText';
 import { UrlHasIdProps, CategoryCrumbs, Pagination, ThreadsPerPage } from './utils';
 import Section from '@polkadot/joy-utils/Section';
-import { JoyWarn } from '@polkadot/joy-utils/JoyWarn';
+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';

+ 3 - 3
packages/joy-forum/src/ForumSudo.tsx

@@ -14,7 +14,7 @@ import Section from '@polkadot/joy-utils/Section';
 import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext';
 import { withOnlySudo } from '@polkadot/joy-utils/Sudo';
 import { AccountId } from '@polkadot/types/interfaces';
-import { JoyWarn } from '@polkadot/joy-utils/JoyWarn';
+import { JoyError } from '@polkadot/joy-utils/JoyStatus';
 import AddressMini from '@polkadot/react-components/AddressMiniJoy';
 import { withForumCalls } from './calls';
 import { TxFailedCallback, TxCallback } from '@polkadot/react-components/Status/types';
@@ -212,10 +212,10 @@ function innerWithOnlyForumSudo<P extends LoadStructProps> (Component: React.Com
       return <Component {...props} />;
     } else {
       return (
-        <JoyWarn title={`Only forum sudo can access this functionality.`}>
+        <JoyError title={`Only forum sudo can access this functionality.`}>
           <div>Current forum sudo:</div>
           <div>{sudo ? <AddressMini value={sudo} /> : 'UNDEFINED'}</div>
-        </JoyWarn>
+        </JoyError>
       );
     }
   };

+ 1 - 1
packages/joy-forum/src/ViewReply.tsx

@@ -5,7 +5,7 @@ import { Segment, Button } from 'semantic-ui-react';
 
 import { Post, Category, Thread } from '@joystream/types/forum';
 import { Moderate } from './Moderate';
-import { JoyWarn } from '@polkadot/joy-utils/JoyWarn';
+import { JoyWarn } from '@polkadot/joy-utils/JoyStatus';
 import { useMyAccount } from '@polkadot/joy-utils/MyAccountContext';
 import { IfIAmForumSudo } from './ForumSudo';
 import { MemberPreview } from '@polkadot/joy-members/MemberPreview';

+ 1 - 1
packages/joy-forum/src/ViewThread.tsx

@@ -10,7 +10,7 @@ import { Pagination, RepliesPerPage, CategoryCrumbs } from './utils';
 import { ViewReply } from './ViewReply';
 import { Moderate } from './Moderate';
 import { MutedSpan } from '@polkadot/joy-utils/MutedText';
-import { JoyWarn } from '@polkadot/joy-utils/JoyWarn';
+import { JoyWarn } from '@polkadot/joy-utils/JoyStatus';
 import { withForumCalls } from './calls';
 import { withApi, withMulti } from '@polkadot/react-api';
 import { ApiProps } from '@polkadot/react-api/types';

+ 2 - 1
packages/joy-media/src/DiscoveryProvider.tsx

@@ -8,6 +8,7 @@ import { Vec } from '@polkadot/types';
 import { Url } from '@joystream/types/discovery'
 import ApiContext from '@polkadot/react-api/ApiContext';
 import { ApiProps } from '@polkadot/react-api/types';
+import { JoyInfo } from '@polkadot/joy-utils/JoyStatus';
 
 export type BootstrapNodes = {
   bootstrapNodes?: Url[],
@@ -155,7 +156,7 @@ export function withDiscoveryProvider(Component: React.ComponentType<DiscoveryPr
   return (props: React.PropsWithChildren<{}>) => {
     const discoveryProvider = useDiscoveryProvider()
     if (!discoveryProvider) {
-      return <em>Loading discovery provider. Please wait...</em>
+      return <JoyInfo title={`Please wait...`}>Loading discovery provider.</JoyInfo>
     }
 
     return (

+ 19 - 2
packages/joy-media/src/MediaView.tsx

@@ -3,6 +3,7 @@ import { MediaTransport } from './transport';
 import { MemberId } from '@joystream/types/members';
 import { useMyMembership } from '@polkadot/joy-utils/MyMembershipContext';
 import { useTransportContext } from './TransportContext';
+import { withMembershipRequired } from '@polkadot/joy-utils/MyAccount';
 
 type InitialPropsWithMembership<A> = A & {
   myAddress?: string
@@ -23,6 +24,9 @@ type BaseProps<A, B> = {
    * if values of such properties changed. 
    */
   triggers?: (keyof A)[]
+
+  /** Set `true` if only members should have access to this component. `false` by default. */
+  membersOnly?: boolean
 }
 
 function serializeTrigger(val: any): any {
@@ -36,7 +40,8 @@ function serializeTrigger(val: any): any {
 }
 
 export function MediaView<A = {}, B = {}> (baseProps: BaseProps<A, B>) {
-  return function (initialProps: A & B) {
+
+  function InnerView (initialProps: A & B) {
     const { component: Component, resolveProps, triggers = [], unresolvedView = null } = baseProps;
 
     const transport = useTransportContext();
@@ -73,8 +78,20 @@ export function MediaView<A = {}, B = {}> (baseProps: BaseProps<A, B>) {
     
     console.log('Rerender deps of Media View:', rerenderDeps);
 
+    const renderResolving = () => {
+      return unresolvedView
+        ? unresolvedView
+        : <div className='ui active centered inline loader' />
+    }
+
     return propsResolved
       ? <Component {...initialProps} {...resolvedProps} />
-      : unresolvedView;
+      : renderResolving()
   }
+
+  const { membersOnly = false } = baseProps
+
+  return membersOnly
+    ? withMembershipRequired(InnerView)
+    : InnerView
 }

+ 9 - 7
packages/joy-media/src/Upload.tsx

@@ -9,25 +9,26 @@ import { ApiProps } from '@polkadot/react-api/types';
 import { I18nProps } from '@polkadot/react-components/types';
 import { SubmittableResult } from '@polkadot/api';
 import { Option } from '@polkadot/types/codec';
-import { withMulti } from '@polkadot/react-api';
+import { withMulti, withApi } from '@polkadot/react-api';
 import { formatNumber } from '@polkadot/util';
 import { AccountId } from '@polkadot/types/interfaces';
 
 import translate from './translate';
 import { fileNameWoExt } from './utils';
 import { ContentId, DataObject } from '@joystream/types/media';
-import { MyAccountProps, withOnlyMembers } from '@polkadot/joy-utils/MyAccount';
+import { withMembershipRequired } from '@polkadot/joy-utils/MyAccount';
 import { DiscoveryProviderProps, withDiscoveryProvider } from './DiscoveryProvider';
 import TxButton from '@polkadot/joy-utils/TxButton';
 import IpfsHash from 'ipfs-only-hash';
 import { ChannelId } from '@joystream/types/content-working-group';
 import { EditVideoView } from './upload/EditVideo.view';
+import { JoyInfo } from '@polkadot/joy-utils/JoyStatus';
 import { IterableFile } from './IterableFile';
 
 const MAX_FILE_SIZE_MB = 500;
 const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
 
-type Props = ApiProps & I18nProps & MyAccountProps & DiscoveryProviderProps & {
+type Props = ApiProps & I18nProps & DiscoveryProviderProps & {
   channelId: ChannelId
   history?: History
   match: {
@@ -114,7 +115,7 @@ class Component extends React.PureComponent<Props, State> {
 
   private renderUploading () {
     const { file, newContentId, progress, error } = this.state;
-    if (!file || !file.name) return <em>Loading...</em>;
+    if (!file || !file.name) return <JoyInfo title='Loading...' />;
 
     const success = !error && progress >= 100;
     const { history, match: { params: { channelId } } } = this.props
@@ -133,7 +134,7 @@ class Component extends React.PureComponent<Props, State> {
   }
 
   private renderDiscovering () {
-    return <em>Contacting Storage Provider...</em>;
+    return <JoyInfo title={`Please wait...`}>Contacting storage provider.</JoyInfo>;
   }
 
   private renderProgress () {
@@ -238,7 +239,7 @@ class Component extends React.PureComponent<Props, State> {
   }
 
   private renderComputingHash() {
-    return <em>Processing your file. Please wait...</em>
+    return <JoyInfo title='Processing your file. Please wait...' />
   }
 
   private buildTxParams = () => {
@@ -349,6 +350,7 @@ class Component extends React.PureComponent<Props, State> {
 export const UploadWithRouter = withMulti(
   Component,
   translate,
-  withOnlyMembers,
+  withApi,
+  withMembershipRequired,
   withDiscoveryProvider
 )

+ 5 - 6
packages/joy-media/src/channels/ChannelHelpers.ts

@@ -1,5 +1,4 @@
 import { AccountId } from '@polkadot/types/interfaces';
-import { ChannelEntity } from "../entities/ChannelEntity";
 import { ChannelType } from "../schemas/channel/Channel";
 import { ChannelPublicationStatusAllValues } from "@joystream/types/content-working-group";
 
@@ -7,15 +6,15 @@ export const ChannelPublicationStatusDropdownOptions =
   ChannelPublicationStatusAllValues
     .map(x => ({ key: x, value: x, text: x }))
 
-export const isVideoChannel = (channel: ChannelEntity) => {
+export const isVideoChannel = (channel: ChannelType) => {
   return channel.content === 'Video';
 };
 
-export const isMusicChannel = (channel: ChannelEntity) => {
+export const isMusicChannel = (channel: ChannelType) => {
   return channel.content === 'Music';
 };
 
-export const isAccountAChannelOwner = (channel?: ChannelEntity, account?: AccountId | string): boolean => {
+export const isAccountAChannelOwner = (channel?: ChannelType, account?: AccountId | string): boolean => {
   return (channel && account) ? channel.roleAccount.eq(account) : false
 };
 
@@ -26,10 +25,10 @@ export function isPublicChannel(channel: ChannelType): boolean {
   );
 }
 
-export function isCensoredChannel(channel: ChannelEntity) : boolean {
+export function isCensoredChannel(channel: ChannelType): boolean {
   return channel.curationStatus == 'Censored'
 }
 
-export function isVerifiedChannel(channel: ChannelEntity) : boolean {
+export function isVerifiedChannel(channel: ChannelType): boolean {
   return channel.verified
 }

+ 2 - 1
packages/joy-media/src/channels/ChannelsByOwner.view.tsx

@@ -4,6 +4,7 @@ import { RouteComponentProps } from 'react-router';
 import { GenericAccountId } from '@polkadot/types';
 import { MediaView } from '../MediaView';
 import { ChannelsByOwnerProps, ChannelsByOwner } from './ChannelsByOwner';
+import { JoyError } from '@polkadot/joy-utils/JoyStatus';
 
 type Props = ChannelsByOwnerProps;
 
@@ -27,5 +28,5 @@ export const ChannelsByOwnerWithRouter = (props: Props & RouteComponentProps<any
     }
   }
 
-  return <em>ERROR: Invalid account id in URL: ${account}</em>;
+  return <JoyError title={`Invalid account address in URL`}>{account}</JoyError>
 }

+ 9 - 3
packages/joy-media/src/channels/EditChannel.tsx

@@ -12,10 +12,11 @@ import { MediaDropdownOptions } from '../common/MediaDropdownOptions';
 import { ChannelId, ChannelContentType, ChannelPublicationStatus, OptionalText } from '@joystream/types/content-working-group';
 import { newOptionalText, findFirstParamOfSubstrateEvent } from '@polkadot/joy-utils/index';
 import { useMyMembership } from '@polkadot/joy-utils/MyMembershipContext';
-import { ChannelPublicationStatusDropdownOptions } from './ChannelHelpers';
+import { ChannelPublicationStatusDropdownOptions, isAccountAChannelOwner } from './ChannelHelpers';
 import { TxCallback } from '@polkadot/react-components/Status/types';
 import { SubmittableResult } from '@polkadot/api';
 import { ChannelValidationConstraints } from '../transport';
+import { JoyError } from '@polkadot/joy-utils/JoyStatus';
 
 export type OuterProps = {
   history?: History,
@@ -53,7 +54,12 @@ const InnerForm = (props: MediaFormProps<OuterProps, FormValues>) => {
     resetForm
   } = props;
 
-  const { myAddress, myMemberId } = useMyMembership();
+  const { myAccountId, myMemberId } = useMyMembership();
+
+  if (entity && !isAccountAChannelOwner(entity, myAccountId)) {
+    return <JoyError title={`Only owner can edit channel`} />
+  }
+
   const { avatar } = values;
   const isNew = !entity;
 
@@ -86,7 +92,7 @@ const InnerForm = (props: MediaFormProps<OuterProps, FormValues>) => {
       // Create a new channel
 
       const channelOwner = myMemberId;
-      const roleAccount = myAddress;
+      const roleAccount = myAccountId;
       const contentType = new ChannelContentType(values.content);
 
       return [

+ 7 - 3
packages/joy-media/src/channels/EditChannel.view.tsx

@@ -3,11 +3,13 @@ import { RouteComponentProps } from 'react-router';
 import { MediaView } from '../MediaView';
 import { OuterProps, EditForm } from './EditChannel';
 import { ChannelId } from '@joystream/types/content-working-group';
+import { JoyError } from '@polkadot/joy-utils/JoyStatus';
 
 type Props = OuterProps;
 
 export const EditChannelView = MediaView<Props>({
   component: EditForm,
+  membersOnly: true,
   triggers: [ 'id' ],
   resolveProps: async (props) => {
     const { transport, id } = props;
@@ -15,9 +17,11 @@ export const EditChannelView = MediaView<Props>({
     const constraints = await transport.channelValidationConstraints()
     return { entity, constraints };
   }
-});
+})
 
-export const EditChannelWithRouter = (props: Props & RouteComponentProps<any>) => {
+type WithRouterProps = Props & RouteComponentProps<any>
+
+export const EditChannelWithRouter = (props: WithRouterProps) => {
   const { match: { params: { id }}} = props;
 
   if (id) {
@@ -28,5 +32,5 @@ export const EditChannelWithRouter = (props: Props & RouteComponentProps<any>) =
     }
   }
 
-  return <em>ERROR: Invalid channel id in URL: ${id}</em>;
+  return <JoyError title={`Invalid channel id in URL`}>{id}</JoyError>
 }

+ 3 - 2
packages/joy-media/src/channels/ViewChannel.tsx

@@ -8,6 +8,7 @@ import { ViewVideoChannel } from './ViewVideoChannel';
 import { ViewMusicChannel } from './ViewMusicChannel';
 import { toVideoPreviews } from '../video/VideoPreview';
 import { isVideoChannel, isMusicChannel } from './ChannelHelpers';
+import { JoyError } from '@polkadot/joy-utils/JoyStatus';
 
 export type ViewChannelProps = {
   id: ChannelId,
@@ -25,7 +26,7 @@ export function ViewChannel (props: ViewChannelProps) {
   const { channel, videos = [], albums = [], tracks = [] } = props;
 
   if (!channel) {
-    return <em>Channel is not found</em>;
+    return <JoyError title={`Channel was not found`} />
   }
 
   if (isVideoChannel(channel)) {
@@ -34,6 +35,6 @@ export function ViewChannel (props: ViewChannelProps) {
   } else if (isMusicChannel(channel)) {
     return <ViewMusicChannel channel={channel} albums={albums} tracks={tracks} />;
   } else {
-    return <em>Unsupported channel type: {channel.content}</em>
+    return <JoyError title={`Unsupported channel type`}>{channel.content}</JoyError>
   }
 }

+ 2 - 1
packages/joy-media/src/channels/ViewChannel.view.tsx

@@ -3,6 +3,7 @@ import { RouteComponentProps } from 'react-router';
 import { MediaView } from '../MediaView';
 import { ViewChannelProps, ViewChannel } from './ViewChannel';
 import { ChannelId } from '@joystream/types/content-working-group';
+import { JoyError } from '@polkadot/joy-utils/JoyStatus';
 
 type Props = ViewChannelProps;
 
@@ -28,5 +29,5 @@ export const ViewChannelWithRouter = (props: Props & RouteComponentProps<any>) =
     }
   }
 
-  return <em>ERROR: Invalid channel id in URL: ${id}</em>;
+  return <JoyError title={`Invalid channel id in URL`}>{id}</JoyError>
 }

+ 2 - 1
packages/joy-media/src/common/MediaPlayerView.tsx

@@ -15,6 +15,7 @@ import { VideoType } from '../schemas/video/Video';
 import { isAccountAChannelOwner } from '../channels/ChannelHelpers';
 import { ChannelEntity } from '../entities/ChannelEntity';
 import { useMyMembership } from '@polkadot/joy-utils/MyMembershipContext';
+import { JoyError } from '@polkadot/joy-utils/JoyStatus';
 
 const PLAYER_COMMON_PARAMS = {
   lang: 'en',
@@ -93,7 +94,7 @@ function Player(props: PlayerProps) {
     />;
   }
 
-  return <em>Unsupported type of content: {contentType}</em>;
+  return <JoyError title={`Unsupported type of content`}>{contentType}</JoyError>
 }
 
 function InnerComponent(props: MediaPlayerViewProps) {

+ 2 - 1
packages/joy-media/src/common/MediaPlayerWithResolver.tsx

@@ -13,6 +13,7 @@ import { DiscoveryProviderProps, withDiscoveryProvider } from '../DiscoveryProvi
 import { DataObjectStorageRelationshipId, DataObjectStorageRelationship } from '@joystream/types/media';
 import { Message } from 'semantic-ui-react';
 import { MediaPlayerView, RequiredMediaPlayerProps } from './MediaPlayerView';
+import { JoyInfo } from '@polkadot/joy-utils/JoyStatus';
 
 type Props = ApiProps & I18nProps & DiscoveryProviderProps & RequiredMediaPlayerProps;
 
@@ -125,7 +126,7 @@ function InnerComponent(props: Props) {
   }
 
   if (!resolvedAssetUrl) {
-    return <em>Resolving media content...</em>
+    return <JoyInfo title={`Please wait...`}>Resolving media content.</JoyInfo>
   }
 
   const playerProps = { ...props, contentType, resolvedAssetUrl }

+ 22 - 11
packages/joy-media/src/common/index.css

@@ -266,6 +266,7 @@ $borderColor: #e4e4e4;
 
 .ChannelPreview {
   display: flex;
+  align-items: normal;
 
   .ListOfChannels & {
     margin-bottom: 1rem;
@@ -282,30 +283,40 @@ $borderColor: #e4e4e4;
   .ChannelTitle {
     font-size: 1.5rem;
     font-weight: 500;
+    margin: 0;
     margin-top: .75rem;
-    margin-bottom: .5rem;
 
     .ChannelHeader & {
       margin-top: 1rem;
     }
   }
-  &.big .ChannelTitle {
-    font-size: 2rem;
-  }
-  &.small .ChannelTitle {
-    font-size: 1rem;
-    a {
-      color: $blackFont;
+  
+  &.small {
+    align-items: center;
+
+    .ChannelTitle {
+      margin-top: 0;
+      font-size: 1rem;
+      a {
+        color: $blackFont;
+      }
+    }
+    .ChannelAvatar {
+      margin-right: .5rem;
     }
   }
-  &.small .ChannelAvatar {
-    margin-right: .5rem;
+
+  &.big {
+    .ChannelTitle {
+      font-size: 2rem;
+    }
   }
 
   .ChannelSubtitle {
     color: $grayFont;
     font-size: .9rem;
     text-transform: uppercase;
+    margin-top: .5rem;
 
     .icon {
       margin-right: .5rem;
@@ -375,7 +386,7 @@ $borderColor: #e4e4e4;
     }
   }
 
-  .JoyPlayAlbum_Featured {
+  .JoyPlayAlbum_RightSidePanel {
     max-width: 450px;
   }
 }

+ 1 - 1
packages/joy-media/src/explore/PlayContent.tsx

@@ -83,7 +83,7 @@ export function PlayContent (props: Props) {
       </div>
     </div>
     {featuredAlbums.length > 0 &&
-      <div className='JoyPlayAlbum_Featured'>
+      <div className='JoyPlayAlbum_RightSidePanel'>
         <h3>Featured albums</h3>
         {featuredAlbums.map(x => <MusicAlbumPreview {...x} size={170} />)}
       </div>

+ 11 - 4
packages/joy-media/src/transport.ts

@@ -156,9 +156,16 @@ export abstract class MediaTransport extends Transport {
 
   abstract allMusicAlbums(): Promise<MusicAlbumType[]>
 
-  async videosByChannelId(channelId: ChannelId): Promise<VideoType[]> {
-    return (await this.allVideos())
-      .filter(x => channelId && channelId.eq(x.channelId))
+  async videosByChannelId(channelId: ChannelId, limit?: number, additionalFilter?: (x: VideoType) => boolean): Promise<VideoType[]> {
+    let videos = (await this.allVideos())
+      .filter(x => channelId && channelId.eq(x.channelId) && (additionalFilter || (() => true))(x))
+      .sort(x => -1 * x.id)
+
+    if (limit && limit > 0) {
+      videos = videos.slice(0, limit)
+    }
+
+    return videos
   }
 
   async videosByAccount(accountId: AccountId): Promise<VideoType[]> {
@@ -215,7 +222,7 @@ export abstract class MediaTransport extends Transport {
       .find(x =>
         insensitiveEq(x.value, 'Public')
       )?.id
-    
+
     const idsOfCuratedCS = (await this.allCurationStatuses())
       .filter(x =>
         insensitiveEq(x.value, 'Under review') ||

+ 9 - 5
packages/joy-media/src/upload/EditVideo.view.tsx

@@ -4,11 +4,13 @@ import { MediaView } from '../MediaView';
 import { OuterProps, EditForm } from './UploadVideo';
 import EntityId from '@joystream/types/versioned-store/EntityId';
 import { ChannelId } from '@joystream/types/content-working-group';
+import { JoyError } from '@polkadot/joy-utils/JoyStatus';
 
 type Props = OuterProps;
 
 export const EditVideoView = MediaView<Props>({
   component: EditForm,
+  membersOnly: true,
   triggers: [ 'id' ],
   resolveProps: async (props) => {
     const { transport, id, channelId } = props;
@@ -19,9 +21,11 @@ export const EditVideoView = MediaView<Props>({
     const opts = await transport.dropdownOptions();
     return { channel, mediaObjectClass, entityClass, entity, opts };
   }
-});
+})
 
-export const UploadVideoWithRouter = (props: Props & RouteComponentProps<any>) => {
+type WithRouterProps = Props & RouteComponentProps<any>
+
+export const UploadVideoWithRouter = (props: WithRouterProps) => {
   const { match: { params: { channelId }}} = props;
 
   if (channelId) {
@@ -32,10 +36,10 @@ export const UploadVideoWithRouter = (props: Props & RouteComponentProps<any>) =
     }
   }
 
-  return <em>ERROR: Invalid channel id in URL: ${channelId}</em>;
+  return <JoyError title={`Invalid channel id in URL`}>{channelId}</JoyError>
 }
 
-export const EditVideoWithRouter = (props: Props & RouteComponentProps<any>) => {
+export const EditVideoWithRouter = (props: WithRouterProps) => {
   const { match: { params: { id }}} = props;
 
   if (id) {
@@ -46,7 +50,7 @@ export const EditVideoWithRouter = (props: Props & RouteComponentProps<any>) =>
     }
   }
 
-  return <em>ERROR: Invalid video id in URL: ${id}</em>;
+  return <JoyError title={`Invalid video id in URL`}>{id}</JoyError>
 }
 
 export default EditVideoView;

+ 4 - 3
packages/joy-media/src/upload/UploadVideo.tsx

@@ -30,6 +30,7 @@ import { ParametrizedPropertyValue } from '@joystream/types/versioned-store/perm
 import { ParameterizedClassPropertyValues } from '@joystream/types/versioned-store/permissions/batching/operations';
 import { useMyMembership } from '@polkadot/joy-utils/MyMembershipContext';
 import { isAccountAChannelOwner } from '../channels/ChannelHelpers';
+import { JoyError } from '@polkadot/joy-utils/JoyStatus';
 
 /** Example: "2019-01-23" -> 1548201600 */
 function humanDateToUnixTs(humanFriendlyDate: string): number | undefined {
@@ -90,15 +91,15 @@ const InnerForm = (props: MediaFormProps<OuterProps, FormValues>) => {
   const { thumbnail } = values
 
   if (!mediaObjectClass) {
-    return <em>ERROR: "Media Object" entity class is undefined</em>
+    return <JoyError title={`"Media Object" entity class is undefined`} />
   }
 
   if (!entityClass) {
-    return <em>ERROR: Video entity class is undefined</em>
+    return <JoyError title={`"Video" entity class is undefined<`} />
   }
 
   if (entity && !isAccountAChannelOwner(entity.channel, myAccountId)) {
-    return <em>ERROR: Only owner can edit video</em>
+    return <JoyError title={`Only owner can edit video`} />
   }
 
   // Next consts are used in tx params:

+ 32 - 13
packages/joy-media/src/video/PlayVideo.tsx

@@ -12,24 +12,37 @@ import { printExplicit, printReleaseDate, printLanguage } from '../entities/Enti
 import { MediaObjectType } from '../schemas/general/MediaObject';
 import { MediaPlayerWithResolver } from '../common/MediaPlayerWithResolver';
 import { ContentId } from '@joystream/types/media';
+import { JoyError } from '@polkadot/joy-utils/JoyStatus';
 
 export type PlayVideoProps = {
+  channel?: ChannelEntity
   mediaObject?: MediaObjectType
   id: EntityId
   video?: VideoType
-  channel?: ChannelEntity
+  moreChannelVideos?: VideoType[]
   featuredVideos?: VideoType[]
-};
+}
+
+type ListOfVideoPreviewProps = {
+  videos?: VideoType[]
+}
+
+function VertialListOfVideoPreviews(props: ListOfVideoPreviewProps) {
+  const { videos = [] } = props
+  return <>{videos.map((video) =>
+    <VideoPreview key={`VideoPreview-${video.id}`} {...video} size='small' orientation='horizontal' withChannel />
+  )}</>
+}
 
 export function PlayVideo (props: PlayVideoProps) {
-  const { mediaObject, video, channel, featuredVideos = [] } = props;
+  const { channel, mediaObject, video, moreChannelVideos = [], featuredVideos = [] } = props;
 
   if (!mediaObject || !video) {
-    return <em>Video was not found</em>
+    return <JoyError title={`Video was not found`} />
   }
 
   if (!channel) {
-    return <em>Channel was not found</em>
+    return <JoyError title={`Channel was not found`} />
   }
 
   const metaField = (field: VideoGenericProp, value: React.ReactNode | string) => (
@@ -98,13 +111,19 @@ export function PlayVideo (props: PlayVideoProps) {
       </div>
     </div>
 
-    {featuredVideos.length > 0 &&
-      <div className='JoyPlayAlbum_Featured'>
-        <h3 style={{ marginBottom: '1rem' }}>Featured videos</h3>
-        {featuredVideos.map((x) =>
-          <VideoPreview key={`VideoPreview-${x.id}`} {...x} size='small' orientation='horizontal' withChannel />
-        )}
-      </div>
-    }
+    <div className='JoyPlayAlbum_RightSidePanel'>
+      {featuredVideos.length > 0 &&
+        <div>
+          <h3 style={{ marginBottom: '1rem' }}>Featured videos</h3>
+          <VertialListOfVideoPreviews videos={featuredVideos} />
+        </div>
+      }
+      {moreChannelVideos.length > 0 &&
+        <div style={{ marginTop: '1rem' }}>
+          <h3 style={{ marginBottom: '1rem' }}>More from this channel</h3>
+          <VertialListOfVideoPreviews videos={moreChannelVideos} />
+        </div>
+      }
+    </div>
   </div>;
 }

+ 4 - 2
packages/joy-media/src/video/PlayVideo.view.tsx

@@ -4,6 +4,7 @@ import { MediaView } from '../MediaView';
 import { PlayVideoProps, PlayVideo } from './PlayVideo';
 import { ChannelId } from '@joystream/types/content-working-group';
 import { EntityId } from '@joystream/types/versioned-store';
+import { JoyError } from '@polkadot/joy-utils/JoyStatus';
 
 type Props = PlayVideoProps;
 
@@ -18,10 +19,11 @@ export const PlayVideoView = MediaView<Props>({
 
     const channelId = new ChannelId(video.channelId)
     const channel = await transport.channelById(channelId)
+    const moreChannelVideos = (await transport.videosByChannelId(channelId, 5, x => x.id !== video.id));
     const featuredVideos = await transport.featuredVideos()
     const mediaObject = video.object
 
-    return { mediaObject, video, channel, featuredVideos }
+    return { channel, mediaObject, video, moreChannelVideos, featuredVideos }
   }
 });
 
@@ -36,5 +38,5 @@ export const PlayVideoWithRouter = (props: Props & RouteComponentProps<any>) =>
     }
   }
 
-  return <em>ERROR: Invalid video id in URL: ${id}</em>;
+  return <JoyError title={`Invalid video id in URL`}>{id}</JoyError>
 }

+ 42 - 0
packages/joy-utils/src/JoyStatus.tsx

@@ -0,0 +1,42 @@
+import React from 'react';
+import { Message } from 'semantic-ui-react';
+import { nonEmptyStr } from '.';
+
+type BaseProps = {
+  title?: React.ReactNode
+  children?: React.ReactNode
+}
+
+type Variants = {
+  info?: boolean
+  success?: boolean
+  warning?: boolean
+  error?: boolean
+}
+
+type Props = BaseProps & Variants
+
+export const JoyStatus = (props: Props) => {
+  const { title, children, ...variants } = props
+
+  return (
+    <Message className='JoyMainStatus' {...variants}>
+      {nonEmptyStr(title) &&
+        <Message.Header>{title}</Message.Header>
+      }
+      {children &&
+        <div style={{ marginTop: '1rem' }}>
+          {children}
+        </div>
+      }
+    </Message>
+  )
+}
+
+export const JoyInfo = (props: BaseProps) => <JoyStatus {...props} info />
+
+export const JoySuccess = (props: BaseProps) => <JoyStatus {...props} success />
+
+export const JoyWarn = (props: BaseProps) => <JoyStatus {...props} warning />
+
+export const JoyError = (props: BaseProps) => <JoyStatus {...props} error />

+ 0 - 16
packages/joy-utils/src/JoyWarn.tsx

@@ -1,16 +0,0 @@
-import React from 'react';
-import { Message } from 'semantic-ui-react';
-
-type Props = {
-  title: React.ReactNode
-  children?: React.ReactNode
-};
-
-export const JoyWarn = ({ title, children }: Props) => (
-  <Message warning className='JoyMainStatus'>
-    <Message.Header>{title}</Message.Header>
-    {children && <div style={{ marginTop: '1rem' }}>
-      {children}
-    </div>}
-  </Message>
-);

+ 27 - 22
packages/joy-utils/src/MyAccount.tsx

@@ -272,30 +272,30 @@ export const withMyAccount = <P extends MyAccountProps>(Component: React.Compone
     withCurationActor
   );
 
-function OnlyMembers<P extends MyAccountProps>(Component: React.ComponentType<P>) {
-  return function(props: P) {
-    const { myMemberIdChecked, iAmMember } = props;
+export function MembershipRequired<P extends {}>(Component: React.ComponentType<P>): React.ComponentType<P> {
+  return function (props: P) {
+    const { myMemberIdChecked, iAmMember } = useMyMembership()
 
     if (!myMemberIdChecked) {
       return <em>Loading...</em>;
     } else if (iAmMember) {
       return <Component {...props} />;
-    } else {
-      return (
-        <Message warning className="JoyMainStatus">
-          <Message.Header>Only members can access this functionality.</Message.Header>
-          <div style={{ marginTop: '1rem' }}>
-            <Link to={`/members/edit`} className="ui button orange">
-              Register here
-            </Link>
-            <span style={{ margin: '0 .5rem' }}> or </span>
-            <Link to={`/accounts`} className="ui button">
-              Change key
-            </Link>
-          </div>
-        </Message>
-      );
     }
+
+    return (
+      <Message warning className="JoyMainStatus">
+        <Message.Header>Only members can access this functionality.</Message.Header>
+        <div style={{ marginTop: '1rem' }}>
+          <Link to={`/members/edit`} className="ui button orange">
+            Register here
+          </Link>
+          <span style={{ margin: '0 .5rem' }}> or </span>
+          <Link to={`/accounts`} className="ui button">
+            Change key
+          </Link>
+        </div>
+      </Message>
+    );
   };
 }
 
@@ -320,8 +320,13 @@ export function AccountRequired<P extends {}>(Component: React.ComponentType<P>)
   };
 }
 
-export const withOnlyMembers = <P extends MyAccountProps>(Component: React.ComponentType<P>) =>
-  withMulti(Component, withMyAccount, AccountRequired, OnlyMembers);
-
-export const withAccountRequired = <P extends MyAccountProps>(Component: React.ComponentType<P>) =>
+// 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> =>
   withMulti(Component, withMyAccount, AccountRequired);
+
+export const withMembershipRequired = <P extends {}> (Component: React.ComponentType<P>): React.ComponentType<P> =>
+  withMulti(Component, AccountRequired, MembershipRequired)
+
+export const withOnlyMembers = <P extends MyAccountProps>(Component: React.ComponentType<P>): React.ComponentType<P> =>
+  withMulti(Component, withMyAccount, withMembershipRequired);